Flutter アニメーションのキャッチアップのスクラップ

riveとかライブラリの公式とかみて綺麗だし直感的にエフェクトつけたりできて、良いじゃん?と思った
でも、なんでライブラリを使う必要があるのかと聞かれると、逆になんで自作する必要があるのかと思ってしまったりして、結局はアニメーションについて理解ができてないからそんなことを思ってしまうのだろう
なので本記事は0から始めるFlutterにおけるアニメーションを会得するためのスクラップ
疑問だったり気になることが描かれていくであろう

まずは公式でもみてみるか

公式によると、色々方法はあるからまずはFutterのアニメーション実装時は以下のツリーを使って最適なアプローチを選ぶと良いよ、ということらしい。

やりたいことをグラフの描画のアニメーションとしてみる
質問1:アニメーションは絵に近いか?RowやColumなどの範疇を超えたレイアウト移動が含まれるか?
回答:No
質問2:デザインチームと協力していますか?アニメーションにはベクターグラフィックスまたはラスターイメージの描画が含まれていますか?動きをコードで表現するのは難しいですか?アニメーションが何をするかを視覚的に描きたいですか?
回答:No
とすると、「CustomPainter」が適しているということになった

それぞれこんな感じだろうか
・アニメーションにイラスト要素がある場合や、RowやColumnで対応が難しいレイアウト移動をさせたい
・多少のデザイン業務が発生しても問題ない
・アニメーションをコードだけで実装することが難しく、視覚的に調整したい
→ RiveやLottieなどライブラリが適している
・RowやColumnのレイアウト範疇でアニメーションが収まる
・デザイン・アニメーションをコードだけで表現したい(実装ができる)
・ベクタ画像、ラスタ画像を含まない
→ CustomPainterが適している
・イラスト要素などがなく、レイアウト移動がRowやColumnの範疇にあり、テキスト要素にアニメーションをつけたい
→ AnimatedDefaultTextStyleが適している
・アニメーションに不連続な処理がなく、一定の動きを永続的に動き続けて欲しい
・アニメーションの対象がchildである(children:複数Widgetではない)
・AnimatedFooWidgetに表現させたいものがある(Fooは各プロパティ名)
→ AnimatedFooが適している
上記でAnimatedFoo Widgetに欲しいものがなかった場合
→ TweenAnimationBuilderが適している
・FooTransitionWidgetに表現させたいものがある(Fooは各プロパティ名)
→ FooTransitionが適している
FooTransitionWidgetに欲しいものがなかった場合で
・対象が別Widgetのbuildメソッドに属してる場合
→ AnimatedBuilderが適している
FooTransitionWidgetに欲しいものがなかった場合で
・独立したWidgetとしたい
→ AnimatedWidget

調べたら日本語のバージョンがあった

なぜFooTransitionとAnimatedFooで分かれているのか?
そもそもそれらの正体は何なのか、どのような違いがあるのだろうか。
FooTransition
→ Explicit Animation(明示的なアニメーション)
・AnimationControllerを用意して、必要なタイミング(ボタンを押されたなど)でcontrollerに対して
forward/reverse/stopなどを明示的に呼ぶ。
(アニメーションの基本動作はこちら側で制御できる)
AnimatedFoo
→ Implicit Animation(暗黙的なアニメーション)
・AnimationControllerは自前で用意せず、Flutter側で用意してくれたクラスが内包してくれる。
・プロパティを変更するだけでアニメーションをしてくれる。(変更できるプロパティが決まっている)
・AnimatedContainer, AnimatedSizeなどが用意されている。
自由度が高いのがFooTransitionと思っていたけれど、微妙に違った。
それぞれ扱えるものに制限があるので、FooTransitionで扱えるパラメータでアニメーションを表現できるのか否か、AnimatedFooではどうだろうか、という観点で見るべきものだった。

よく出てくるTransitionとかImplicitってなんなの?
Transition Widgets
→ FooTransitionを総称したもの。親クラスはAnimatedWidget
であり、TransitionAnimatedWidgetなるものがあるわけではない。(= 親クラス名由来の総称ではない。)
Implicit Widgets
→ AnimatedFooを総称したもの。こちらは親クラスがImplicitlyAnimatedWidget
であることが由来。

Explict Widgets
→ 自分で全部animationControler、tween、curveなどをanimatedBuilderとかanimatedWidgetで対象のWidgetに紐付ける方法を取るもの。
使い分け
Implicit Widgets
・複数の値に変化させたい(複数回アニメーションさせたい)
・forwardさせるだけのアニメーションの場合
(特徴:拡張性(カスタム性)が低い)
Transition Widgets
・TweenSequence、Intervalを使って複雑なアニメーションをさせたい場合
・repeatなど複雑なアニメーションの操作をしたい場合
(特徴:ちょっとカスタマイズできる)
Explict Widget
・めっちゃカスタマイズしたいとき
・複数のアニメーションをさせたいとき(一つのアニメーションしかさせない時はTransition Widgetsで良い、あくまでTransitionで複数やろうとするとめちゃネストするため、Explictで良いのではというところ)

Animation = AnimatedController.drive(Tween);
Animation = Tween.animate(AnimationController);
のようにAnimatedControllerから動かす記法とTweenから動かす記法が存在するが、
結論、基本的にはどちらでも良い。
相当複雑な場合はTween起点でないと書けない場合もある。
(Tween記法の方が分かりやすいかも)

