Flutter明示的アニメーション

明示的アニメーションで、もっと細かく制御できます

目次

暗黙的 vs 明示的 の違い

暗黙的アニメーション:

// 値を変えるだけ
AnimatedContainer(
  width: _isExpanded ? 200 : 100,  // ← 値を変える
  duration: Duration(milliseconds: 300),
)

特徴:

  • ✅ 簡単
  • ❌ 細かい制御はできない
  • ❌ 途中で止められない

明示的アニメーション:

// アニメーションを直接操作
AnimationController controller = AnimationController(
  vsync: this,
  duration: Duration(seconds: 2),
);

controller.forward();  // 再生
controller.reverse();  // 逆再生
controller.stop();     // 停止
controller.repeat();   // 繰り返し

特徴:

  • ✅ 細かく制御できる
  • ✅ 途中で止められる
  • ✅ 繰り返し・逆再生が簡単
  • ❌ ちょっと複雑

AnimationControllerは、アニメーションを操作するリモコンのようなもの!
比喩で言うと: 音楽プレーヤーのようなもの 🎵
forward() = 再生 ▶️
reverse() = 巻き戻し ⏪
stop() = 停止 ⏸️
repeat() = リピート 🔁

基本的な構造

ステップ1: AnimationController を作る

class MyPage extends StatefulWidget {
  @override
  _MyPageState createState() => _MyPageState();
}

class _MyPageState extends State<MyPage>
    with SingleTickerProviderStateMixin {  // ← これが必要!
  
  late AnimationController _controller;
  
  @override
  void initState() {
    super.initState();
    
    // コントローラーを作成
    _controller = AnimationController(
      vsync: this,  // アニメーションのタイミング調整
      duration: Duration(seconds: 2),  // 2秒かけて 0.0 → 1.0
    );
  }
  
  @override
  void dispose() {
    _controller.dispose();  // 必ず破棄!
    super.dispose();
  }
}

重要ポイント:

SingleTickerProviderStateMixin を追加
initState で作成
dispose で破棄(必須!)

ステップ2: Tween で値の範囲を決める

Tweenは、「0.0〜1.0」を「実際の値」に変換するもの!

// 0.0〜1.0 を 0〜300 に変換
Animation<double> _animation = Tween<double>(
  begin: 0.0,    // 開始値
  end: 300.0,    // 終了値
).animate(_controller);

比喩: 翻訳機のようなもの

入力: 0.0〜1.0 (コントローラーの値)
出力: 0〜300 (実際に使う値)

ステップ3: 値を使う

@override
Widget build(BuildContext context) {
  return AnimatedBuilder(
    animation: _controller,
    builder: (context, child) {
      return Container(
        width: _animation.value,  // ← アニメーション中の値
        height: 100,
        color: Colors.blue,
      );
    },
  );
}

実践例1: サイズが変わるボックス

import 'package:flutter/material.dart';

class SizeAnimationDemo extends StatefulWidget {
  @override
  _SizeAnimationDemoState createState() => _SizeAnimationDemoState();
}

class _SizeAnimationDemoState extends State<SizeAnimationDemo>
    with SingleTickerProviderStateMixin {
  
  late AnimationController _controller;
  late Animation<double> _animation;
  
  @override
  void initState() {
    super.initState();
    
    // コントローラー作成
    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 2),
    );
    
    // Tween 設定
    _animation = Tween<double>(
      begin: 50.0,   // 開始: 50
      end: 300.0,    // 終了: 300
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInOut,  // カーブも設定できる!
    ));
  }
  
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Size Animation')),
      body: Center(
        child: AnimatedBuilder(
          animation: _controller,
          builder: (context, child) {
            return Container(
              width: _animation.value,   // アニメーション中の値
              height: _animation.value,
              decoration: BoxDecoration(
                color: Colors.blue,
                borderRadius: BorderRadius.circular(20),
              ),
              child: Center(
                child: Text(
                  '${_animation.value.toInt()}',
                  style: TextStyle(color: Colors.white, fontSize: 24),
                ),
              ),
            );
          },
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: () => _controller.forward(),  // 再生
            child: Icon(Icons.play_arrow),
          ),
          SizedBox(height: 10),
          FloatingActionButton(
            onPressed: () => _controller.reverse(),  // 逆再生
            child: Icon(Icons.fast_rewind),
          ),
          SizedBox(height: 10),
          FloatingActionButton(
            onPressed: () => _controller.reset(),  // リセット
            child: Icon(Icons.refresh),
          ),
        ],
      ),
    );
  }
}

動き:

▶️ ボタン: 50 → 300 に拡大
⏪ ボタン: 300 → 50 に縮小
🔄 ボタン: 50 にリセット

実践例2: 回転するアイコン

