1. はじめに
アニメーションは、アプリのユーザー エクスペリエンスを向上させ、重要な情報をユーザーに伝え、アプリをより洗練された使いやすいものにするための優れた方法です。
Flutter のアニメーション フレームワークの概要
Flutter は、フレームごとにウィジェット ツリーの一部を再構築することで、アニメーション効果を表示します。アニメーション効果やその他の API が組み込まれており、アニメーションの作成と構成を容易に行うことができます。
- 暗黙的なアニメーションは、アニメーション全体を自動的に実行する事前構築済みのアニメーション効果です。アニメーションのターゲット値が変更されると、現在値からターゲット値までアニメーションを実行し、その間の各値を表示して、ウィジェットがスムーズにアニメーション化されるようにします。暗黙的なアニメーションの例としては、
AnimatedSize、AnimatedScale、AnimatedPositionedなどがあります。 - 明示的アニメーションも事前構築されたアニメーション効果ですが、動作させるには
Animationオブジェクトが必要です。たとえば、SizeTransition、ScaleTransition、PositionedTransitionなどがあります。 - Animation は、実行中または停止中のアニメーションを表すクラスで、アニメーションが実行されるターゲット値を表す値と、アニメーションが特定の時点で画面に表示する現在の値を表すステータスで構成されます。
Listenableのサブクラスであり、アニメーションの実行中にステータスが変化するとリスナーに通知します。 - AnimationController は、アニメーションを作成してその状態を制御する方法です。
forward()、reset()、stop()、repeat()などのメソッドを使用すると、表示されるアニメーション効果(スケール、サイズ、位置など)を定義しなくてもアニメーションを制御できます。 - トゥイーンは、開始値と終了値の間の値を補間するために使用され、double、
Offset、Colorなどの任意の型を表すことができます。 - カーブは、パラメータの時間経過に伴う変化率を調整するために使用されます。アニメーションの実行時に、アニメーションの開始時または終了時の変化率を速くしたり遅くしたりするために、イージング曲線を適用するのが一般的です。曲線は 0.0 ~ 1.0 の入力値を受け取り、0.0 ~ 1.0 の出力値を返します。
作成するアプリの概要
この Codelab では、さまざまなアニメーション効果とテクニックを特徴とする多肢選択式のクイズゲームを作成します。

このガイドでは、次の方法について説明します。
- サイズと色をアニメーション化するウィジェットを作成する
- 3D カードフリップ効果を構築する
- アニメーション パッケージの高度な組み込みアニメーション効果を使用する
- 最新バージョンの Android で利用可能な予測型「戻る」ジェスチャーのサポートを追加
学習内容
この Codelab では、次のことを学びます。
- 暗黙的なアニメーション効果を使用して、多くのコードを必要とせずに見栄えの良いアニメーションを実現する方法。
- 明示的にアニメーション化された効果を使用して、
AnimatedSwitcherやAnimationControllerなどのビルド済みアニメーション ウィジェットを使用して独自の効果を設定する方法。 AnimationControllerを使用して、3D 効果を表示する独自のウィジェットを定義する方法。animationsパッケージを使用して、最小限の設定で凝ったアニメーション効果を表示する方法。
必要なもの
- Flutter SDK
- IDE(VSCode、Android Studio、IntelliJ など)
2. Flutter の開発環境をセットアップする
このラボを完了するには、Flutter SDK とエディタの 2 つのソフトウェアが必要です。
この Codelab は、次のいずれかのデバイスを使って実行できます。
- パソコンに接続され、デベロッパー モードに設定された物理デバイス(Android(ステップ 7 で予測型「戻る」を実装する場合は推奨)または iOS)
- iOS シミュレータ(Xcode ツールのインストールが必要)。
- Android Emulator(Android Studio でセットアップが必要)。
- ブラウザ(デバッグには Chrome が必要)
- Windows、Linux、macOS のデスクトップ パソコン。開発はデプロイする予定のプラットフォームで行う必要があります。たとえば、Windows のデスクトップ アプリを開発する場合は、適切なビルドチェーンにアクセスできるように Windows で開発する必要があります。オペレーティング システム固有の要件については、docs.flutter.dev/desktop に詳しい説明があります。
インストールを確認する
Flutter SDK が正しく構成され、上記のターゲット プラットフォームのいずれかがインストールされていることを確認するには、Flutter Doctor ツールを使用します。
$ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.24.2, on macOS 14.6.1 23G93 darwin-arm64, locale
en)
[✓] Android toolchain - develop for Android devices
[✓] Xcode - develop for iOS and macOS
[✓] Chrome - develop for the web
[✓] Android Studio
[✓] IntelliJ IDEA Ultimate Edition
[✓] VS Code
[✓] Connected device (4 available)
[✓] Network resources
• No issues found!
3. スターター アプリを実行する
スターター アプリをダウンロードする
git を使用して、GitHub の flutter/samples リポジトリからスタートアプリのクローンを作成します。
git clone https://github.com/flutter/codelabs.git cd codelabs/animations/step_01/
または、ソースコードを ZIP ファイルとしてダウンロードすることもできます。
アプリを実行する
アプリを実行するには、flutter run コマンドを使用して、android、ios、chrome などのターゲット デバイスを指定します。サポートされているプラットフォームの一覧については、サポートされているプラットフォームのページをご覧ください。
flutter run -d android
任意の IDE を使用してアプリを実行し、デバッグすることもできます。詳しくは、Flutter の公式ドキュメントをご覧ください。
コードについて
スターター アプリは、モデルビュービューモデル(MVVM)設計パターンに従った 2 つの画面で構成される多肢選択式のクイズゲームです。QuestionScreen(ビュー)は、QuizViewModel(ビューモデル)クラスを使用して、QuestionBank(モデル)クラスからユーザーに多肢選択式の質問をします。
- home_screen.dart - [New Game] ボタンのある画面を表示します。
- main.dart - Material 3 を使用してホーム画面を表示するように
MaterialAppを構成します。 - model.dart - アプリ全体で使用されるコアクラスを定義します。
- question_screen.dart - クイズゲームの UI を表示します。
- view_model.dart -
QuestionScreenで表示されるクイズゲームの状態とロジックを保存します。

