Open20

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

mashmash

riveとかライブラリの公式とかみて綺麗だし直感的にエフェクトつけたりできて、良いじゃん?と思った
https://pub.dev/packages/rive

でも、なんでライブラリを使う必要があるのかと聞かれると、逆になんで自作する必要があるのかと思ってしまったりして、結局はアニメーションについて理解ができてないからそんなことを思ってしまうのだろう

なので本記事は0から始めるFlutterにおけるアニメーションを会得するためのスクラップ
疑問だったり気になることが描かれていくであろう

mashmash

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

mashmash

やりたいことをグラフの描画のアニメーションとしてみる

質問1:アニメーションは絵に近いか?RowやColumなどの範疇を超えたレイアウト移動が含まれるか?
回答:No

質問2:デザインチームと協力していますか?アニメーションにはベクターグラフィックスまたはラスターイメージの描画が含まれていますか?動きをコードで表現するのは難しいですか?アニメーションが何をするかを視覚的に描きたいですか?
回答:No

とすると、「CustomPainter」が適しているということになった

mashmash

それぞれこんな感じだろうか

・アニメーションにイラスト要素がある場合や、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

mashmash

なぜFooTransitionとAnimatedFooで分かれているのか?
そもそもそれらの正体は何なのか、どのような違いがあるのだろうか。

FooTransition

→ Explicit Animation(明示的なアニメーション)
・AnimationControllerを用意して、必要なタイミング(ボタンを押されたなど)でcontrollerに対して
forward/reverse/stopなどを明示的に呼ぶ。
(アニメーションの基本動作はこちら側で制御できる)

AnimatedFoo

→ Implicit Animation(暗黙的なアニメーション)
・AnimationControllerは自前で用意せず、Flutter側で用意してくれたクラスが内包してくれる。
・プロパティを変更するだけでアニメーションをしてくれる。(変更できるプロパティが決まっている)
・AnimatedContainer, AnimatedSizeなどが用意されている。

自由度が高いのがFooTransitionと思っていたけれど、微妙に違った。

それぞれ扱えるものに制限があるので、FooTransitionで扱えるパラメータでアニメーションを表現できるのか否か、AnimatedFooではどうだろうか、という観点で見るべきものだった。

mashmash

よく出てくるTransitionとかImplicitってなんなの?

Transition Widgets

→ FooTransitionを総称したもの。親クラスはAnimatedWidgetであり、TransitionAnimatedWidgetなるものがあるわけではない。(= 親クラス名由来の総称ではない。)

Implicit Widgets

→ AnimatedFooを総称したもの。こちらは親クラスがImplicitlyAnimatedWidgetであることが由来。

mashmash

Explict Widgets

→ 自分で全部animationControler、tween、curveなどをanimatedBuilderとかanimatedWidgetで対象のWidgetに紐付ける方法を取るもの。

使い分け

Implicit Widgets

・複数の値に変化させたい(複数回アニメーションさせたい)
・forwardさせるだけのアニメーションの場合
(特徴:拡張性(カスタム性)が低い)

Transition Widgets

・TweenSequence、Intervalを使って複雑なアニメーションをさせたい場合
・repeatなど複雑なアニメーションの操作をしたい場合
(特徴:ちょっとカスタマイズできる)

Explict Widget

・めっちゃカスタマイズしたいとき
・複数のアニメーションをさせたいとき(一つのアニメーションしかさせない時はTransition Widgetsで良い、あくまでTransitionで複数やろうとするとめちゃネストするため、Explictで良いのではというところ)

mashmash

Animation = AnimatedController.drive(Tween);
Animation = Tween.animate(AnimationController);
のようにAnimatedControllerから動かす記法とTweenから動かす記法が存在するが、

結論、基本的にはどちらでも良い。

相当複雑な場合はTween起点でないと書けない場合もある。
Tween記法の方が分かりやすいかも

mashmash

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

mashmash

CustomPaint Widget についての理解をまとめる。

CustomPaint Widget

