この Codelab について
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(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
クラスには星形アイコンのデフォルト実装はありません。
代わりに、ImplicitlyAnimatedWidget
の別のサブクラスである TweenAnimationBuilder
を使用します。これは、Tween
をパラメータとして受け取ります。トゥイーンとは、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
パラメータの値に基づいて変化します。これは、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
を使用して、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 を使用して遅延を追加する
このセクションでは、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
を使用して遅延を適用する新しい Animation
を作成します。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>([ // 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 つの画面でエフェクトをカスタマイズする方法について説明してきましたが、アニメーションを使用するもう 1 つの方法として、画面間の遷移にアニメーションを使用する方法があります。このセクションでは、組み込みのアニメーション効果と、pub.dev の公式の アニメーション パッケージで提供される魅力的なビルド済みアニメーション効果を使用して、画面遷移にアニメーション効果を適用する方法を学習します。
ナビゲーション遷移をアニメーション化する
PageRouteBuilder
クラスは、遷移アニメーションをカスタマイズできる Route
です。これにより、transitionBuilder
コールバックをオーバーライドできます。このコールバックは、Navigator によって実行される受信アニメーションと送信アニメーションを表す 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( // 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'),
),
animations パッケージには、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 は、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 にあります。
- アニメーションの概要
- アニメーション チュートリアル(チュートリアル)
- 暗黙的アニメーション(チュートリアル)
- コンテナのプロパティをアニメーション化する(cookbook)
- ウィジェットのフェードインとフェードアウト(クックブック)
- ヒーロー アニメーション
- ページルートの遷移をアニメーション化する(クックブック)
- 物理シミュレーションを使用してウィジェットをアニメーション化する(クックブック)
- アニメーションのずらし
- アニメーションとモーションのウィジェット(ウィジェット カタログ)
Medium の記事もご覧ください。
- アニメーションの詳細
- Flutter のカスタム暗黙的アニメーション
- Flutter と Flux / Redux によるアニメーション管理
- 適切な Flutter アニメーション ウィジェットを選択する方法
- 明示的なアニメーションが組み込まれた方向アニメーション
- 暗黙的アニメーションを使用した Flutter アニメーションの基本
- AnimatedBuilder と AnimatedWidget はどのような場合に使用すればよいですか?