このアプリは、ユーザーが [New Game] ボタンを押したときに Flutter の Navigator クラスによって表示されるデフォルトのビュー トランジションを除き、アニメーション効果をまだサポートしていません。
4. 暗黙的なアニメーション効果を使用する
暗黙的なアニメーションは特別な構成を必要としないため、多くの状況で最適な選択肢となります。このセクションでは、アニメーション スコアボードを表示するように StatusBar ウィジェットを更新します。一般的な暗黙的アニメーション効果については、ImplicitlyAnimatedWidget API ドキュメントをご覧ください。

アニメーションなしのスコアボード ウィジェットを作成する
次のコードを含む新しいファイル lib/scoreboard.dart を作成します。
lib/scoreboard.dart
import 'package:flutter/material.dart';
class Scoreboard extends StatelessWidget {
final int score;
final int totalQuestions;
const Scoreboard({
super.key,
required this.score,
required this.totalQuestions,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (var i = 0; i < totalQuestions; i++)
Icon(
Icons.star,
size: 50,
color: score < i + 1
? Colors.grey.shade400
: Colors.yellow.shade700,
),
],
),
);
}
}
次に、StatusBar ウィジェットの子に Scoreboard ウィジェットを追加し、以前にスコアと合計質問数を表示していた Text ウィジェットを置き換えます。エディタによって、必要な import "scoreboard.dart" がファイルの先頭に自動的に追加されます。
lib/question_screen.dart
class StatusBar extends StatelessWidget {
final QuizViewModel viewModel;
const StatusBar({required this.viewModel, super.key});
@override
Widget build(BuildContext context) {
return Card(
elevation: 4,
child: Padding(
padding: EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Scoreboard( // NEW
score: viewModel.score, // NEW
totalQuestions: viewModel.totalQuestions, // NEW
),
],
),
),
);
}
}
このウィジェットには、各質問の星アイコンが表示されます。質問に正解すると、アニメーションなしで別の星がすぐに点灯します。次の手順では、スコアのサイズと色をアニメーション化して、スコアが変更されたことをユーザーに通知します。
暗黙的なアニメーション効果を使用する
AnimatedScale ウィジェットを使用して、星がアクティブになったときに scale の金額を 0.5 から 1.0 に変更する AnimatedStar という新しいウィジェットを作成します。
lib/scoreboard.dart
import 'package:flutter/material.dart';
class Scoreboard extends StatelessWidget {
final int score;
final int totalQuestions;
const Scoreboard({
super.key,
required this.score,
required this.totalQuestions,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (var i = 0; i < totalQuestions; i++)
AnimatedStar(isActive: score > i), // Edit this line.
],
),
);
}
}
class AnimatedStar extends StatelessWidget { // Add from here...
final bool isActive;
final Duration _duration = const Duration(milliseconds: 1000);
final Color _deactivatedColor = Colors.grey.shade400;
final Color _activatedColor = Colors.yellow.shade700;
AnimatedStar({super.key, required this.isActive});
@override
Widget build(BuildContext context) {
return AnimatedScale(
scale: isActive ? 1.0 : 0.5,
duration: _duration,
child: Icon(
Icons.star,
size: 50,
color: isActive ? _activatedColor : _deactivatedColor,
),
);
}
} // To here.
ユーザーが質問に正しく答えると、AnimatedStar ウィジェットは暗黙的なアニメーションを使用してサイズを更新します。ここでは Icon の color はアニメーション化されず、scale のみがアニメーション化されます。これは AnimatedScale ウィジェットによって行われます。

