animations について
Flutter 開発者向けに、組み込みアニメーション API と Material 3 のモーション指針に沿った実装・レビューを支援するスキルです。必要十分な暗黙的アニメーションを選び、レイアウト負荷の高い手法を避けます。
主な機能
- 01暗黙的・明示的アニメーションの選択
- 02Material 3 の時間とイージングトークン
- 03コントローラーのライフサイクルとTicker指針
- 04スタッガーとページ遷移のパターン
- 05アニメーションの性能とアンチパターン確認
活用シーン
- 01目標値に応じたUI遷移の実装
- 02順次再生・反復アニメーションの構築
- 03Flutterアニメーションコードのレビュー
- 04GoRouterのページ遷移追加
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ verygoodopensource/ vgv-ai-flutter-plugin --skill animationsインストール後、Claude Code で/animationsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
agents/openai.yamlreferences/explicit-animations.mdimplicit-animations.mdlooping-animations.mdpage-transitions.mdstaggered-animations.md詳細
アニメーション
組み込みのアニメーションフレームワークと Material 3 のモーションガイドラインに基づく、Flutter アニメーションのベストプラクティスです。サードパーティ製のアニメーションライブラリ(Lottie、Rive など)は使用しません。
コアスタンダード
すべてのアニメーション作業に、以下のスタンダードを適用します。
- 依頼が曖昧な場合は、視覚的な意図を確認する — 開発者がプロパティ、トリガー、期間、カーブを指定せずに「アニメーションを追加して」や「もっと滑らかにして」と言った場合は、コードを書く前に確認します。(例:「カードが表示されたときに、300ms のイーズインでフェード」など)仕様が明確であれば、そのまま進めます
- 機能する範囲で最もシンプルなアニメーション方法を使う — 以下の判断ツリーに従います。複数のプロパティを同時にアニメーションさせる場合も含め、暗黙的アニメーションで十分なときに
AnimationControllerを使うことは避けます - コントローラーを名指しで求められても、暗黙的な形式を維持する — 単純な目標値へのアニメーションに対して「
AnimationControllerとAnimatedBuilderで実装して」という依頼は、以下のアンチパターンに該当します。暗黙的な形式で実装し、このケースでそれが十分な理由を一文で述べて、その時点で完了します。注記と併せてコントローラーを使った実装を提示してはいけません。また、どちらを希望するか尋ねるのではなく、コードを書きます。理由を読んだあとで開発者がコントローラーを再度求めた場合は、コントローラーを使って実装します - 期間とイージングには Material 3 のモーショントークンを使う — 任意の
DurationやCurveの値をハードコードしてはいけません - アニメーション定数を切り出す — 期間、カーブ、オフセットはインラインにせず、名前付き定数または一元管理する
AppMotionクラスに定義します - コントローラーを破棄する — すべての
AnimationControllerは、Stateのdispose()メソッド内でsuper.dispose()の前に破棄する必要があります - コントローラーが 1 つの場合は
SingleTickerProviderStateMixinを使う —TickerProviderStateMixinを使うのは、ウィジェットが複数のコントローラーを所有する場合に限ります - アニメーション対象のサブツリーを小さく保つ — アニメーションビルダーの内側には、ウィジェットツリー全体ではなく、変化するウィジェットだけを配置します
- レイアウトを発生させるプロパティはアニメーションさせない —
width、height、padding、SizedBoxの寸法は、階層が深いかどうかにかかわらず、子が 1 つのツリーでも各フレームで新たなレイアウトパスを強制します。代わりにTransformをアニメーションさせます。サイズにはTransform.scale、位置にはTransform.translate、フェードにはOpacityを使います。これらはコンポジットレイヤー上で動作し、レイアウトをスキップします - Dart 3.13 のプライマリーコンストラクター — Dart 3.13 以降を対象とする場合は、
this.fieldではなく、ウィジェットのフィールドを宣言するパラメーターとしてプライマリーコンストラクターを使います(class const LabelChip({required final String label, super.key}) extends StatelessWidget)。3.13 未満の場合のみ、従来の形式を使います
アニメーションの判断ツリー
要件を満たす、最もシンプルな方法を選びます。
Does the widget rebuild when the value changes?
|
YES --> Does the framework provide an AnimatedFoo widget?
| |
| YES --> Use the implicit AnimatedFoo widget
| | (AnimatedContainer, AnimatedOpacity, AnimatedAlign, etc.)
| |
| NO --> Use TweenAnimationBuilder
|
NO --> Do you need fine-grained control?
(repeat, reverse, sequence, listen to status)
|
YES --> Use AnimationController + AnimatedBuilder
|
NO --> Use TweenAnimationBuilder
経験則: アニメーションが「目標値を設定し、そこまでアニメーションさせる」ものであれば、暗黙的アニメーションを使います。 指示に応じて再生、一時停止、逆再生、または繰り返しを行う必要がある場合は、明示的アニメーションを使います。
2 つのプロパティを同時にアニメーションさせる場合も、暗黙的アニメーションです。 データが届いたときにカードをフェードイン し、さらに 上方向にスライドさせる場合は、目標値をそれぞれ 1 つ設定した 2 つの暗黙的ウィジェットを入れ子にします。同時に開始することと、順番に実行することは異なります。2 つ目のプロパティを 1 つ目の開始 後に アニメーションさせる必要がある場合、またはアニメーションの再生を制御する必要がある場合に限り、コントローラーを使います。フラグの切り替え(値の到着、bool の切り替え、項目の表示)によって開始するエントリーアニメーションは、何個のプロパティを動かす場合でも暗黙的アニメーションです。
Material 3 のモーショントークン
Flutter 組み込みの Durations クラスと Easing クラスを使います。新しいコードでは、Duration(milliseconds: ...) をハードコードしたり、Curves.* を使ったりしてはいけません。フレームワークの定数は Material 3 のモーション仕様に準拠しています。
// Bad — arbitrary values with no semantic meaning
AnimatedContainer(
duration: Duration(milliseconds: 375),
curve: Curves.easeInOutCubic,
)
// Good — M3 tokens with clear intent
AnimatedContainer(
duration: Durations.medium2,
curve: Easing.standard,
)
モーション定数の一元管理
プロジェクト内の複数の機能でアニメーションを使う場合は、AppMotion クラスを導入します。アプリ内のアニメーションが 1 つだけの場合は、M3 トークンをインラインで使えば十分です。
abstract class AppMotion {
// Standard transitions
static const Duration standardDuration = Durations.medium2;
static const Curve standardCurve = Easing.standard;
// Page transitions
static const Duration pageDuration = Durations.medium4;
static const Curve pageEnterCurve = Easing.emphasizedDecelerate;
static const Curve pageExitCurve = Easing.emphasizedAccelerate;
// Fades
static const Duration fadeDuration = Durations.short3;
static const Curve fadeCurve = Easing.standard;
}
暗黙的なアニメーション
ウィジェットが新しい目標値で再ビルドされる場合は、暗黙的なアニメーションを使用します。フレームワークが自動的に補間します。Flutter には組み込みの AnimatedFoo ウィジェット(AnimatedContainer、AnimatedOpacity、AnimatedSlide、AnimatedSwitcher など)が用意されています。アニメーションさせるプロパティに合ったものを使用してください。組み込みウィジェットがない場合は、TweenAnimationBuilder を使用します。
複数のプロパティが同時に動く場合は、プロパティごとに AnimatedFoo を組み合わせます。以下はエントリーアニメーションの形です。データが到着するまで非表示にし、その後フェードインしながら所定の位置にスライドします。
class const SummaryCard({required final Summary? summary, super.key})
extends StatelessWidget {
@override
Widget build(BuildContext context) {
final hasData = summary != null;
return AnimatedOpacity(
opacity: hasData ? 1 : 0,
duration: Durations.medium2,
curve: Easing.standard,
child: AnimatedSlide(
offset: hasData ? Offset.zero : const Offset(0, 0.1),
duration: Durations.medium2,
curve: Easing.emphasizedDecelerate,
child: Card(child: _SummaryContents(summary: summary)),
),
);
}
}
StatefulWidget も、コントローラーも、ティッカーも、dispose も必要ありません。どちらのプロパティも、同じ再ビルドをきっかけにアニメーションします。
TweenAnimationBuilder
プロパティに対応する組み込みの AnimatedFoo ウィジェットがなく、暗黙的な「設定したらあとはおまかせ」のアニメーションを使いたい場合は、TweenAnimationBuilder を使用します。
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: isActive ? 1.0 : 0.0),
duration: Durations.medium2,
curve: Easing.standard,
builder: (context, value, child) {
return Transform.scale(
scale: 0.8 + (0.2 * value),
child: Opacity(
opacity: value,
child: child,
),
);
},
child: child, // child is not rebuilt — optimization
)
child パラメーターは重要です。不要な再ビルドを避けるため、アニメーション値に依存しないウィジェットを渡してください。
明示的なアニメーション
再生、一時停止、逆再生、繰り返し、アニメーションの状態の監視など、再生を制御する必要がある場合は、明示的なアニメーションを使用します。
AnimationController の設定
class _MyWidgetState extends State<MyWidget>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Durations.medium2,
vsync: this,
);
_fadeAnimation = CurvedAnimation(
parent: _controller,
curve: Easing.standard,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _fadeAnimation,
builder: (context, child) {
return Opacity(
opacity: _fadeAnimation.value,
child: child,
);
},
child: child, // static child — not rebuilt each frame
);
}
}
didUpdateWidget のパターン、テスト可能なコントローラーのコンストラクターインジェクション、トランジションウィジェットと AnimatedBuilder の使い分けについては、references/explicit-animations.md を参照してください。
Interval を使った段階的なアニメーション
単一のコントローラー上でアニメーションを段階的にずらすには、CurvedAnimation 内で Interval を使用します。これにより、スライドはフェードと同時に始まるのではなく、フェードの途中から始まります。Interval の範囲を重ねることが、このパターンの要点です。
目標値に向けて同時にアニメーションするプロパティには、この方法を使いません。同じ再ビルドをきっかけにフェードとスライドの両方を実行する場合は、2 つの暗黙的なウィジェットを使います。2 つの Interval を持つコントローラーではありません。
late final Animation<double> _fadeAnimation = CurvedAnimation(
parent: _controller,
curve: const Interval(0.0, 0.5, curve: Easing.standard),
);
late final Animation<Offset> _slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.25),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.2, 0.8, curve: Easing.emphasized),
),
);
段階的なエントリーと段階的なリストの完全な例については、references/staggered-animations.md を参照してください。繰り返しアニメーションとパルスアニメーションのパターンについては、references/looping-animations.md を参照してください。
ページ遷移
カスタムページ遷移は、GoRouteData.buildPage 内の CustomTransitionPage を介して GoRouter に組み込めます。複数のルートで遷移が必要になったら、共通の AppPageTransitions ヘルパーに切り出してください。同じ transitionsBuilder をルートごとにインラインで記述してはいけません。ヘルパー、GoRouter との接続、および Hero による共有要素の遷移については、references/page-transitions.md を参照してください。
パフォーマンス
すること
TransformとOpacityをアニメーションさせる — これらは合成レイヤー上で動作し、レイアウトと描画をスキップしますAnimatedBuilderとTweenAnimationBuilderのchildパラメーターを使う — 静的なウィジェットが毎フレーム再ビルドされるのを防ぎます- 複雑なレイアウトでは、アニメーションするウィジェットを
RepaintBoundaryで囲む — 再描画を分離します
しないこと
- 画面全体を
AnimatedBuilderで囲まない — 変更されるサブツリーだけを囲みます - タイミングを共有するアニメーションに、複数の
AnimationControllerインスタンスを作成しない — 1 つのコントローラーでIntervalを使用します
アンチパターン
Transform の代わりに幅をアニメーションさせる
// Bad — every frame re-runs layout on the SizedBox and everything under it
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return SizedBox(
width: 200 + (_controller.value * 120),
child: child,
);
},
child: const ExpensiveChart(),
)
// Good — Transform.scale runs on the compositing layer, no layout pass
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.scale(
scaleX: 1 + (_controller.value * 0.6),
child: child,
);
},
child: const ExpensiveChart(),
)
レビュー時には、これを名前を挙げて指摘してください。アニメーションする width や height は、毎フレームレイアウトパスを発生させます。修正には Transform.scale または Transform.translate を使用します。
静的な子ウィジェットを毎フレーム再構築する
// Bad — entire subtree rebuilds 60 times/second
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Opacity(
opacity: _controller.value,
child: const ExpensiveWidget(), // rebuilt every frame
);
},
)
暗黙的アニメーションで十分なのに、明示的アニメーションを使っています。通常はすでに誤った答えの形で依頼が届くため、特に注意が必要なケースです。
// Bad — unnecessary complexity for a simple target-value animation
class _FadeWidgetState extends State<FadeWidget>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
// ... 20+ lines of boilerplate
// Good — one widget, zero boilerplate
AnimatedOpacity(
duration: Durations.short3,
curve: Easing.standard,
opacity: isVisible ? 1.0 : 0.0,
child: child,
)
「適切に設定するため、StatefulWidget 内で AnimationController と AnimatedBuilder を使ってください」――bool 値で制御するフェードアニメーションであれば、これは上記の悪い形式をそのまま依頼文にしたものです。AnimatedOpacity 版で回答し、理由を一行で伝え、コントローラーのコードは書かないでください。依頼に応じたコードに、よりシンプルな形式を勧める注記を添えただけでも、ボイラープレートはそのまま残ります。
追加リソース
- references/implicit-animations.md —
AnimatedFooウィジェット、複数のプロパティの組み合わせ、TweenAnimationBuilder - references/explicit-animations.md — コントローラーの設定、
Intervalによる段階的なアニメーション、didUpdateWidget、テスト可能なコントローラー、トランジションウィジェットとAnimatedBuilderの比較 - references/staggered-animations.md — 段階的なエントリーアニメーションと、リスト項目の段階的なアニメーション
- references/page-transitions.md — 再利用可能な
AppPageTransitionsヘルパー、GoRouter の統合、Heroによる共有要素トランジション - references/looping-animations.md — 繰り返し、脈動、連続回転のパターン