“The Boring Flutter Development Show” で取り上げられたアニメーションを様々な方法で書きながら解説

ImplicitlyAnimatedWidgetを継承してお手軽系アニメーションWidgetを自作

FlutterのYouTube公式チャンネル では The Boring Flutter Development Show というシリーズが配信されています。Flutterの公式情報発信の多くはコンパクトに濃密にまとめられていますが、これは1時間くらいペアプロしながら、ノーカットで時々躓きながら実装が進められていく様子が収めされています(”Boring”はこれを表しているはずです)。長くて観るの大変ですが、実際のコーディングする様子を見る中で学ぶことも多く良い面もあります。

なかなか面白いものの1時間程度集中して観るのも大変なので僕も全部は目を通せてないのですが、最近個人的にちょうどアニメーション周りを弄っている中で、第18回のエピソードはそのテーマだったので観てみたら、なかなか興味深い内容でした。

このエピソードは、端的に言うと、ImplicitlyAnimatedWidgetを継承した自前クラスを作って、次のタイトルのアニメーションを組む内容でした。

よく見ると分かりますが、次のようなアニメーションです。

  • 赤と青の色同士が直接変わるのではなく中間に行くにつれて白く変わっていって見えなくなる
  • 文字列もそのちょうど見えなくなった瞬間に変わる

300msで肉眼で分かりにくいので、その肝の部分だけ切り取って1,000msのアニメーションとしたのが以下で、これなら分かりやすいはずです。

本記事では、これを第18回のエピソードで紹介されていた実装方法含めて様々なやり方で実装していきます。

この記事をきちんと理解できれば、以下の2つの簡単なアニメーションのやり方と合わせて、必要に応じて自由自在に最適なアニメーションを組めるようになるはずです。

本記事のコードはすべてこちらに含まれています:

まずはAnimationControllerでsetStateでベタに実装

Flutterでカスタムアニメーションを組む際、AnimatedWidget・AnimatedBuilderがよく使われますが、AnimationController + setStateがまず基本であることを理解するのが大事です。setStateせずにアニメーション可能なWidgetは、利用者が直接呼ぶ必要が無いだけで、どれも内部実装でsetStateが実行されています。

Streams API + StreamBuilderでsetStateが不要なのも、内部でおなじみのStatefulWidget + setStateを適切に呼んでくれているためですが、この関係と同様です。

応用的なWidgetを表面上の使い方だけ把握して使うだけでなく、それがどういう風に動いているのかという基礎的な理解も重要です。

というわけで、ざっくりベタに組むと次のようになります。

ざっくり次のように組んであります。

  • ColorTween ( Tween<Color> に特別なケアがなされたもの)を定義
  • build メソッドではAnimationControllerを用いてColorTweenをevaluate (アニメーション進捗に応じたTweenの現在の値を評価)
  • AnimationControllerにaddListenerして進捗の度にsetState(リビルド)されるように設定
  • ボタンが押されると、AnimationControllerをforwardし、addListener内のsetStateが1フレームごとに呼ばれる

不明な点があれば、公式ドキュメントの Essential animation concepts and classes を併読してみてください。

ただ、今の段階では、次のように、もともと目指していたアニメーションとは違います。

  • 中間で白くなって見えなくなるようになってない
  • ボタンを押した直後にテキストが切り替わってしまう

Tweenを自作

上のアニメーションの課題を、Tweenを自作することで解決していきます。

GhostFadeTween: 中間で白くなるColorのTween

0–1のアニメーション値の変化に連れて、Tweenがどう変わるかをlerpで定義します。デフォルト実装ではbeginとendの「中間」となりますが、今回は middle の色として白色を挟みたいので次のようになります。

SwitchStringTween: アニメーション進捗のちょうど真ん中で文字が変わるTween

Tweenは、doubleやColorのように中間の値がすぐ浮かぶようなものだけでなく、実装次第でString用のTweenも作れます。

今回は、アニメーション進捗の中間で文字が切り替わるようにしたいので、次のように定義します。

自作したTweenを利用する