Tween を使用して 2 つの値を補間する
isActive フィールドが true に変更されると、AnimatedStar ウィジェットの色がすぐに変わります。
アニメーション カラー効果を実現するには、AnimatedContainer ウィジェット(ImplicitlyAnimatedWidget の別のサブクラス)を使用します。このウィジェットは、色を含むすべての属性を自動的にアニメーション化できるためです。残念ながら、ウィジェットにはコンテナではなくアイコンを表示する必要があります。
アイコンの形状間のトランジション効果を実装する AnimatedIcon を試すこともできます。ただし、AnimatedIcons クラスには星アイコンのデフォルト実装はありません。
代わりに、Tween をパラメータとして受け取る TweenAnimationBuilder という ImplicitlyAnimatedWidget の別のサブクラスを使用します。トゥイーンは、2 つの値(begin と end)を取得して中間値を計算し、アニメーションで表示できるようにするクラスです。この例では、アニメーション効果の作成に必要な Tween インターフェースを満たす ColorTween を使用します。
Icon ウィジェットを選択し、IDE の [Wrap with Builder] クイック アクションを使用して、名前を TweenAnimationBuilder に変更します。次に、期間と ColorTween を指定します。
lib/scoreboard.dart
class AnimatedStar extends StatelessWidget {
final bool isActive;
final Duration _duration = const Duration(milliseconds: 1000);
final Color _deactivatedColor = Colors.grey.shade400;
final Color _activatedColor = Colors.yellow.shade700;
AnimatedStar({super.key, required this.isActive});
@override
Widget build(BuildContext context) {
return AnimatedScale(
scale: isActive ? 1.0 : 0.5,
duration: _duration,
child: TweenAnimationBuilder( // Add from here...
duration: _duration,
tween: ColorTween(
begin: _deactivatedColor,
end: isActive ? _activatedColor : _deactivatedColor,
),
builder: (context, value, child) { // To here.
return Icon(Icons.star, size: 50, color: value); // And modify this line.
},
),
);
}
}
アプリをホットリロードして、新しいアニメーションを確認します。

ColorTween の end 値は、isActive パラメータの値に基づいて変化します。これは、Tween.end 値が変更されるたびに TweenAnimationBuilder がアニメーションを再実行するためです。この場合、新しいアニメーションは現在のアニメーション値から新しい終了値まで実行されます。これにより、いつでも(アニメーションの実行中でも)色を変更し、正しい中間値でスムーズなアニメーション効果を表示できます。
カーブを適用する
どちらのアニメーション効果も一定の速度で実行されますが、アニメーションは速度が変化する方が視覚的に面白く、情報量も多くなることがよくあります。
Curve は、パラメータの時間経過に伴う変化率を定義するイージング関数を適用します。Flutter には、Curves クラスに easeIn や easeOut などの事前構築済みのイージング曲線が用意されています。


これらの図(Curves API のドキュメント ページで入手可能)は、曲線の仕組みを理解するうえで役立ちます。曲線は、0.0 ~ 1.0 の入力値(x 軸に表示)を 0.0 ~ 1.0 の出力値(y 軸に表示)に変換します。これらの図には、イージング曲線を使用した場合のさまざまなアニメーション効果のプレビューも示されています。
AnimatedStar に _curve という新しいフィールドを作成し、AnimatedScale ウィジェットと TweenAnimationBuilder ウィジェットにパラメータとして渡します。
lib/scoreboard.dart
class AnimatedStar extends StatelessWidget {
final bool isActive;
final Duration _duration = const Duration(milliseconds: 1000);
final Color _deactivatedColor = Colors.grey.shade400;
final Color _activatedColor = Colors.yellow.shade700;
final Curve _curve = Curves.elasticOut; // NEW
AnimatedStar({super.key, required this.isActive});
@override
Widget build(BuildContext context) {
return AnimatedScale(
scale: isActive ? 1.0 : 0.5,
curve: _curve, // NEW
duration: _duration,
child: TweenAnimationBuilder(
curve: _curve, // NEW
duration: _duration,
tween: ColorTween(
begin: _deactivatedColor,
end: isActive ? _activatedColor : _deactivatedColor,
),
builder: (context, value, child) {
return Icon(Icons.star, size: 50, color: value);
},
),
);
}
}
この例では、elasticOut カーブは、ばねの動きで始まり、最後に向かってバランスが取れる、誇張されたばねの効果を提供します。

アプリをホットリロードして、AnimatedSize と TweenAnimationBuilder にこの曲線が適用されていることを確認します。

DevTools を使用してアニメーションの速度を下げる
アニメーション効果をデバッグするために、Flutter DevTools にはアプリ内のすべてのアニメーションを遅くして、アニメーションをより明確に確認できるようにする機能が用意されています。
DevTools を開くには、アプリがデバッグモードで実行されていることを確認し、VSCode の [Debug toolbar] で [Widget Inspector] を選択するか、IntelliJ / Android Studio の [Debug tool window] で [Open Flutter DevTools] ボタンを選択して、Widget Inspector を開きます。


ウィジェット インスペクタが開いたら、ツールバーの [アニメーションを遅くする] ボタンをクリックします。

