対象者
- Flutterで色をアニメーションで変えたい人
はじめに
ちょっとボタンを目立つようにしたいなぁと思い、アニメーションでボタンの色が変更するようにしようとしました。検索しましたが、意外とうまくいく例がありませんでした。色々やって自分が納得できるものが完成しましたので、以下の二通りで紹介します。
-
AnimationController + AnimatedBuilder
複雑な処理ができる、はず。 -
TweenAnimationBuilder
単純
実施するソース
AnimationController + AnimatedBuilder
late final _controller = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
);
late final Animation<Color?> _animatinColor = ColorTween(
begin: Colors.blue,
end: Colors.red,
).animate(_controller);
bool _isForward = false;
AnimationControllerでアニメーションのコントローラーを定義する。アニメーションを再生させたり、逆再生させたりする。使用後、disposeする必要がある(StatefuleWidgetのdisposeで実行する)。
ColorTweenで値の変化を定義する。今回は、青色から赤色へ変化する一連の値が生成されている。
_isForwardでアニメーションの再生状態を記録しておく。アニメーションを再生すればtrueになり、逆再生するとfalseになる。アニメーションを開始するときに、forwardとrewardでどちらを実施するかに使う。
AnimatedBuilder(
animation: _animatinColor,
builder: (context, child) {
return FilledButton(
onPressed: () {
if (_isForward) {
_controller.reverse();
} else {
_controller.forward();
}
setState(() {
_isForward = !_isForward;
});
},
style: FilledButton.styleFrom(
backgroundColor: _animatinColor.value),
child: child,
);
},
child: Text(_isForward ? '青にする' : '赤にする')
),
- AnimatedBuilder(animation: _animatinColor,
AnimatinBuilderで実装する。使用する変化する値として、色の値(_animatinColor)を渡す。
- FilledButton.onPress
_isForwardでアニメーションの再生状態が記録されているので、状態によってアニメーションを再生・逆再生する。
再生後に _isForwardをひっくり返して、再生状態を記録する。
-
FilledButton.style: FilledButton.styleFrom( backgroundColor: _animatinColor.value)
ボタンの背景色の設定に色の値(_animatinColor)を設定する。こうすることで、アニメーション再生時に値が変化していくので、一緒に色が変化していく。
-
child
さて、child がAnimatedBuilderとFilledButtonの二箇所にあります。さらに、FilledButtonには、builderの引数のchildを渡している。何でだろうと思いました。
簡単にしてしまうと、FilledButtonの下のchildにそのまま子Widgetを書いてしまっても大丈夫です。
ただ、AnimatedBuilderにchildを定義する優位性は、WidgetTreeを分けて、アニメーション部分だけ更新できることです。どういうことかと言いますと、ここの場合Textがあります。このTextですが、FilledButtonの下に書くと、アニメーションするごとにWidgetを再生します。しかしAnimatedBuilderのchildに定義することで、アニメーションによって再作成されることなく、アニメーションの最中も前後も同じTextを使い続けることができます。(この例だと_isForwardが変わった時点で、1度再生成されます。しかしアニメーション中は再生成されません)
TweenAnimationBuilder
TweenAnimationBuilderを使った実装です。AnimationControllerがなくて、簡単に書けます、一度書いたら(苦労しました)。
Color _bottomColor = Colors.blue;
TweenAnimationBuilder(
tween: ColorTween(end: _bottomColor),
duration: const Duration(seconds: 1),
builder: (BuildContext context, Color? color, Widget? child) {
return FilledButton(
style: FilledButton.styleFrom(backgroundColor: color),
onPressed: () {
setState(() {
_bottomColor = _bottomColor == Colors.blue
? Colors.red: Colors.blue;
});
},
child: child,
);
},
child: Text(_bottomColor != Colors.blue ? '青にする' : '赤にする'),
),
-
tween: ColorTween(end: _bottomColor),
Tweenなので最初begin も書いてましたが、トラップでした。なくても大丈夫です。end だけだと、最終何色になる、というのを設定すれば、時間通りに変化して色が変わる、と腑に落ちました。 -
setState(() { _bottomColor = _bottomColor == Colors.blue? Colors.red: Colors.blue; });
状態管理のsetStateを使って、最終何色にしたいかを設定します。今の色が青なら赤、赤なら青になるように設定します。 -
child
前の節と同じです。
まとめ
以上で色のアニメーション方法を解説しました。検索してみると、これぞ、というサンプルがなかったので自分で作ってみました。
(自分のところではアニメーションしなかったり、リスナーにsetStateを入れて実行したり、null-safety以前だったり)
いやぁ、分かってしまえば簡単ですが、結構苦労しました。あなたのお役に立てると幸いです(あと、数ヶ月後の自分)
参考
-
[Flutter]色を変えるアニメーションをするには? – ちょげぶろぐ
リスナーの使い方に感動した。 -
Flutter Animation: Background Color Transition
三色に変化できるかと頑張ったが、ダメでした。
全ソース
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> with TickerProviderStateMixin {
late final _controller = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
);
late final Animation<Color?> _animatinColor = ColorTween(
begin: Colors.blue,
end: Colors.red,
).animate(_controller);
bool _isForward = false;
Color _bottomColor = Colors.blue;
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(title: Text('Animation Color')),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: <Widget>[
AnimatedBuilder(
animation: _animatinColor,
builder: (context, child) {
return FilledButton(
onPressed: () {
if (_isForward) {
_controller.reverse();
} else {
_controller.forward();
}
setState(() {
_isForward = !_isForward;
});
},
style: FilledButton.styleFrom(
backgroundColor: _animatinColor.value),
child: child,
);
},
child: Text(_isForward ? '青にする' : '赤にする')),
TweenAnimationBuilder(
tween: ColorTween(end: _bottomColor),
duration: const Duration(seconds: 1),
builder: (BuildContext context, Color? color, Widget? child) {
return FilledButton(
style: FilledButton.styleFrom(backgroundColor: color),
onPressed: () {
setState(() {
_bottomColor = _bottomColor == Colors.blue
? Colors.red
: Colors.blue;
});
},
child: child,
);
},
child: Text(_isForward ? '青にする' : '赤にする'),
),
],
),
),
),
);
}
}