はじめに
Flutterで明示的アニメーション(Explicit Animations)を実装している際、「1回目は綺麗に動くのに、2回目以降ボタンを押してもアニメーションせずに一瞬で色が切り替わってしまう」という現象に遭遇しました。
今回は、DecoratedBoxTransition を例に、この現象が発生する原因と正しい解決方法を記事にします。
発生する問題
以下は、ボタンを押すたびにコンテナの色がランダムに変化するアニメーションを意図したコードです。
しかし、このコードでは最初の1回しかアニメーションが実行されず、2回目以降は色が一瞬でパッと切り替わってしまいます。
import 'dart:math';
import 'package:flutter/material.dart';
class ColorPage extends StatefulWidget {
const ColorPage({super.key});
@override
State<ColorPage> createState() => _ColorPageState();
}
class _ColorPageState extends State<ColorPage>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
final List<Color> _colors = [
Colors.blue,
Colors.red,
Colors.green,
Colors.yellow,
];
Color _currentColor = Colors.blue;
Color _nextColor = Colors.blue;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Color Animation')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
DecoratedBoxTransition(
decoration: _controller.drive(
DecorationTween(
begin: BoxDecoration(color: _currentColor),
end: BoxDecoration(color: _nextColor),
),
),
child: const SizedBox(width: 100, height: 100),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
final randomIndex = Random().nextInt(_colors.length);
final randomColor = _colors[randomIndex];
setState(() {
_currentColor = _nextColor;
_nextColor = randomColor;
});
_controller.forward(); // ⚠️ 2回目以降、ここでアニメーションしない
},
child: const Text('Animate Color'),
),
],
),
),
);
}
}
原因:AnimationControllerは「1.0」で止まっている
なぜ2回目以降、アニメーションが即座に切り替わってしまうのでしょうか?
原因は、Flutterのアニメーション進捗の管理方法にあります。
- Flutterのアニメーションの進行具合は、内部的に 0.0 から 1.0 の double 型の値で管理されています。
-
_controller.forward()を呼び出すと、値は 0.0 から 1.0 に向かって進みます。
最初の1回目のアニメーションが終わった時点で、_controller の値はすでに最大値(1.0)に達しています。
そのため、2回目にボタンを押して再び _controller.forward() を呼び出しても、「すでに1.0(終了状態)なので、これ以上進まない」状態になってしまいます。
DecoratedBoxTransition は AnimationController の値を使って描画を行うため、コントローラーの値が 1.0 のままだと、新しい DecorationTween を設定しても即座に終了状態として描画されてしまうのです。
解決策:アニメーションをリセットする
2回目以降もボタンを押すたびに最初からアニメーションを再生するには、onPressed の中でコントローラーの値を初期状態(0.0)に戻す必要があります。
修正方法は以下の2通りがあります。
修正パターン1:reset() してから forward() する
forward() を呼ぶ前に _controller.reset() を挟むことで、アニメーションの進捗を明示的に 0.0 に戻します。
ElevatedButton(
onPressed: () {
final randomIndex = Random().nextInt(_colors.length);
final randomColor = _colors[randomIndex];
setState(() {
_currentColor = _nextColor;
_nextColor = randomColor;
});
_controller.reset(); // 1. コントローラーを初期状態(0.0)に戻す
_controller.forward(); // 2. 再度アニメーションを開始する
},
child: const Text('Animate Color'),
),
修正パターン2:forward(from: 0.0) を使う
forward メソッドの引数 from を指定すると、リセットと再生を同時に行うことができます。コードをスッキリ書きたいときはこちらが便利です。
ElevatedButton(
onPressed: () {
final randomIndex = Random().nextInt(_colors.length);
final randomColor = _colors[randomIndex];
setState(() {
_currentColor = _nextColor;
_nextColor = randomColor;
});
// 0.0(最初)からアニメーションを強制的に再生する
_controller.forward(from: 0.0);
},
child: const Text('Animate Color'),
),
まとめ
-
AnimationController.forward()は、現在の値から 1.0 に向かって進む - アニメーションが終了した後は、値が 1.0 のまま保持される
- 繰り返しアニメーションを再生したい場合は、
_controller.reset()を呼ぶか、_controller.forward(from: 0.0)を使用して進捗をリセットする必要がある