明示的アニメーションで、もっと細かく制御できます
目次
暗黙的 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(); // 繰り返し
特徴:
- ✅ 細かく制御できる
- ✅ 途中で止められる
- ✅ 繰り返し・逆再生が簡単
- ❌ ちょっと複雑
基本的な構造
ステップ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(...),
],
);
}
まとめ: 明示的アニメーション
基本の流れ:
- SingleTickerProviderStateMixin を追加
- AnimationController を作成
- Tween で値の範囲を設定
- AnimatedBuilder で使用
- dispose で破棄
_controller.forward(); // 再生
_controller.reverse(); // 逆再生
_controller.repeat(); // 繰り返し
_controller.stop(); // 停止
いつ使う?
細かい制御
回転・繰り返し
ローディング
複雑な動き
一言まとめ:
明示的アニメーションはAnimationControllerで細かく制御!
forward()で再生、
reverse()で逆再生、
repeat()で繰り返し。
Tweenで値の範囲を設定して、AnimatedBuilderで使う。
dispose()を忘れずに!
より自由度の高いアニメーションが作れる! 🎬✨