5. 明示的なアニメーション効果を使用する
暗黙的なアニメーションと同様に、明示的なアニメーションは事前構築されたアニメーション効果ですが、ターゲット値ではなく Animation オブジェクトをパラメータとして受け取ります。そのため、ナビゲーション遷移、AnimatedSwitcher、AnimationController などによってアニメーションがすでに定義されている状況で便利です。
明示的なアニメーション効果を使用する
明示的なアニメーション効果を使用するには、Card ウィジェットを AnimatedSwitcher でラップします。
lib/question_screen.dart
class QuestionCard extends StatelessWidget {
final String? question;
const QuestionCard({required this.question, super.key});
@override
Widget build(BuildContext context) {
return AnimatedSwitcher( // NEW
duration: const Duration(milliseconds: 300), // NEW
child: Card(
key: ValueKey(question),
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
), // NEW
);
}
}
AnimatedSwitcher はデフォルトでクロスフェード効果を使用しますが、transitionBuilder パラメータを使用してオーバーライドできます。トランジション ビルダーは、AnimatedSwitcher に渡された子ウィジェットと Animation オブジェクトを提供します。これは、明示的なアニメーションを使用する絶好の機会です。
この Codelab で使用する最初のエクスプリシット アニメーションは SlideTransition です。これは、インカミング ウィジェットとアウトゴーイング ウィジェットが移動する開始オフセットと終了オフセットを定義する Animation<Offset> を受け取ります。
トゥイーンには、任意の Animation をトゥイーンが適用された別の Animation に変換するヘルパー関数 animate() があります。つまり、Tween を使用して、AnimatedSwitcher によって提供される Animation を Animation に変換し、SlideTransition ウィジェットに提供できます。
lib/question_screen.dart
class QuestionCard extends StatelessWidget {
final String? question;
const QuestionCard({required this.question, super.key});
@override
Widget build(BuildContext context) {
return AnimatedSwitcher(
transitionBuilder: (child, animation) { // Add from here...
final curveAnimation = CurveTween(
curve: Curves.easeInCubic,
).animate(animation);
final offsetAnimation = Tween<Offset>(
begin: Offset(-0.1, 0.0),
end: Offset.zero,
).animate(curveAnimation);
return SlideTransition(position: offsetAnimation, child: child);
}, // To here.
duration: const Duration(milliseconds: 300),
child: Card(
key: ValueKey(question),
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
),
);
}
}
ここでは、Tween.animate を使用して Curve を Animation に適用し、0.0 ~ 1.0 の範囲の Tween から x 軸上で -0.1 ~ 0.0 に移行する Tween に変換します。
また、Animation クラスには、任意の Tween(または Animatable)を受け取って新しい Animation に変換する drive() 関数があります。これにより、トゥイーンを「チェーン」でつなぐことができ、結果として得られるコードがより簡潔になります。
lib/question_screen.dart
transitionBuilder: (child, animation) {
var offsetAnimation = animation
.drive(CurveTween(curve: Curves.easeInCubic))
.drive(Tween<Offset>(begin: Offset(-0.1, 0.0), end: Offset.zero));
return SlideTransition(position: offsetAnimation, child: child);
},
明示的なアニメーションを使用するもう 1 つの利点は、それらを一緒に構成できることです。SlideTransition ウィジェットをラップして同じ曲線アニメーションを使用する、別の明示的なアニメーション FadeTransition を追加します。
lib/question_screen.dart
return AnimatedSwitcher(
transitionBuilder: (child, animation) {
final curveAnimation = CurveTween(
curve: Curves.easeInCubic,
).animate(animation);
final offsetAnimation = Tween<Offset>(
begin: Offset(-0.1, 0.0),
end: Offset.zero,
).animate(curveAnimation);
final fadeInAnimation = curveAnimation; // NEW
return FadeTransition( // NEW
opacity: fadeInAnimation, // NEW
child: SlideTransition(position: offsetAnimation, child: child), // NEW
); // NEW
},
layoutBuilder をカスタマイズする
AnimationSwitcher に小さな問題があることに気づくかもしれません。QuestionCard が新しい質問に切り替わると、アニメーションの実行中は利用可能なスペースの中央にレイアウトされますが、アニメーションが停止すると、ウィジェットは画面の上部にスナップします。質問カードの最終位置がアニメーションの実行中の位置と一致しないため、アニメーションがぎくしゃくします。

この問題を解決するために、AnimatedSwitcher にはレイアウトを定義するために使用できる layoutBuilder パラメータもあります。この関数を使用して、カードを画面の上部に配置するようにレイアウト ビルダーを構成します。
lib/question_screen.dart
@override
Widget build(BuildContext context) {
return AnimatedSwitcher(
layoutBuilder: (currentChild, previousChildren) {
return Stack(
alignment: Alignment.topCenter,
children: <Widget>[
...previousChildren,
if (currentChild != null) currentChild,
],
);
},
このコードは、AnimatedSwitcher クラスの defaultLayoutBuilder の変更版ですが、Alignment.center ではなく Alignment.topCenter を使用しています。
概要
- 明示的なアニメーションは、
Animationオブジェクトを受け取るアニメーション効果です(ターゲットvalueとdurationを受け取るImplicitlyAnimatedWidgetsとは対照的です)。 Animationクラスは実行中のアニメーションを表しますが、特定のエフェクトは定義しません。Tween().animateまたはAnimation.drive()を使用して、TweensとCurves(CurveTweenを使用)をアニメーションに適用します。AnimatedSwitcherのlayoutBuilderパラメータを使用して、子のレイアウト方法を調整します。
6. アニメーションの状態を制御する
これまでのところ、すべてのアニメーションはフレームワークによって自動的に実行されてきました。暗黙的アニメーションは自動的に実行され、明示的アニメーション効果が正しく動作するには Animation が必要です。このセクションでは、AnimationController を使用して独自の Animation オブジェクトを作成し、TweenSequence を使用して Tween を結合する方法について説明します。
AnimationController を使用してアニメーションを実行する
AnimationController を使用してアニメーションを作成するには、次の手順を行います。
StatefulWidgetを作成するStateクラスでSingleTickerProviderStateMixinミックスインを使用して、AnimationControllerにTickerを提供します。initStateライフサイクル メソッドでAnimationControllerを初期化し、現在のStateオブジェクトをvsync(TickerProvider)パラメータに渡します。AnimationControllerがAnimatedBuilderを使用してリスナーに通知するか、listen()とsetStateを手動で呼び出すたびに、ウィジェットが再ビルドされるようにします。
新しいファイル flip_effect.dart を作成し、次のコードをコピーして貼り付けます。
lib/flip_effect.dart
import 'dart:math' as math;
import 'package:flutter/widgets.dart';
class CardFlipEffect extends StatefulWidget {
final Widget child;
final Duration duration;
const CardFlipEffect({
super.key,
required this.child,
required this.duration,
});
@override
State<CardFlipEffect> createState() => _CardFlipEffectState();
}
class _CardFlipEffectState extends State<CardFlipEffect>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
Widget? _previousChild;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: widget.duration,
);
_animationController.addListener(() {
if (_animationController.value == 1) {
_animationController.reset();
}
});
}
@override
void didUpdateWidget(covariant CardFlipEffect oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.child.key != oldWidget.child.key) {
_handleChildChanged(widget.child, oldWidget.child);
}
}
void _handleChildChanged(Widget newChild, Widget previousChild) {
_previousChild = previousChild;
_animationController.forward();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..rotateX(_animationController.value * math.pi),
child: _animationController.isAnimating
? _animationController.value < 0.5
? _previousChild
: Transform.flip(flipY: true, child: child)
: child,
);
},
child: widget.child,
);
}
}
このクラスは AnimationController を設定し、フレームワークが didUpdateWidget を呼び出してウィジェットの構成が変更され、新しい子ウィジェットが存在する可能性があることを通知するたびに、アニメーションを再実行します。
AnimatedBuilder は、AnimationController がリスナーに通知するたびにウィジェット ツリーが再構築されるようにします。Transform ウィジェットは、カードが裏返される様子をシミュレートする 3D 回転効果を適用するために使用されます。
このウィジェットを使用するには、各回答カードを CardFlipEffect ウィジェットでラップします。Card ウィジェットに key を指定してください。
lib/question_screen.dart
@override
Widget build(BuildContext context) {
return GridView.count(
shrinkWrap: true,
crossAxisCount: 2,
childAspectRatio: 5 / 2,
children: List.generate(answers.length, (index) {
var color = Theme.of(context).colorScheme.primaryContainer;
if (correctAnswer == index) {
color = Theme.of(context).colorScheme.tertiaryContainer;
}
return CardFlipEffect( // NEW
duration: const Duration(milliseconds: 300), // NEW
child: Card.filled( // NEW
key: ValueKey(answers[index]), // NEW
color: color,
elevation: 2,
margin: EdgeInsets.all(8),
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () => onTapped(index),
child: Padding(
padding: EdgeInsets.all(16.0),
child: Center(
child: Text(
answers.length > index ? answers[index] : '',
style: Theme.of(context).textTheme.titleMedium,
overflow: TextOverflow.clip,
),
),
),
),
), // NEW
);
}),
);
}
アプリをホットリロードして、CardFlipEffect ウィジェットを使用して回答カードが反転するのを確認します。