先ほどのコードに対して、次のように自作したTweenを利用するように書き換えます。
(フルのコードはこちらを見てください: https://github.com/mono0926/flutter-animations/blob/master/lib/pages/custom/animation_controller_set_state_enhanced_page.dart )

すると、次のように望んでいたアニメーション結果になりました👏

この実装の問題点

見た目的には望んでいたものができましたが、次のような問題があります。

  • アニメーションの度に(60fpsでは16msごとに)、画面全体がリビルドされてしまっていてパフォーマンス観点でよろしくない
  • コードが煩雑

前者についてはこちらに詳しく書いたのでご覧ください。

AnimationControllerにaddListenerしてsetStateするベタ実装では、どうしてもこういう問題が付きまといます。
一応、この延長線上でリビルドを局所化することはできます(こちらのサンプルコード参照)が、後者の「コードが煩雑」という点についてはこのままではどうしようもありません。


というわけで、Flutterフレームワークが用意してくれているアニメーション系の様々な便利Widgetの出番です。

カスタムアニメーションの場合、次のどれかを使うことになるはずです。

今回の場合、一応どれを使っても組めるので、それぞれ試してみます。

AnimatedWidget

AnimatedWidgetを継承すると、外部から渡されたAnimationを元に所望のアニメーションをするWidgetを作れます。

ここで紹介したTransition系アニメーションの多くはそうやって組まれたものなので、これを見るとどういうものを用意する際に適しているのかイメージしやすいはずです。

基本的に、何か1種類の要素を動かす再利用可能なアニメーションWidgetを用意したいときに適しています。

その他、公式ドキュメントの Simplifying with Animated­Widget などご覧ください。

今回のアニメーションの場合、次のように2種類のアニメーションをする必要があって、お行儀よく自作すると2つのAnimatedWidgetを用意・利用する必要が出てきます。

  • 文字列
  • 文字色

なので、今回の要件に対してはあまり適してないですが、がんばって書くと次のようになります。

以下のAnimatedWidgetを用意して、利用側のbuildメソッドで入れ子で利用しています。

  • _TextStyleColorTransition: 与えられた色アニメーションを適用したTextStyleを返す
  • _StringTransition: 与えられた文字列アニメーションのTextを返す

setStateはAnimatedWidget内で適切に呼ばれるので、利用側のコードとしてはAnimationControllerを適切にforwardするだけで良くなりました。

ただ、今回の要件に適していないと書いた通り、コード量は多く、利用側もそこまでシンプルではない問題を抱えています。いまいちですね。

AnimatedBuilder

次にAnimatedBuilderを試してみます。AnimatedBuilderは、別途継承したクラスを用意することなくそのまま使えます。

AnimatedBuilderは、外部から渡されたAnimationの進捗に応じて、builderコールバックが呼ばれるので、その都度Animationを評価してWidgetを返します。

これを利用した場合のコード例は以下です。

不明点があれば、公式ドキュメントの Refactoring with AnimatedBuilder をご覧ください。

動画によるとても分かりやすい解説もあります。

これで、別途Widgetを用意することなく、比較的簡単にアニメーションを組めて、かなり良い感じになってきました。要件によってはこれでゴールで良いと思います。

  • リビルドがAnimatedBuilder配下に局所化された
  • コードもまあまあシンプルになった

とはいえ、まだ改善の余地はあります(これが必要かどうかはケースバイケースです)。

  • AnimationControllerの管理を利用側のコードから無くしたい
  • 簡単に再利用できるWidgetとして切り出したい

ここでImplicitlyAnimatedWidgetの出番です。

[補足] AnimatedBuilderのパフォーマンス観点での注意

今回の例では、配下のWidgetがそれ以上存在しないため、 child 引数の利用が不要でしたが、配下にアニメーションとは関係ないWidgetがぶら下がっている場合は child を適切に利用するように注意してください。

Performance optimizations
If your builder function contains a subtree that does not depend on the animation, it’s more efficient to build that subtree once instead of rebuilding it on every animation tick.
If you pass the pre-built subtree as the child parameter, the AnimatedBuilder will pass it back to your builder function so that you can incorporate it into your build.
Using this pre-built child is entirely optional, but can improve performance significantly in some cases and is therefore a good practice.
https://docs.flutter.io/flutter/widgets/AnimatedBuilder-class.html#performance-optimizations

同様の考え方は、AnimatedWidgetを適切に使う場合にも必要です。詳しくは以下の一連のツイートご覧ください。

ImplicitlyAnimatedWidget

ImplicitlyAnimatedWidgetを継承したWidgetを用意すると、setStateが不要になるだけでなく、さらにAnimationControllerの管理自体も不要になります。

次のお手軽アニメーションを自作すると捉えるとイメージしやすいはずです。

AnimatedWidget・AnimatedBuilderとの差を整理すると、次のようになります。

  • AnimationControllerの管理が不要になる
  • (特にAnimatedBuilderと比べて)コード量は少し増える代わりに、そのアニメーションが簡単に他でも再利用可能になる
  • AnimationControllerを用いたきめ細かい制御はできなくなる

なので、無闇にImplicitlyAnimatedWidgetに寄せる必要はないですが、適宜使うと良いですし、今回のアニメーションとの相性も良く、 The Boring Flutter Development Show第18回のエピソードでもこれを使って実装しています。

コード例は次のようになります。

まず、利用側の_ImplicitlyAnimatedWidgetPageStateでは、もうAnimationControllerを管理する必要がなくなり、SingleTickerProviderStateMixinを適用する必要も無くなりました。さらにTweenの管理も不要になり、単純に現在表示するべき文字列と色の管理だけするだけで良くなりました。

こういった複雑性は、ImplicitlyAnimatedWidgetを継承したHeadlineクラスに外出しされました。

forEachTweenメソッドをオーバーライド

ImplicitlyAnimatedWidgetはStatefulWidgetであり、またStateとしては通常AnimatedWidgetBaseStateをオーバーライドします。

その際、forEachTweenメソッドのオーバーライドが必須で、これが肝です。

使い方としてはドキュメントにしっかりと書かれています。また、ソースコードを合わせて追うと理解が進むと思います。

Subclasses must implement this function by running through the following steps for each animatable facet in the class:
1. Call the visitor callback with three arguments, the first argument being the current value of the Tween object that represents the tween (initially null), the second argument, of type T, being the value on the Widget that represents the current target value of the tween, and the third being a callback that takes a value T (which will be the second argument to the visitor callback), and that returns an Tween object for the tween, configured with the given value as the begin value.
2. Take the value returned from the callback, and store it. This is the value to use as the current value the next time that the forEachTween method is called.
https://docs.flutter.io/flutter/widgets/ImplicitlyAnimatedWidgetState/forEachTween.html

ざっくり説明すると、visitor引数は次のようになっていて、

  • 第1引数: 現在のTween(初期値はnull)
  • 第2引数: アニメーション対象のターゲット値
  • 第3引数: Tweenの初期値を生成して返すコールバック

さらにvisitorの結果のTweenを保持するコードを書いておくと、ターゲット値が変わる度(親Widgetがリビルドされる度)にフレームワークによって適宜Tweenが更新され、またそのTweenはbuildメソッドで使われるためアニメーションが実行される、ということになっています。

なお、_HeadlineStateで使っているanimation変数は、親クラスのAnimatedWidgetBaseStateが適切に管理してくれているものです。

というわけで、今回のアニメーションの場合はAnimatedBuilderかImplicitlyAnimatedWidgetを利用するのが良さそうです。


実は、 AnimatedSwitcher を使うのが手っ取り早いかも

今回のアニメーションと見た目的にほぼ同じものをAnimatedSwitcherを使うと次のようにかなり短いコードで書けてしまいます。自作Tweenを別途用意する必要もなく、コード全体としてたったこれだけです。

見た目的には次のようになります。
(文字色が白くなっているのではなくフェードアウト・フェードインしているなど、目に見えにくい部分で差があります。)

AnimatedSwitcherはこちらの記事でも紹介した、簡単に良い感じの繋ぎアニメーションを組めるWidgetです。

今回は、次のようにオプション引数の transitionBuilder も指定して、ちょうど良いタイミングでのフェードがかかるようにしたら、ほとんど見た目が一緒になりました。

どうしてもという細かいこだわりが無ければ、今回のアニメーションについてはこちらで充分な気がしました🤔


以上、ある1つのアニメーションを実現するために様々な実装パターンをなぞることでカスタムアニメーションを組む際の色々な考え方に触れられたと思います。

複雑なカスタムアニメーション

こういった基礎がしっかり身に付けば、次のようなアニメーションもあまり苦労なく組めるようになります。

解説は大変なので割愛しますが、サンプルリポジトリのflight_searchに置いてあるので、ぜひご覧ください。

さらにリッチなアニメーション

コードで書くのが大変なリッチなアニメーションの場合はFlareを活用するという方法もあります。

ドキュメントはこちらです。

動画によるチュートリアルを何本か観るとある程度使い方がつかめると思います。

この英語記事がよくまとまっています。

日本語記事での基本的な使い方としては、こちらがよくまとまっています。

僕自身まだまだ勉強中ですが、ベーシックなものならまあまあ作れるようになってきたので、それらに触れた簡単めの記事をいつか書きたいなと思っています。