1. はじめに
アニメーションは、アプリのユーザー エクスペリエンスを向上させ、重要な情報をユーザーに伝え、アプリをより洗練されたものにし、使いやすくするための優れた方法です。
Flutter のアニメーション フレームワークの概要
Flutter は、各フレームでウィジェット ツリーの一部を再ビルドすることでアニメーション エフェクトを表示します。事前構築済みのアニメーション効果やその他の API が用意されており、アニメーションの作成と合成が容易になります。
- 暗黙的アニメーションは、アニメーション全体を自動的に実行する事前ビルドされたアニメーション エフェクトです。アニメーションのターゲット値が変更されると、現在の値からターゲット値へのアニメーションが実行され、その間の各値が表示されるため、ウィジェットがスムーズにアニメーション化されます。暗黙的なアニメーションの例としては、
AnimatedSize
、AnimatedScale
、AnimatedPositioned
などがあります。 - 明示的アニメーションも事前ビルドされたアニメーション エフェクトですが、動作させるには
Animation
オブジェクトが必要です。例:SizeTransition
、ScaleTransition
、PositionedTransition
など。 - Animation は、実行中または停止中のアニメーションを表すクラスです。アニメーションが実行するターゲット値を表す値と、アニメーションが特定の時点で画面に表示する現在の値を表すステータスで構成されています。これは
Listenable
のサブクラスであり、アニメーションの実行中にステータスが変更されたときにリスナーに通知します。 - AnimationController は、アニメーションを作成してその状態を制御する方法です。
forward()
、reset()
、stop()
、repeat()
などのメソッドを使用して、表示されるアニメーション効果(スケール、サイズ、位置など)を定義しなくてもアニメーションを制御できます。 - Tween は、開始値と終了値の間の値を補間するために使用され、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 が正しく構成され、上記のターゲット プラットフォームの 1 つ以上がインストールされていることを確認するには、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 - マテリアル 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
),
],
),
),
);
}
}
このウィジェットには、質問ごとに星形アイコンが表示されます。質問に正解すると、アニメーションなしですぐに別の星が光ります。次のステップでは、スコアのサイズと色をアニメーション化して、スコアが変更されたことをユーザーに通知します。
暗黙的なアニメーション効果を使用する
AnimatedStar
という新しいウィジェットを作成します。このウィジェットは、AnimatedScale
ウィジェットを使用して、星が有効になったときに scale
の値を 0.5
から 1.0
に変更します。
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( // NEW
isActive: score > i, // NEW
) // NEW
],
),
);
}
}
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
クラスには星形アイコンのデフォルト実装はありません。
代わりに、ImplicitlyAnimatedWidget
の別のサブクラスである TweenAnimationBuilder
を使用します。これは、Tween
をパラメータとして受け取ります。トゥイーンとは、2 つの値(begin
と end
)を受け取り、アニメーションで表示できるように中間値を計算するクラスです。この例では、アニメーション エフェクトの作成に必要な Tween<Color>
インターフェースを満たす 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, // Modify from here...
);
}, // To here.
),
);
}
}
アプリをホットリロードして、新しいアニメーションを確認します。
ColorTween
の end
値は、isActive
パラメータの値に基づいて変化します。これは、TweenAnimationBuilder
が Tween.end
値が変更されるたびにアニメーションを再実行するためです。この場合、新しいアニメーションは現在のアニメーション値から新しい終了値まで実行されます。これにより、(アニメーションの実行中でも)いつでも色を変更し、正しい中間値でスムーズなアニメーション効果を表示できます。
曲線を適用する
どちらのアニメーション効果も一定の速度で実行されますが、アニメーションは速度を上げたり下げたりすることで、視覚的に興味深く、有益なものになることがあります。
Curve
は、時間の経過に伴うパラメータの変化率を定義する減衰関数を適用します。Flutter には、easeIn
や easeOut
など、Curves
クラスに事前ビルドされたイージング カーブのコレクションが付属しています。
これらの図(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 のデバッグ ツールバーで [Widget Inspector] を選択するか、IntelliJ / Android Studio のデバッグ ツール ウィンドウで [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>
を受け取ります。
Tween には、任意の Animation
を Tween が適用された別の Animation
に変換するヘルパー関数 animate()
があります。つまり、Tween<Offset>
を使用して、AnimatedSwitcher
から提供された Animation<double>
を Animation<Offset>
に変換し、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<double>
を、x 軸で -0.1 ~ 0.0 に遷移する Tween<Offset>
に変換します。
または、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 オブジェクトを受け取るアニメーション効果です(目標値と時間の長さを受け取る ImplicitlyAnimatedWidgets とは対照的です)。
- Animation クラスは実行中のアニメーションを表しますが、特定の効果を定義しません。
- Tween().animate または Animation.drive() を使用して、トゥイーンとカーブ(CurveTween を使用)をアニメーションに適用します。
- AnimatedSwitcher の layoutBuilder パラメータを使用して、子のレイアウト方法を調整します。
6. アニメーションの状態を制御する
これまで、すべてのアニメーションはフレームワークによって自動的に実行されていました。暗黙的なアニメーションは自動的に実行されますが、明示的なアニメーション エフェクトが正しく動作するには、アニメーションが必要です。このセクションでは、AnimationController を使用して独自の Animation オブジェクトを作成し、TweenSequence を使用して Tween を組み合わせる方法について説明します。
AnimationController を使用してアニメーションを実行する
AnimationController を使用してアニメーションを作成する手順は次のとおりです。
- StatefulWidget を作成する
- State クラスで SingleTickerProviderStateMixin ミキシンを使用して、AnimationController に Ticker を提供する
- initState ライフサイクル メソッドで AnimationController を初期化し、現在の State オブジェクトを
vsync
(TickerProvider)パラメータに渡します。 - AnimatedBuilder を使用するか、listen() と setState を手動で呼び出して、AnimationController がリスナーに通知するたびにウィジェットが再ビルドされるようにします。
新しいファイル 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 ウィジェットでラップします。カード ウィジェットに 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 を使用して遅延を追加する
このセクションでは、CardFlipEffect ウィジェットに遅延を追加して、各カードが 1 枚ずつめくられるようにします。まず、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
を使用して遅延を適用する新しいアニメーションを作成します。Future.delayed
など、dart:async
ライブラリのユーティリティは使用されません。これは、遅延がアニメーションの一部であり、ウィジェットが AnimationController を使用するときに明示的に制御するものではないためです。これにより、同じ TickerProvider を使用するため、DevTools で遅いアニメーションを有効にしたときにアニメーション効果を簡単にデバッグできるようになります。
TweenSequence
を使用するには、2 つの TweenSequenceItem
を作成します。1 つは、アニメーションを相対的な時間だけ 0 に保つ ConstantTween
と、0.0
から 1.0
に移動する通常の Tween
を含む TweenSequenceItem
です。
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>([ // NEW
if (widget.delayAmount > 0) // NEW
TweenSequenceItem( // NEW
tween: ConstantTween<double>(0.0), // NEW
weight: widget.delayAmount, // NEW
), // NEW
TweenSequenceItem( // NEW
tween: Tween(begin: 0.0, end: 1.0), // NEW
weight: 1.0, // NEW
), // NEW
]).animate(_animationController); // NEW
}
最後に、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 つの画面でエフェクトをカスタマイズする方法について説明してきましたが、アニメーションを使用するもう 1 つの方法として、画面間の遷移にアニメーションを使用する方法があります。このセクションでは、組み込みのアニメーション エフェクトと、pub.dev の公式の アニメーション パッケージで提供される魅力的なビルド済みアニメーション エフェクトを使用して、画面遷移にアニメーション エフェクトを適用する方法について説明します。
ナビゲーション遷移をアニメーション化する
PageRouteBuilder
クラスは、遷移アニメーションをカスタマイズできる Route
です。これにより、transitionBuilder
コールバックをオーバーライドできます。このコールバックは、ナビゲータによって実行される受信アニメーションと送信アニメーションを表す 2 つの Animation オブジェクトを提供します。
遷移アニメーションをカスタマイズするには、MaterialPageRoute
を PageRouteBuilder
に置き換えます。ユーザーが HomeScreen
から QuestionScreen
に移動する際の遷移アニメーションをカスタマイズするには、FadeTransition(明示的にアニメーション化されたウィジェット)を使用して、新しい画面を前の画面の上にフェードインします。
lib/home_screen.dart
ElevatedButton(
onPressed: () {
// Show the question screen to start the game
Navigator.push(
context,
PageRouteBuilder( // NEW
pageBuilder: (context, animation, secondaryAnimation) { // NEW
return QuestionScreen(); // NEW
}, // NEW
transitionsBuilder: // NEW
(context, animation, secondaryAnimation, child) { // NEW
return FadeTransition( // NEW
opacity: animation, // NEW
child: child, // NEW
); // NEW
}, // NEW
), // NEW
);
},
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( // NEW
animation: animation, // NEW
secondaryAnimation: secondaryAnimation, // NEW
child: child, // NEW
); // NEW
},
),
);
},
child: Text('New Game'),
),
予測型「戻る」アニメーションをカスタマイズする
予測型「戻る」は、現在のルートやアプリの背後を覗き込んで、ナビゲートする前に背後にあるものを確認できる Android の新しい機能です。ピーク アニメーションは、ユーザーが画面をドラッグして戻す際の指の位置によって決まります。
Flutter は、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),
useMaterial3: true,
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(builder: (context) { // NEW
return const QuestionScreen(); // NEW
}), // NEW
);
},
child: Text('New Game'),
),
FadeThroughTransition を使用して現在の質問を変更する
AnimatedSwitcher ウィジェットは、ビルダー コールバックで 1 つのアニメーションのみを提供します。この問題に対処するため、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( // NEW
layoutBuilder: (entries) { // NEW
return Stack( // NEW
alignment: Alignment.topCenter, // NEW
children: entries, // NEW
); // NEW
}, // NEW
transitionBuilder: (child, animation, secondaryAnimation) { // NEW
return FadeThroughTransition( // NEW
animation: animation, // NEW
secondaryAnimation: secondaryAnimation, // NEW
child: child, // NEW
); // NEW
}, // NEW
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 をアニメーション パッケージの 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 の使用方法
- アニメーションにカーブとトゥイーンを適用する方法
- AnimatedSwitcher や PageRouteBuilder などの事前構築済みの遷移ウィジェットの使用方法
animations
パッケージの FadeThroughTransition や OpenContainer などの、事前ビルドされた凝ったアニメーション効果を使用する方法- Android の予測型「戻る」のサポートを追加するなど、デフォルトの遷移アニメーションをカスタマイズする方法。
次のステップ
以下の Codelab をご覧ください。
- マテリアル 3 でアニメーション化されたレスポンシブ アプリ レイアウトを作成する
- Flutter 用マテリアル モーションで美しい遷移を作成する
- Flutter アプリを「退屈なアプリ」から「見栄えの良いアプリ」に変える
または、さまざまなアニメーション手法を紹介するアニメーション サンプルアプリをダウンロードしてください。
参考資料
アニメーションに関するその他のリソースについては、flutter.dev をご覧ください。
- アニメーションの概要
- アニメーション チュートリアル(チュートリアル)
- 暗黙的アニメーション(チュートリアル)
- コンテナのプロパティをアニメーション化する(cookbook)
- ウィジェットのフェードインとフェードアウト(クックブック)
- ヒーロー アニメーション
- ページルートの遷移をアニメーション化する(クックブック)
- 物理シミュレーションを使用してウィジェットをアニメーション化する(クックブック)
- アニメーションのずらし
- アニメーションとモーションのウィジェット(ウィジェット カタログ)
Medium の記事もご覧ください。
- アニメーションの詳細
- Flutter のカスタム暗黙的アニメーション
- Flutter と Flux / Redux によるアニメーション管理
- 最適な Flutter アニメーション ウィジェットを選択する方法
- 明示的なアニメーションが組み込まれた方向アニメーション
- 暗黙的アニメーションを使用した Flutter アニメーションの基本
- AnimatedBuilder と AnimatedWidget はどのような場合に使用すればよいですか?