このクラスは、明示的なアニメーション効果に似ていることに気づくかもしれません。実際、AnimatedWidget クラスを直接拡張して独自のバージョンを実装することをおすすめします。残念ながら、このクラスは State に前のウィジェットを保存する必要があるため、StatefulWidget を使用する必要があります。独自の明示的なアニメーション効果を作成する方法については、AnimatedWidget の API ドキュメントをご覧ください。
TweenSequence を使用して遅延を追加する
このセクションでは、各カードが 1 つずつ反転するように、CardFlipEffect ウィジェットに遅延を追加します。まず、delayAmount という新しいフィールドを追加します。
lib/flip_effect.dart
class CardFlipEffect extends StatefulWidget {
final Widget child;
final Duration duration;
final double delayAmount; // NEW
const CardFlipEffect({
super.key,
required this.child,
required this.duration,
required this.delayAmount, // NEW
});
@override
State<CardFlipEffect> createState() => _CardFlipEffectState();
}
次に、delayAmount を AnswerCards ビルドメソッドに追加します。
lib/question_screen.dart
@override
Widget build(BuildContext context) {
return GridView.count(
shrinkWrap: true,
crossAxisCount: 2,
childAspectRatio: 5 / 2,
children: List.generate(answers.length, (index) {
var color = Theme.of(context).colorScheme.primaryContainer;
if (correctAnswer == index) {
color = Theme.of(context).colorScheme.tertiaryContainer;
}
return CardFlipEffect(
delayAmount: index.toDouble() / 2, // NEW
duration: const Duration(milliseconds: 300),
child: Card.filled(
key: ValueKey(answers[index]),
次に、_CardFlipEffectState で、TweenSequence を使用して遅延を適用する新しい Animation を作成します。このコードでは、Future.delayed など、dart:async ライブラリのユーティリティは使用されていません。これは、遅延がアニメーションの一部であり、ウィジェットが AnimationController を使用するときに明示的に制御するものではないためです。同じ TickerProvider を使用するため、DevTools でアニメーションを遅くすると、アニメーション効果のデバッグが容易になります。
TweenSequence を使用するには、2 つの TweenSequenceItem を作成します。1 つは、アニメーションを相対的な期間 0 に保つ ConstantTween と、0.0 から 1.0 に移行する通常の Tween を含みます。
lib/flip_effect.dart
class _CardFlipEffectState extends State<CardFlipEffect>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
Widget? _previousChild;
late final Animation<double> _animationWithDelay; // NEW
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: widget.duration * (widget.delayAmount + 1),
);
_animationController.addListener(() {
if (_animationController.value == 1) {
_animationController.reset();
}
});
_animationWithDelay = TweenSequence<double>([ // Add from here...
if (widget.delayAmount > 0)
TweenSequenceItem(
tween: ConstantTween<double>(0.0),
weight: widget.delayAmount,
),
TweenSequenceItem(tween: Tween(begin: 0.0, end: 1.0), weight: 1.0),
]).animate(_animationController); // To here.
}
最後に、build メソッドで AnimationController のアニメーションを新しい遅延アニメーションに置き換えます。
lib/flip_effect.dart
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationWithDelay, // Modify this line
builder: (context, child) {
return Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..rotateX(_animationWithDelay.value * math.pi), // And this line
child: _animationController.isAnimating
? _animationWithDelay.value < 0.5 // And this one.
? _previousChild
: Transform.flip(flipY: true, child: child)
: child,
);
},
child: widget.child,
);
}
アプリをホットリロードして、カードが 1 枚ずつ反転する様子を確認します。チャレンジとして、Transform ウィジェットが提供する 3D 効果の視点を変更してみてください。

7. カスタム ナビゲーション トランジションを使用する
ここまでで、単一の画面でエフェクトをカスタマイズする方法を見てきましたが、アニメーションのもう 1 つの使用方法は、画面間の遷移に使用することです。このセクションでは、組み込みのアニメーション効果と、pub.dev の公式 animations パッケージで提供される高度な組み込みアニメーション効果を使用して、画面遷移にアニメーション効果を適用する方法を学びます。
ナビゲーションの遷移をアニメーション化する
PageRouteBuilder クラスは、遷移アニメーションをカスタマイズできる Route です。このコールバックをオーバーライドすると、Navigator によって実行されるインカミング アニメーションとアウトゴーイング アニメーションを表す 2 つの Animation オブジェクトが提供されます。transitionBuilder
遷移アニメーションをカスタマイズするには、MaterialPageRoute を PageRouteBuilder に置き換えます。ユーザーが HomeScreen から QuestionScreen に移動するときの遷移アニメーションをカスタマイズするには、FadeTransition(明示的にアニメーション化されたウィジェット)を使用して、新しい画面を前の画面の上にフェードインします。
lib/home_screen.dart
ElevatedButton(
onPressed: () {
// Show the question screen to start the game
Navigator.push(
context,
PageRouteBuilder( // Add from here...
pageBuilder: (context, animation, secondaryAnimation) {
return const QuestionScreen();
},
transitionsBuilder:
(context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: child,
);
},
), // To here.
);
},
child: Text('New Game'),
),
アニメーション パッケージは、FadeThroughTransition などの凝った組み込み済みアニメーション効果を提供します。アニメーション パッケージをインポートし、FadeTransition を FadeThroughTransition ウィジェットに置き換えます。
lib/home_screen.dart
import 'package;animations/animations.dart';
ElevatedButton(
onPressed: () {
// Show the question screen to start the game
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) {
return const QuestionScreen();
},
transitionsBuilder:
(context, animation, secondaryAnimation, child) {
return FadeThroughTransition( // Add from here...
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
); // To here.
},
),
);
},
child: Text('New Game'),
),
予測型「戻る」アニメーションをカスタマイズする