class RotationDemo extends StatefulWidget {
  @override
  _RotationDemoState createState() => _RotationDemoState();
}

class _RotationDemoState extends State<RotationDemo>
    with SingleTickerProviderStateMixin {
  
  late AnimationController _controller;
  
  @override
  void initState() {
    super.initState();
    
    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 2),
    );
  }
  
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Rotation Animation')),
      body: Center(
        child: AnimatedBuilder(
          animation: _controller,
          builder: (context, child) {
            return Transform.rotate(
              angle: _controller.value * 2 * 3.14159,  // 0〜2π (1回転)
              child: Icon(
                Icons.refresh,
                size: 100,
                color: Colors.blue,
              ),
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          if (_controller.isAnimating) {
            _controller.stop();  // 停止
          } else {
            _controller.repeat();  // 繰り返し回転
          }
        },
        child: Icon(
          _controller.isAnimating ? Icons.pause : Icons.play_arrow,
        ),
      ),
    );
  }
}

動き: アイコンがグルグル回転! 🔄

実践例3: 複数のアニメーションを同時に

class MultiAnimationDemo extends StatefulWidget {
  @override
  _MultiAnimationDemoState createState() => _MultiAnimationDemoState();
}

class _MultiAnimationDemoState extends State<MultiAnimationDemo>
    with SingleTickerProviderStateMixin {
  
  late AnimationController _controller;
  late Animation<double> _sizeAnimation;
  late Animation<double> _opacityAnimation;
  late Animation<Color?> _colorAnimation;
  
  @override
  void initState() {
    super.initState();
    
    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 2),
    );
    
    // サイズ: 50 → 200
    _sizeAnimation = Tween<double>(
      begin: 50.0,
      end: 200.0,
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInOut,
    ));
    
    // 透明度: 0.3 → 1.0
    _opacityAnimation = Tween<double>(
      begin: 0.3,
      end: 1.0,
    ).animate(_controller);
    
    // 色: 赤 → 青
    _colorAnimation = ColorTween(
      begin: Colors.red,
      end: Colors.blue,
    ).animate(_controller);
  }
  
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Multi Animation')),
      body: Center(
        child: AnimatedBuilder(
          animation: _controller,
          builder: (context, child) {
            return Opacity(
              opacity: _opacityAnimation.value,  // 透明度
              child: Container(
                width: _sizeAnimation.value,   // サイズ
                height: _sizeAnimation.value,
                decoration: BoxDecoration(
                  color: _colorAnimation.value,  // 色
                  borderRadius: BorderRadius.circular(20),
                ),
                child: Center(
                  child: Text(
                    'Hello',
                    style: TextStyle(color: Colors.white, fontSize: 24),
                  ),
                ),
              ),
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          if (_controller.status == AnimationStatus.completed) {
            _controller.reverse();
          } else {
            _controller.forward();
          }
        },
        child: Icon(Icons.play_arrow),
      ),
    );
  }
}

動き: サイズ・色・透明度が同時に変化! ✨

Tween の種類

Tween用途
Tween数値サイズ、位置、回転
ColorTween赤 → 青
AlignmentTween位置左 → 右
BorderRadiusTween角丸0 → 50
DecorationTween装飾影、グラデーション
TextStyleTweenテキストフォントサイズ、色

🎮 コントローラーの操作方法

// ========================================
// 基本操作
// ========================================

_controller.forward();   // 0.0 → 1.0 に進む
_controller.reverse();   // 1.0 → 0.0 に戻る
_controller.reset();     // 0.0 にリセット
_controller.stop();      // 停止

// ========================================
// 繰り返し
// ========================================

_controller.repeat();              // 無限に繰り返す
_controller.repeat(reverse: true); // 往復を繰り返す
_controller.repeat(min: 0.2, max: 0.8);  // 範囲指定

// ========================================
// 特定の値にアニメーション
// ========================================

_controller.animateTo(0.5);  // 0.5 までアニメーション
_controller.animateBack(0.0); // 0.0 に戻る

// ========================================
// 値を直接設定(アニメーションなし)
// ========================================

_controller.value = 0.5;  // 即座に 0.5 に

// ========================================
// ステータス確認
// ========================================

_controller.isAnimating;   // アニメーション中?
_controller.isCompleted;   // 完了した?
_controller.isDismissed;   // 最初の状態?

アニメーションの状態を監視

@override
void initState() {
  super.initState();
  
  _controller = AnimationController(
    vsync: this,
    duration: Duration(seconds: 2),
  );
  
  // ========================================
  // 状態変化を監視
  // ========================================
  _controller.addStatusListener((status) {
    if (status == AnimationStatus.completed) {
      print('アニメーション完了!');
      // 完了後の処理
    } else if (status == AnimationStatus.dismissed) {
      print('最初の状態に戻った');
    }
  });
  
  // ========================================
  // 値の変化を監視
  // ========================================
  _controller.addListener(() {
    print('現在の値: ${_controller.value}');
    // 値が変わるたびに呼ばれる
  });
}