とりあえず、アニメーションにはどのようなアプローチがあるのか軽くさらったので
今回はグラフに対してのアニメーションをつけようと思っているため、まずはCustomPaintから調査する。

CustomPaint Widget についての理解をまとめる。
CustomPaint Widget
:キャンバスを提供して、painterのロジックを指定する場所。3つのパラメータを持つ。
CustomPaint(
child: childWidget(),
foregroundPainter: DemoForegroundPainter(), // CustomPainter型
painter: DemoPainter(), // CustomPainter型
)
描画順序
painter → child → foregroundPainter
スタックのような形式で3つのレイヤーを構築する。(= それぞれが重なるイメージ)
サイズ
Widgetそのもののサイズはchildのサイズとなる。childがnullの場合、sizeパラメータの値をみてサイズが決まる。
(new) CustomPaint CustomPaint({
Key? key,
CustomPainter? painter,
CustomPainter? foregroundPainter,
Size size = Size.zero, // ←コレ
bool isComplex = false,
bool willChange = false,
Widget? child,
})
)

CustomPainterの継承
painter
とforegroundPainter
はCustomPainterを継承するため、paint()
とshouldRepaint()
の2つのメソッドをオーバーライドする必要がある。
それぞれ役割はこうだ。
・paint()
:キャンバスに絵を描くための指示を与えるもの。Canvas
とsize
を引数にもつ。
・shouldRepaint()
:新しいCustomPainter のインスタンスが提供されたとき、新しいパラメータに基づいてキャンバスを再描画する必要があるかどうかを判断するもの。

paint()
の中では、描画に関するパラメータの指定ができる。
Paint
画面上のオブジェクトの描画に関連する属性の指定
final paint = Paint()
..color = Colors.black // 描画される線や図形の色
..style = PaintingStyle.stroke // strokeで輪郭線を表示するスタイルにする
..strokeWidth = 1.0 // 描画される線や図形の輪郭線の太さ
..isAntiAlias = true; // 線や図形が滑らかに見えるアンチエイリアシングの有無
Rect
幅と高さを持つ仮想的な矩形を作成して、実際の矩形を描画したり、内部の図形の境界を設定
Rect.fromCenter(
center: Offset(100, 100), // 矩形の中心座標を指定
width: 50, // 横幅
height: 80, // 高さ
);
Path
図形の輪郭線などを表現、Rectからの書き換えも可能?
Path()
..moveTo(0, 0) // 描画位置を座標(0, 0)に指定(= ポリゴンの始点を指定)
..lineTo(100, 100) // 描画位置から指定した座標(100, 100)までの直線を描く、例では(0, 0)から(100, 100)までの線が描かれる
..lineTo(0, 100) // (100, 100)から(0, 100)までの線が描かれる
..lineTo(0, 0); // (0, 100)から(0, 0)までの線が描かれる
canvas
・drawColor()
キャンバスの色(レイヤーの中では一番後ろにあるもの)の色の指定
void paint(Canvas canvas, Size size) {
canvas.drawColor(Colors.black, BlendMode.color); // 第二引数で描画の方法を指定(←は塗りつぶし)
}
・drawArc()
円の弧を描く
void paint(Canvas canvas, Size size) {
var center = size / 2;
var paint = Paint()..color = Colors.yellow;
canvas.drawArc(
Rect.fromCenter(
center: Offset(center.width, center.height),
width: 50,
height: 50,
),
0.4,
2 * pi - 0.8,
true,
paint,
);
}
・drawPath()
前述のPathクラスはカスタムシェイプ(型取り)を作成できる。一度作成したPathを、canvas.drawPath()メソッドを使ってキャンバスに描くこともできる
var paint = Paint()
..color = Colors.yellow
..strokeWidth = 20.0;
var path = Path()
..moveTo(0, 0)
..lineTo(100, 100)
..lineTo(0, 100)
..lineTo(0, 0);
canvas.drawPath(path, paint);

描画まではできるようになったが、まだアニメーションに行き着いていないことに気づいた。
前述までがCustomPaintの基礎となるものなのか。

CustomPaint とは「描画」を担当するのであってアニメーションはCustomPaint内で作るものではなかったようだ。
Implicit Widgets、Transition Widgets、Explict Widgetsを使ってアニメーションを付与していく必要がある、という理解となった。

controllerはこちらで持ってみたいので、Transition Widgetsから入ってみよう。

SingleTickerProviderStateMixinとTickerProviderStateMixinはState<StatefulWidget>を必要とするため、StatefulWidgetでの実装を行う。
【補足】
SingleTickerProviderStateMixinとTickerProviderStateMixinの違いはAnimationControllerを2つ以上持つか持たないか。
アニメーションコントローラの生成は、vsync
とduration
の2つを指定して生成するのが基本らしい。
AnimationController(
vsync: this, // SingleTickerProviderStateMixinもしくはTickerProviderStateMixinを実装していることを伝えている
duration: const Duration(milliseconds: 500), // アニメーション長さの指定
);
そして、生成したら必ずdispose
する必要がある。
(アニメーション中にStateが破棄され、AnimationControllerが動き続けている(値を持ち続けている)とエラーログが出る。)