予測型「戻る」は、ナビゲーションの前に現在のルートやアプリの背後にあるものを確認できる新しい Android 機能です。ピーク アニメーションは、ユーザーが画面をドラッグして戻す際の指の位置によって駆動されます。
Flutter は、ナビゲーション スタックにポップするルートがない場合、つまり「戻る」でアプリが終了する場合に、システムレベルで機能を有効にすることで、システムの予測型「戻る」をサポートします。このアニメーションはシステムによって処理され、Flutter 自体では処理されません。
Flutter アプリ内のルート間を移動する際、Flutter は予測型「戻る」もサポートしています。PredictiveBackPageTransitionsBuilder という特別な PageTransitionsBuilder が、システムの予測型「戻る」ジェスチャーをリッスンし、ジェスチャーの進行状況に応じてページ遷移を制御します。
予測型「戻る」は Android U 以降でのみサポートされていますが、Flutter は元の「戻る」ジェスチャーの動作と ZoomPageTransitionBuilder に適切にフォールバックします。詳しくは、ブログ投稿をご覧ください。このブログ投稿には、独自のアプリで設定する方法についてのセクションもあります。
アプリの ThemeData 構成で、Android では PredictiveBack を使用し、他のプラットフォームではアニメーション パッケージのフェードスルー トランジション効果を使用するように PageTransitionsTheme を構成します。
lib/main.dart
import 'package:animations/animations.dart'; // NEW
import 'package:flutter/material.dart';
import 'home_screen.dart';
void main() {
runApp(MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
pageTransitionsTheme: PageTransitionsTheme(
builders: {
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(), // NEW
TargetPlatform.iOS: FadeThroughPageTransitionsBuilder(), // NEW
TargetPlatform.macOS: FadeThroughPageTransitionsBuilder(), // NEW
TargetPlatform.windows: FadeThroughPageTransitionsBuilder(), // NEW
TargetPlatform.linux: FadeThroughPageTransitionsBuilder(), // NEW
},
),
),
home: HomeScreen(),
);
}
}
これで、Navigator.push() コールバックを MaterialPageRoute に変更できます。
lib/home_screen.dart
ElevatedButton(
onPressed: () {
// Show the question screen to start the game
Navigator.push(
context,
MaterialPageRoute( // Add from here...
builder: (context) {
return const QuestionScreen();
},
), // To here.
);
},
child: Text('New Game'),
),
FadeThroughTransition を使用して現在の質問を変更する
AnimatedSwitcher ウィジェットは、ビルダー コールバックで 1 つの Animation のみを提供します。これに対処するため、animations パッケージには PageTransitionSwitcher が用意されています。
lib/question_screen.dart
class QuestionCard extends StatelessWidget {
final String? question;
const QuestionCard({required this.question, super.key});
@override
Widget build(BuildContext context) {
return PageTransitionSwitcher( // Add from here...
layoutBuilder: (entries) {
return Stack(alignment: Alignment.topCenter, children: entries);
},
transitionBuilder: (child, animation, secondaryAnimation) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
}, // To here.
duration: const Duration(milliseconds: 300),
child: Card(
key: ValueKey(question),
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
),
);
}
}
OpenContainer を使用する