:キャンバスを提供して、painterのロジックを指定する場所。3つのパラメータを持つ。

CustomPaint
CustomPaint(
    child: childWidget(),
    foregroundPainter: DemoForegroundPainter(), // CustomPainter型
    painter: DemoPainter(), //  CustomPainter型
)

描画順序

painter → child → foregroundPainter
スタックのような形式で3つのレイヤーを構築する。(= それぞれが重なるイメージ)

サイズ

Widgetそのもののサイズはchildのサイズとなる。childがnullの場合、sizeパラメータの値をみてサイズが決まる。

CustomPaint
(new) CustomPaint CustomPaint({
  Key? key,
  CustomPainter? painter,
  CustomPainter? foregroundPainter,
  Size size = Size.zero, // ←コレ
  bool isComplex = false,
  bool willChange = false,
  Widget? child,
})
)
mashmash

CustomPainterの継承

painterforegroundPainter はCustomPainterを継承するため、paint()shouldRepaint()の2つのメソッドをオーバーライドする必要がある。
それぞれ役割はこうだ。

paint():キャンバスに絵を描くための指示を与えるもの。Canvassizeを引数にもつ。

shouldRepaint():新しいCustomPainter のインスタンスが提供されたとき、新しいパラメータに基づいてキャンバスを再描画する必要があるかどうかを判断するもの。

mashmash

paint()の中では、描画に関するパラメータの指定ができる。

Paint

画面上のオブジェクトの描画に関連する属性の指定

paint
final paint = Paint()
      ..color = Colors.black // 描画される線や図形の色
      ..style = PaintingStyle.stroke // strokeで輪郭線を表示するスタイルにする
      ..strokeWidth = 1.0 // 描画される線や図形の輪郭線の太さ
      ..isAntiAlias = true; // 線や図形が滑らかに見えるアンチエイリアシングの有無

Rect

幅と高さを持つ仮想的な矩形を作成して、実際の矩形を描画したり、内部の図形の境界を設定

Rect
Rect.fromCenter(
      center: Offset(100, 100), // 矩形の中心座標を指定
      width: 50, // 横幅
      height: 80, // 高さ
    );

Path

図形の輪郭線などを表現、Rectからの書き換えも可能?

Path
 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()
キャンバスの色(レイヤーの中では一番後ろにあるもの)の色の指定

canvas.drawColor
  void paint(Canvas canvas, Size size) {
    canvas.drawColor(Colors.black, BlendMode.color); // 第二引数で描画の方法を指定(←は塗りつぶし)
  }

・drawArc()
円の弧を描く

canvas.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()メソッドを使ってキャンバスに描くこともできる

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);
mashmash

canvasを作るメソッドは結構あった。
canvas.drawLine()
canvas.drawRect()
canvas.drawCircle()
canvas.drawArc()
canvas.drawPath()
canvas.drawImage()
canvas.drawImageNine()
canvas.drawParagraph()

mashmash

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

mashmash

CustomPaint とは「描画」を担当するのであってアニメーションはCustomPaint内で作るものではなかったようだ。

Implicit Widgets、Transition Widgets、Explict Widgetsを使ってアニメーションを付与していく必要がある、という理解となった。

mashmash

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

mashmash

SingleTickerProviderStateMixinとTickerProviderStateMixinはState<StatefulWidget>を必要とするため、StatefulWidgetでの実装を行う。

【補足】
SingleTickerProviderStateMixinとTickerProviderStateMixinの違いはAnimationControllerを2つ以上持つか持たないか。

アニメーションコントローラの生成は、vsyncdurationの2つを指定して生成するのが基本らしい。

AnimationController
AnimationController(
  vsync: this, // SingleTickerProviderStateMixinもしくはTickerProviderStateMixinを実装していることを伝えている
  duration: const Duration(milliseconds: 500), // アニメーション長さの指定
);

そして、生成したら必ずdisposeする必要がある。
(アニメーション中にStateが破棄され、AnimationControllerが動き続けている(値を持ち続けている)とエラーログが出る。)