Flutterのお手軽にアニメーションを扱えるAnimated系Widgetをすべて紹介
シンプルなアニメーションはImplicitlyAnimatedWidgetで扱うと楽に組めます
※ 本記事は、GIF動画多めなため通信量が多い(10MBくらい)ので注意してください
Flutterの公式ドキュメントでは、アニメーションについては以下などが載っています。
そこにはAnimationControllerを使った場合の説明が手厚く書かれていて、それはそれで正しいのですが、AnimationControllerを使うことなくもっと簡単にアニメーションを扱えるImplicitlyAnimatedWidget(の派生クラス)があって、シンプルなアニメーション実装ならそれで済むことも多いです。
「Flutterはアニメーションが難しい・取っ付きにくい」という感想をたまに見ますが、簡単なアニメーションの説明が手薄なことに起因する気がしていて、Flutter本来のアニメーション対応のしやすさは個人的には次のように思っています。
- シンプルなアニメーションならとても簡単
- 複雑なアニメーションはその複雑度に応じて実装が煩雑になっていくが、他(僕のスキルセットにより主にiOS)と比べて特に実装しにくいわけではない
Animations with Flutter: When to Use What — YouTube に載っている次の図が分かりやすいです。公式ドキュメントで手厚く説明されているのは主に”Explicit Widgets”ですが、ImplicitおよびTransition Widgetsも把握・活用した上でそれでは実現できないときに”Explicit Widgets”を使う方がコードもシンプルで手早く組めるはずです。
というわけで、本記事ではImplicitlyAnimatedWidget(の派生クラス)について説明していきます。どういうWidgetがあるのか把握していると、いざアニメーションを組もうとした時にImplicitlyAnimatedWidgetなどで間に合うのか、より複雑な実装が必要かどうかの判断ができるようになるはずです。
ImplicitlyAnimatedWidget のドキュメントには、派生クラスとして以下の9つのWidgetがあることが分かります。
- AnimatedAlign
- AnimatedContainer
- AnimatedDefaultTextStyle
- AnimatedOpacity
- AnimatedPadding
- AnimatedPhysicalModel
- AnimatedPositioned
- AnimatedPositionedDirectional
- AnimatedTheme
それでは1つずつ見ていきます。
なお、本記事のサンプルリポジトリはこちらです:
また、デモサイトの https://animation-mono.web.app から本記事に載せてあるアニメーションサンプルを実際に触れます。
AnimatedAlign
AnimatedAlignは、その名の通りアニメーション対応のAlignで、次のように使います(1つ目なので、ソースも貼りますが、2つ目からは省略してGitHubリポジトリへのリンク+α程度にとどめます)。
buildメソッド内のAnimatedAlign以下のみがアニメーション部分のコードで、AnimationControllerなどは登場せず、duration(アニメーション長さ)とcurve(アニメーションの進み方のパラメーター)を指定するだけで簡単です。
この場合、ボタンが押される度にsetStateメソッド内で _alignment
が変わって、リビルドされてAnimatedAlignが再生成されることをトリガーにアニメーションが発火します。
この例から、AnimatedXxx Widgetは、ImplicitlyAnimatedWidgetを継承していて、Xxxに対応するWidgetを滑らかにアニメーションするものだということが分かるはずです。
Animatedではない普通のAlignの方ですが次の説明もとても良いです。
AnimatedContainer
Container Widgetは高機能ゆえに、そのAnimated版のこのWidgetも色々なプロパティを変えられて便利です。
AnimatedAlignと同様、使い方自体は難しくないので説明は省略します。
この動画解説がとても分かりやすいです。
また、CookbookにAnimate the properties of a Container の例も載っています。
AnimatedDefaultTextStyle
DefaultTextStyle を使うと下位ツリーのTextのスタイルを指定できます(上位ツリーで指定されていれば上書き)が、そのアニメーション対応版です。
AnimatedOpacity
Opacity のアニメーション対応版です。
Animatedではない普通のOpacityの方ですが次の説明もとても良いです。
また、CookbookにFade a Widget in and outの例も載っています。
AnimatedPadding
Padding のアニメーション対応版です。Paddingのレイアウトはより高機能なContainerでも可能なことと同様、AnimatedPaddingでできることはAnimatedContainerでもできます。ただ、Paddingだけを変えるアニメーションの場合は専用のこのWidgetを使った方が分かりやすくパフォーマンス的にも少しだけ有利です。
AnimatedPhysicalModel
PhysicalModel のアニメーション対応版です。PhysicalModel 自体が聞きなれないかもしれませんが、AppBar のelevationなどで使われているものという説明が分かりやすいかなと思います。elevationを変えると次のように浮き上がり具合(シャドー具合)が変わります。
次の例ですが、浮いた時に意図せず白いものが見えてしまっていて、原因が分かり次第直しておきます。
AnimatedPositioned
Positioned のアニメーション対応版です。
AnimatedPositionedDirectional
PositionedDirectionalのアニメーション対応版で、AnimatedPositionedとほとんど一緒です。このWidget同士を比べるのではなくPositionedとPositionedDirectionalを比べた方が分かりやすいです。
- Positioned: 横方向についてleftとrightで扱う
- PositionedDirectional: 横方向についてstartとendで扱う
多くの国では情報の流れは左から右(LTR)ですが、逆の国(RTL)へのケアとしてXxxDirectional系が用意されていて、同じ start
でも国によって左からの相対値か右からの相対値か変わるように指定できます。
次のMaterial Designガイドラインに豊富な例とともに詳しく説明されています。
サンプルコードでは、Directionality でrtl(右から左)を指定した上で、AnimatedPositionedDirectionalのstartを0にして右揃えにしてみましたが、実際のアプリでは MaterialApp の localizationsDelegates にて指定するの一般的なはずです。
AnimatedTheme
Theme のアニメーション対応版です。MaterialAppのthemeもこれで実装されているため、特にアニメーションを意識せずに普通にそれを切り替えると滑らかにテーマが切り替わって心地良いです。
番外編 — ImplicitlyAnimatedWidgetではないAnimated系のWidget
AnimatedXxxという名前でありながら、実装の都合かImplicitlyAnimatedWidgetを継承していないものもあります。それらも上で紹介したImplicitlyAnimatedWidgetを継承した9つのWidgetと同様にお手軽にアニメーションを組める仲間なので一緒に紹介します。
AnimatedCrossFade
ちょっと変わったWidgetでchildフィールドを持たずに、代わりにfirstChildとsecondChildを指定して、さらにcrossFadeStateにてshowFirst or showSecondを指定して、その2つのWidgetの表示をクロスフェードで滑らかに繋いでくれます。
This widget is intended to be used to fade a pair of widgets with the same width.
ドキュメントにこう記載されている通り、異なるサイズのWidgetを指定するとクロスフェードが不自然になるので注意しましょう。
日本語の解説記事もあります:
AnimatedIcon
アニメーションに対応したIconです。
以下の14種類が対応していて、普通のIconよりは種類が少ないですが、ぴったりフィットするものがあればコスパよく良い感じのボタンを設置できて良いです。
https://github.com/flutter/flutter/tree/master/packages/flutter/lib/src/material/animated_icons/data
ちなみに、これらはSVGファイルをもとに vitool で生成しているようで、自作もできそうです(今なら、Flareの方が良い気もしますが)。
なお、本記事で紹介しているこれ以外のすべてのWidgetはAnimationControllerを用意しなくても使えますが、これには必要です。
AnimatedList
ListView のアニメーション対応版に相当するものです。
これは少し使い方が難しいので、本記事で扱うべきではないかもしれませんが、便宜上一緒にします。
AnimatedList — Flutter Cookbook に基本的な使い方が載っているのでそれを見るのをお勧めします。
また、次の記事も良いです。
今回のサンプルリポジトリとは別の、たまに弄っているTODOアプリで利用してみて、ソースはこのあたりです:
https://github.com/TaskShare/taskshare-flutter/blob/master/lib/pages/task/list/task_list.dart
AnimatedModalBarrier
ModalBarrier のアニメーション対応版です。showDialog関数などを使うと暗黙的に画面遷移がなされて背面が暗くなりますが、そこの ModalRoute などで使われています。dismissibleプロパティをtrueにしておくと背面部分をタップされるとNavigator.pop()が呼ばれて閉じられます。
画面遷移を自作などするにしても、AnimatedModalBarrierを直接使うことよりも既存のRouteクラスを継承するか、PageRouteBuilderを使うことが多そうな気がします。
一応AnimatedModalBarrierを使った最小実装をしてみましたが、細かいケアがなされてないので、そのまま真似せずFlutterフレームワークでの利用箇所を読むことをお勧めします。
AnimatedSize
AnimatedSize自体でサイズを指定するのではなくChildの要求するサイズに合わせて表示領域が滑らかに伸縮するWidgetです。
表示内容によっては、次のように少し不自然なサイズ変化になるなと思いました。
次の記事が詳しいです:
AnimatedSwitcher
サンプルコード: https://github.com/mono0926/flutter-animations/blob/master/lib/pages/implicitly_animated/animated_switcher_page.dart
(実行結果は AnimatedCrossFade と同様なのでGIFは省略します。)
AnimatedSwitcherはすごく便利なWidgetで、簡単に言うと、直前のWidgetと更新後のWidgetをとても良い感じに繋げてくれます。AnimatedCrossFade的な目的にも使えますが、AnimatedCrossFadeの場合は表示対象の2つのWidgetが同一サイズではないとダメでしたが、AnimatedSwitcherはそういう場合も良い感じに表示してくれて、とても使い勝手が良いです。
アニメーション無しのパリッとした冷たい表示切替をとりあえずスムーズに繋げたい、という場合などに簡単に対応できて良いです。
次の記事を読むと、簡単なコードでいかに良い感じのアニメーションが実現できるかがよく分かるはずです。
FadeInImage
ネットワーク画像のURLを指定して表示する NetworkImage Widgetがありますが、単にそれを使うだけだと読み込み中は非表示で読み込みが終わった瞬間にパリッと切り替わる感じになってしまいます。
FadeInImage を使うと、読み込み中のPlaceholderを設定できたり読み込み完了後の表示をフェードインさせる対応がとても簡単にできます。
Cookbookの Fade in images with a placeholder にも載っています。
さらに紹介動画まであります。
Hero
これもAnimated始まりのアニメーションではないですが、AnimationControllerなしに簡単に良い感じのHeroアニメーションを実現できるので仲間に入れて良いかなと思いました。
公式ドキュメントにも手厚いサンプルと解説があります。
テキストと相性があまり良くないようで、Slow Animationを有効にすると次のように少し乱れてしまいますが、通常の速度ならあまり気にならない範囲でした。
テキスト周りの細かい制御(Materialで包むワークアラウンド?など)についてはこちらに載っています。
また、HeroはPageRouteのサブクラスによる遷移でのみ機能します。例えば、ダイアログ系はPopupRouteが使われているのでHeroは機能せず、もし対応させたい場合は次のようなワークアラウンドが必要です。
そして、ちょっと怪しいので、どうしてもという時以外は諦めた方が良い気がしています🤔
(フレームワークの意図と反することをやってかろうじて動いても、今後動作し続けられるかとても危ういと思います。)
長くなりましたが、以上となります。次の記事では、AnimationControllerの扱い方の説明と、それを用いたTransition系のアニメーションWidgetを紹介していきます。
最後に本記事のサンプルリポジトリを再度載せておきます。
参考
本記事を書いた後に見つけましたが、以下も同様の内容でよくまとまっています。