animations パッケージの OpenContainer ウィジェットは、2 つのウィジェット間に視覚的なつながりを作成するために拡大するコンテナ変換アニメーション効果を提供します。
closedBuilder から返されたウィジェットが最初に表示され、コンテナがタップされたとき、または openContainer コールバックが呼び出されたときに、openBuilder から返されたウィジェットに展開されます。
openContainer コールバックをビューモデルに接続するには、新しいパスを追加して viewModel を QuestionCard ウィジェットに渡し、「ゲームオーバー」画面を表示するために使用されるコールバックを保存します。
lib/question_screen.dart
class QuestionScreen extends StatefulWidget {
const QuestionScreen({super.key});
@override
State<QuestionScreen> createState() => _QuestionScreenState();
}
class _QuestionScreenState extends State<QuestionScreen> {
late final QuizViewModel viewModel = QuizViewModel(
onGameOver: _handleGameOver,
);
VoidCallback? _showGameOverScreen; // NEW
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: viewModel,
builder: (context, child) {
return Scaffold(
appBar: AppBar(
actions: [
TextButton(
onPressed:
viewModel.hasNextQuestion && viewModel.didAnswerQuestion
? () {
viewModel.getNextQuestion();
}
: null,
child: const Text('Next'),
),
],
),
body: Center(
child: Column(
children: [
QuestionCard( // NEW
onChangeOpenContainer: _handleChangeOpenContainer, // NEW
question: viewModel.currentQuestion?.question, // NEW
viewModel: viewModel, // NEW
), // NEW
Spacer(),
AnswerCards(
onTapped: (index) {
viewModel.checkAnswer(index);
},
answers: viewModel.currentQuestion?.possibleAnswers ?? [],
correctAnswer: viewModel.didAnswerQuestion
? viewModel.currentQuestion?.correctAnswer
: null,
),
StatusBar(viewModel: viewModel),
],
),
),
);
},
);
}
void _handleChangeOpenContainer(VoidCallback openContainer) { // NEW
_showGameOverScreen = openContainer; // NEW
} // NEW
void _handleGameOver() { // NEW
if (_showGameOverScreen != null) { // NEW
_showGameOverScreen!(); // NEW
} // NEW
} // NEW
}
新しいウィジェット GameOverScreen を追加します。
lib/question_screen.dart
class GameOverScreen extends StatelessWidget {
final QuizViewModel viewModel;
const GameOverScreen({required this.viewModel, super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(automaticallyImplyLeading: false),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Scoreboard(
score: viewModel.score,
totalQuestions: viewModel.totalQuestions,
),
Text('You Win!', style: Theme.of(context).textTheme.displayLarge),
Text(
'Score: ${viewModel.score} / ${viewModel.totalQuestions}',
style: Theme.of(context).textTheme.displaySmall,
),
ElevatedButton(
child: Text('OK'),
onPressed: () {
Navigator.popUntil(context, (route) => route.isFirst);
},
),
],
),
),
);
}
}
QuestionCard ウィジェットで、Card を animations パッケージの OpenContainer ウィジェットに置き換え、viewModel とオープン コンテナ コールバック用の 2 つの新しいフィールドを追加します。
lib/question_screen.dart
class QuestionCard extends StatelessWidget {
final String? question;
const QuestionCard({
required this.onChangeOpenContainer,
required this.question,
required this.viewModel,
super.key,
});
final ValueChanged<VoidCallback> onChangeOpenContainer;
final QuizViewModel viewModel;
static const _backgroundColor = Color(0xfff2f3fa);
@override
Widget build(BuildContext context) {
return PageTransitionSwitcher(
duration: const Duration(milliseconds: 200),
transitionBuilder: (child, animation, secondaryAnimation) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
},
child: OpenContainer( // NEW
key: ValueKey(question), // NEW
tappable: false, // NEW
closedColor: _backgroundColor, // NEW
closedShape: const RoundedRectangleBorder( // NEW
borderRadius: BorderRadius.all(Radius.circular(12.0)), // NEW
), // NEW
closedElevation: 4, // NEW
closedBuilder: (context, openContainer) { // NEW
onChangeOpenContainer(openContainer); // NEW
return ColoredBox( // NEW
color: _backgroundColor, // NEW
child: Padding( // NEW
padding: const EdgeInsets.all(16.0), // NEW
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
);
},
openBuilder: (context, closeContainer) { // NEW
return GameOverScreen(viewModel: viewModel); // NEW
}, // NEW
),
);
}
}