AnimationStatus の種類:

forward・・・・・・・・・0.0 → 1.0 に進行中
reverse・・・・・・・・・1.0 → 0.0 に逆再生中
completed・・・・・・・1.0 に到達(完了)
dismissed・・・・・・・0.0 に到達(最初)

実践例4: スライドインするメッセージ

class SlideInMessage extends StatefulWidget {
  final String message;
  
  const SlideInMessage({required this.message});
  
  @override
  _SlideInMessageState createState() => _SlideInMessageState();
}

class _SlideInMessageState extends State<SlideInMessage>
    with SingleTickerProviderStateMixin {
  
  late AnimationController _controller;
  late Animation<Offset> _offsetAnimation;
  
  @override
  void initState() {
    super.initState();
    
    _controller = AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 500),
    );
    
    // オフセット: 右外 → 中央
    _offsetAnimation = Tween<Offset>(
      begin: Offset(1.0, 0.0),  // 右側の外
      end: Offset.zero,          // 中央
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeOut,
    ));
    
    // 表示開始
    _controller.forward();
    
    // 3秒後に消える
    Future.delayed(Duration(seconds: 3), () {
      if (mounted) {
        _controller.reverse();
      }
    });
  }
  
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return SlideTransition(
      position: _offsetAnimation,
      child: Container(
        padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12),
        decoration: BoxDecoration(
          color: Colors.green,
          borderRadius: BorderRadius.circular(8),
        ),
        child: Text(
          widget.message,
          style: TextStyle(color: Colors.white),
        ),
      ),
    );
  }
}

// 使い方
void showSuccessMessage() {
  setState(() {
    // メッセージを表示
  });
}

効果: メッセージが右からスライドイン! 📨

注意点とコツ

必ず dispose する!

// ❌ 危険 - メモリリーク!
@override
void dispose() {
  // _controller.dispose() を忘れた!
  super.dispose();
}

// ✅ 正しい
@override
void dispose() {
  _controller.dispose();  // 必須!
  super.dispose();
}

忘れると: メモリリークでアプリが重くなる! ⚠️

SingleTickerProviderStateMixin を忘れずに

// ❌ エラー
class _MyPageState extends State<MyPage> {
  late AnimationController _controller;
  
  @override
  void initState() {
    _controller = AnimationController(
      vsync: this,  // ← エラー!
    );
  }
}

// ✅ 正しい
class _MyPageState extends State<MyPage>
    with SingleTickerProviderStateMixin {  // ← これが必要!
  // ...
}

複数のコントローラーなら TickerProviderStateMixin

// 複数のコントローラー
class _MyPageState extends State<MyPage>
    with TickerProviderStateMixin {  // Single じゃない!
  
  late AnimationController _controller1;
  late AnimationController _controller2;
  late AnimationController _controller3;
  
  // ...
}

AnimatedBuilder で効率化

// ❌ 非効率 - 全体が再描画される
@override
Widget build(BuildContext context) {
  return Column(
    children: [
      Transform.rotate(
        angle: _controller.value * 2 * 3.14159,
        child: Icon(Icons.refresh),
      ),
      Text('たくさんの他のWidget'),
      ListView(...),  // ← これも毎回再描画される!
    ],
  );
}

// ✅ 効率的 - 必要な部分だけ
@override
Widget build(BuildContext context) {
  return Column(
    children: [
      AnimatedBuilder(
        animation: _controller,
        builder: (context, child) {
          return Transform.rotate(
            angle: _controller.value * 2 * 3.14159,
            child: Icon(Icons.refresh),
          );
        },
      ),
      Text('たくさんの他のWidget'),  // ← 再描画されない!
      ListView(...),
    ],
  );
}

まとめ: 明示的アニメーション


基本の流れ:

  1. SingleTickerProviderStateMixin を追加
  2. AnimationController を作成
  3. Tween で値の範囲を設定
  4. AnimatedBuilder で使用
  5. dispose で破棄
_controller.forward();   // 再生
_controller.reverse();   // 逆再生
_controller.repeat();    // 繰り返し
_controller.stop();      // 停止

いつ使う?

細かい制御
回転・繰り返し
ローディング
複雑な動き

一言まとめ:
明示的アニメーションはAnimationControllerで細かく制御!
forward()で再生、
reverse()で逆再生、
repeat()で繰り返し。
Tweenで値の範囲を設定して、AnimatedBuilderで使う。
dispose()を忘れずに!
より自由度の高いアニメーションが作れる! 🎬✨

目次