FlutterのTransition系アニメーションWidgetをすべて紹介
AnimationControllerの扱い方も手厚く解説 📝
※ 本記事は、GIF動画多めなため通信量が多い(10MBくらい)ので注意してください
Flutterのアニメーションをとても簡単に(AnimationControllerを使わずに)組めるAnimated系Widgetを以下の記事で紹介しましたが、本記事ではもう少しカスタマイズ性の高い Transition アニメーション系のWidgetを見ていきます。
サンプルリポジトリは同じくこちらです:
Transition アニメーション系のWidgetは次の10種類があります。
- AlignTransition
- DecoratedBoxTransition
- DefaultTextStyleTransition
- FadeTransition
- PositionedTransition
- RelativePositionedTransition
- RotationTransition
- ScaleTransition
- SizeTransition
- SlideTransition
Animated系WidgetとTransitionアニメーション系のWidgetとの違い
これらのTransitionアニメーション系のWidgetの Animated系Widget との違いは、AnimationController を自前管理する必要があるということ、そしてそれによってAnimationControllerを用いた柔軟なコントロールができることです。
- Animated系 Widget: AnimationController不要でsetStateだけでアニメーションを実行(扱いが容易・簡潔)
- Transition アニメーション系 Widget: AnimationController経由でアニメーションを実行(コード量が少し増える一方、細かい制御が可能)
つまり、それぞれ一長一短ですが、Animated系Widgetで組めるものは基本的にそれで済ます方がベターです。不要なカスタマイズ性のためにコードの複雑性を上げるのは無駄ですし、扱いが容易なWidgetの方が誤用もしにくいです。パフォーマンス的にも最適化されています。
では、どういう時にTransitionアニメーション系 Widgetが必要かというと、それはすなわちAnimationControllerの機能を利用したいときであり、主に次のような要件が必要なときです。
- アニメーションの状態を監視してその状態をトリガーに別の処理を実行
- 複数のアニメーションをタイミング良くつなぎ合わせる(Staggered Animations)
- アニメーション状態のきめ細かい制御
- 逆向きのアニメーションを順再生とちょうど逆再生した動きにしたい(reverseメソッド利用)
以下、各種Transition系Widgetの紹介をしていきたいところですが、その前にその利用に必須のAnimationControllerの基本的な使い方をなぞります。
AnimationControllerの使い方
AnimationControllerの生成
次のように、 vsync
と duration
を指定して生成するのが基本です(他の指定はドキュメントを参照してください)。
AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
この例の場合、500msの長さで60fps端末の場合16msごとに約30回、次のような0から1までの値を等間隔に発行するAnimationControllerを定義したことになります。
0.0, 0.030448, 0.063136, 0.129906, 0.170572, 0.199, 0.23129, 0.26791, 0.299818, 0.363878, 0.402834, 0.43583, 0.462866, 0.49619, 0.532776, 0.565306, 0.597992, 0.63176, 0.663938, 0.732478, 0.765738, 0.797618, 0.837338, 0.867224, 0.89866, 0.934644, 0.962772, 0.999252, 1.0
それぞれのプロパティでの指定しているものは以下の通りです。
- vsync: 毎フレームごとに更新を伝える
- duration: アニメーション長さ
vsync は TickerProvider型で、createTicker メソッドを実装している必要がありますが、自前実装などは必要なく、以下のいずれかのmixinを適用( with
)するだけで良いです。
適切な使い分けは以下です。
- SingleTickerProviderStateMixin: そのクラスで利用するAnimationControllerが1つだけのとき(こちらで済むことの方が多いはず)
- TickerProviderStateMixin: そのクラスで利用するAnimationControllerが2つ以上のとき
AnimationControllerを2つ以上使っているにも関わらずSingleTickerProviderStateMixinを適用していると次のようなエラーが表示され、すぐに誤用に気付けます。
flutter: ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
flutter: The following assertion was thrown building Builder:
flutter: _AlignTransitionPageState is a SingleTickerProviderStateMixin but multiple tickers were created.
flutter: A SingleTickerProviderStateMixin can only be used as a TickerProvider once. If a State is used for
flutter: multiple AnimationController objects, or if it is passed to other objects and those objects might
flutter: use it more than one time in total, then instead of mixing in a SingleTickerProviderStateMixin, use
flutter: a regular TickerProviderStateMixin.
なので、基本的にSingleTickerProviderStateMixinを使う癖を付けておいて、上記エラーで指摘されたらTickerProviderStateMixinに書き換える、という程度でも良いと思います。
また、ドキュメントに以下のように記載されている通り、そのWidgetツリーがアクティブでない時(ざっくり非表示の時)は、フレームの更新は伝えられず余計な負荷がかからない作りになっています。
Provides Ticker objects that are configured to only tick while the current tree is enabled, as defined by TickerMode.
逆に言うと、そういった非表示状態のあるWidgetを自前実装するときは、TickerModeの制御をして無駄なアニメーションが裏で走るのを抑制する必要があります。その例として、次の記事など参考になります。
ただ、上の例ではOffstageとTickerModeを自前制御していますが、Visibility Widgetではそれらラップして適切に管理してくれる( visible: false
にすれば非表示になるとともにアニメーションも止まる)ので通常はそれを使うので十分だと思います(Visibilityが対象Widgetをツリーから消したりTickerModeを制御してくれたり適切に管理してくれているので無駄なアニメーション負荷がかからない、という理解は重要)。
StateでAnimationControllerを生成・保持
Flutterの特性上、アニメーションはWidgetのリビルドによってなされ、 ゆえにAnimationControllerはリビルド前後で状態が保持される必要があります。そのため、StatefulWidgetのStateでAnimationControllerを生成・保持します。
まず、Stateのクラス宣言としては、上で紹介したSingleTickerProviderStateMixinを適用して次のようになります。
class SomeState extends State<SomePage> with SingleTickerProviderStateMixin {}
AnimationControllerの生成は、次のように書ければスッキリしますが、フィールドの宣言とともthisを使うと Invalid reference to 'this' expression.
というコンパイルエラーになってしまうので、
class SomeState extends State<SomePage> with SingleTickerProviderStateMixin {
// AnimationControllerの初期化
final AnimationController _animationController = AnimationController(vsync: this);
}
次のように initState
でセットします。
また、生成したAnimationControllerは必ず上のようにStateのdisposeメソッド内でdisposeする必要があります。アニメーション中にStateが破棄され、AnimationControllerが生きたままだと次のようなエラーログが吐かれます。
(アニメーション実行中以外にStateが破棄された場合、このエラーログは吐かれず気付きにくいこともありますが、とにかく必ず対で呼ぶように気をつけましょう。)
flutter: ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
flutter: The following assertion was thrown while finalizing the widget tree:
flutter: _AlignTransitionPageState#1e7ab(ticker active) was disposed with an active Ticker.
flutter: _AlignTransitionPageState created a Ticker via its SingleTickerProviderStateMixin, but at the time
flutter: dispose() was called on the mixin, that Ticker was still active. The Ticker must be disposed before
flutter: calling super.dispose(). Tickers used by AnimationControllers should be disposed by calling
flutter: dispose() on the AnimationController itself. Otherwise, the ticker will leak.
flutter: The offending ticker was: Ticker(created by _AlignTransitionPageState#1e7ab(lifecycle state:
flutter: created))
AnimationControllerの生成・保持の仕方が分かったところで、実際のTransition系Widgetを使いつつ説明していきます。
AlignTransition
Align系のWidgetは3種類あって、次のような関係です。
- 普通にレイアウト: Align
- Animated版(お手軽アニメーション版): AnimatedAlign
- Transition版(AnimationControllerが必要版): AlignTransition
AnimationControllerから適したAlignTransitionが受け付けられるAnimation型への変換
Transtion系Widgetは、アニメーション可能なプロパティが Animation<T>
型になっています( T
はWidgetのプロパティによって様々)。一方、AnimationControllerは Animation<double>
型に準拠していて、Transition系の多くのWidgetにそのまま指定できません。また、型が同じだとしても0〜1の直線的に変化する値をそのまま設定しても普通は所望のアニメーションにはならず値の加工が必要です。
AlignTransitionの場合、alignmentプロパティはAnimation<AlignmentGeometry>
型なので、AnimationControllerをその型の所望のAnimationに変換する必要があります。
変換方法は、AnimationControllerのdriveメソッドを使う方法と、Animatableのanimateメソッドを使う方法があって、次のように適用順が逆です。
個人的には、ソース元であるAnimationControllerからdriveで順につなげていく前者の書き方が好きですが、感覚次第で好みが分かれるかもしれません。
生成されるAnimation<AlignmentGeometry>
型のインスタンスはどちらも同じで、この場合はAnimationControllerの変化に応じて、Alignment.topLeft(左上)からAlignment.bottomRight(右下)に連続的にAlignment値が変わっていくアニメーションとなります。
Tween型の取り扱いに注意
ちなみに、アニメーション種類によってはこのままでも動きますが、以下の分かりにくいエラーが発生したりうまく動かないことがあります(method '-'
とは引き算を表しています)。
Another exception was thrown: NoSuchMethodError: Class '' has no instance method '-'.
TweenのTypes with special considerations に詳しく載っていますが、 Tween
クラスの T lerp(double t)
メソッドの代わりに専用の型でoverrideされたものが使われるようにすると解決します。
つまり、上の例の場合、beginとendはAlignment型なので、 次のようにAlignmentTween
型を使うのが良いです。
同様のTweenには他に以下などがあります。
- IntTween: Tween<int>用
- StepTween: 同じくTween<int>用
- SizeTween: Tween<Size>用
- MaterialPointArcTween: Tween<Offset>用
- AlignmentGeometryTween: Tween<AlignmentGeometry>用
- FractionalOffsetTween: Tween<FractionalOffset>用
動かしてみる
AlignTransitionに以上のように用意したalignment用のAnimationを設定したコード例がこちらです:
動かしたいタイミングで次のコードを実行すると、
_animationController.forward();
次のように動きます。
リファクタリング・パフォーマンス調整
Animationが不変の場合、buildメソッド内で生成するのではなく、次のように、AnimationControllerと同じくフィールドに保持するようにしてinitStateで生成するようにするとbuidメソッドがすっきりします。
見た目だけの問題ではなく、Stateがリビルドされても同一のTweenインスタンスが使われるようになってパフォーマンス的にも有利なようです(TweenのPerformance optimizations に詳しく載っています)。
ただ、このパフォーマンスの差は通常の利用では気にするほどのものではなく、基本的にはその文脈において見やすい書き方優先でも問題ないと思います。サンプルリポジトリでもサンプルとしての見やすさ・弄りやすさを優先してbuildメソッドにベタ書きしています。
以上でAlignTransitionはうまく使うことができましたが、これだけだとより扱いが簡単なAnimatedAlignを使えば済む(むしろその方がベター)ということになってしまうので、AnimationControllerを活用する例に書き換えていきます。
複数のアニメーションをタイミング良くつなぎ合わせる(Staggered Animations)
一連のグループの複数のオブジェクトが時間差で次々と一定の規則に従ってアニメーションするものを”Staggered Animations”といいます。例として次のように、手前の画像が動いて少し遅れて裏側の画像が付いていく、というものを実装してみます。
アニメーションが少し被るように、0–1のアニメーション範囲内で次のように時間差をつけてみます。
- 手前の画像: 0.0–0.6
- 奥の画像: 0.4–1.0
ベタ書きすると、次のように書くと上のような動きになります。
次のように、CurveTweenでIntervalを指定しているものをdriveメソッドで挟んで指定しているのがポイントです。
上で animate
メソッドを使った逆方向の書き方もあると書いた通り、他の書き方もあります。
AnimationControllerのその他の主な機能の紹介
その他、以下などを活用するなどして、きめ細かいアニメーション制御をできます。
- forward メソッド: 最もよく使うであろうアニメーションを普通に実行するメソッド(
from
引数で開始位置指定可能・await
するとアニメーション終了後に後続の処理が呼ばれて便利なシーンあり) - reverseメソッド: forwardメソッドの逆
- stop メソッド: アニメーション停止
- addListener メソッド: アニメーションの値が変わる度に呼ばれる(60fpsの場合16msごと)
- addStatusListener メソッド: statusが変わるたびに呼ばれる(アニメーション終了タイミングを知りたいときなど)
よく使うのはこのあたりですが、他にも色々あるので、ドキュメントをご覧ください。
繰り返しになりますが、こういったAnimationControllerを活用した、Animated系Widgetで組むのが難しいアニメーションを組みたいときにTransition系Widgetが有効です。
(本記事では詳しく触れませんが、さらにTransition系Widgetでも実現困難なアニメーションを組みたいときは、AnimatedBuilder およびAnimatedWidget を用いたフルカスタムアニメーションを組むことになります。)
以上、AlignTransition を例にTransition系Widgetを使う上での肝に触れたので、他のWidgetは同様の要領で使えます。
というわけで、その他9種類のTransition系Widgetはサンプルコードとその動作する様子を見ながらあっさり目に紹介していきます。
DecoratedBoxTransition
DecoratedBox のTransition版です。
FlutterLogoDecoration という面白いWidgetがあったので、使ってみました。Decoration 継承クラスの実装例として参考になります。
もっとポピュラーな使い方としては、ドキュメントに載っている次のようなアニメーションだとは思います。
完全に対応したAnimated系Widgetは無いですが、AnimatedContainer にも decoration プロパティ があるので、同様のアニメーションをそちらでも実現できます。
DefaultTextStyleTransition
Theme.of(context).textTheme.display1
とTheme.of(context).textTheme.display4
を切り替えるだけの簡単なサンプルです。
AnimatedDefaultTextStyle のTransition版です。
FadeTransition
上でも触れたStaggered Animationsで4つの画像を連続的に切り替えてみました。
AnimatedOpacity のTransition版です。AnimatedOpacityの内部実装でFadeTransitionが使われていたりします。
ちなみに、Performance best practices や Performance considerations for opacity animationに書いてある通り、こういった半透明系のWidgetは透過度具合のアニメーション以外にも同じ透過度の半透明なWidgetを動かす時にもパフォーマンス観点で有用です。
公式動画での解説もあります:
PositionedTransition
Curves.elasticInOut を指定して独特な動きにしてみました。
AnimatedPositioned のTransition版です。
RelativePositionedTransition
PositionedTransition とほとんど一緒ですが、以下のWidgetの名前付きコンストラクタベースの指定の仕方になっています。
- Positioned.fromRelativeRectコンストラクタ
- RelativeRect.fromSizeコンストラクタ
RotationTransition
Curves.elasticOut で振動を付けてみました。
ScaleTransition
Staggered Animationsで画像と文字のアニメーションを少しずらしてみました。
SizeTransition
縮んだ時のY方向をaxisAlignmentプロパティで少しずらして目が覗く感じにしてみました👀
AnimatedSizeのTransition版です。
SlideTransition
暗幕が開く感じにしてみました。Navigatorに頼らない画面遷移チックなものを組みたい時などに良さそうです。
Transition系Widget周りは以下が肝で、それさえ分かれば個別の使い方は難しく無いので後半はあっさりとした紹介となりましたが、以上です。
- AnimationController周りの使い方に習熟すること
- どういう既存のTransition系Widgetがあるのか知ること
(本記事では詳しく触れませんが、さらにTransition系Widgetでも実現困難なアニメーションを組みたいときは、AnimatedBuilder およびAnimatedWidget を用いたフルカスタムアニメーションを組むことになります。)
さらにカスタムアニメーションの書き方については次の記事に続きます。