8. 完了
お疲れさまでした。これで、Flutter アプリにアニメーション効果を追加し、Flutter のアニメーション システムのコア コンポーネントについて学習しました。具体的には、次のことを学びました。
ImplicitlyAnimatedWidgetの使用方法ExplicitlyAnimatedWidgetの使用方法- アニメーションに
CurvesとTweensを適用する方法 AnimatedSwitcherやPageRouteBuilderなどのビルド済みのトランジション ウィジェットの使用方法animationsパッケージのFadeThroughTransitionやOpenContainerなどの、構築済みの凝ったアニメーション効果の使用方法- デフォルトの遷移アニメーションをカスタマイズする方法(Android での予測型「戻る」のサポートの追加を含む)。

次のステップ
次の Codelab をご覧ください。
- マテリアル 3 でアニメーション化されたレスポンシブ アプリ レイアウトを作成する
- Flutter 向けマテリアル モーションで美しい遷移を作成する
- Flutter アプリを「退屈なアプリ」から「見栄えの良いアプリ」に変える
または、さまざまなアニメーション手法を紹介するアニメーションのサンプルアプリをダウンロードしてください。
関連情報
その他のアニメーション リソースについては、flutter.dev をご覧ください。
- アニメーションの概要
- アニメーションのチュートリアル(チュートリアル)
- 暗黙的なアニメーション(チュートリアル)
- コンテナのプロパティをアニメーション化する(クックブック)
- ウィジェットをフェードイン / フェードアウトする(クックブック)
- ヒーロー アニメーション
- ページルートの遷移をアニメーションにする(クックブック)
- 物理シミュレーションを使用してウィジェットをアニメーション化する(クックブック)
- スタッガード アニメーション
- アニメーションとモーションのウィジェット(ウィジェット カタログ)
または、Medium の以下の記事をご覧ください。
- アニメーションの詳細
- Flutter のカスタム暗黙的アニメーション
- Flutter と Flux / Redux を使用したアニメーション管理
- 自分に合った Flutter アニメーション ウィジェットの選び方
- 組み込みの明示的なアニメーションによる方向性のあるアニメーション
- 暗黙的なアニメーションを使用した Flutter アニメーションの基本
- AnimatedBuilder と AnimatedWidget はどのような場合に使用すべきですか?