mcpField
A
animations

animations

github.com/verygoodopensourceデザイン・UI175106 回インストール3 日前に更新
リポジトリ

animations について

Flutter 開発者向けに、組み込みアニメーション API と Material 3 のモーション指針に沿った実装・レビューを支援するスキルです。必要十分な暗黙的アニメーションを選び、レイアウト負荷の高い手法を避けます。

主な機能

  1. 01暗黙的・明示的アニメーションの選択
  2. 02Material 3 の時間とイージングトークン
  3. 03コントローラーのライフサイクルとTicker指針
  4. 04スタッガーとページ遷移のパターン
  5. 05アニメーションの性能とアンチパターン確認

活用シーン

  1. 01目標値に応じたUI遷移の実装
  2. 02順次再生・反復アニメーションの構築
  3. 03Flutterアニメーションコードのレビュー
  4. 04GoRouterのページ遷移追加

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/verygoodopensource/vgv-ai-flutter-plugin --skill animations

インストール後、Claude Code で/animationsを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

agents/
openai.yaml
references/
explicit-animations.md
implicit-animations.md
looping-animations.md
page-transitions.md
staggered-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 版で回答し、理由を一行で伝え、コントローラーのコードは書かないでください。依頼に応じたコードに、よりシンプルな形式を勧める注記を添えただけでも、ボイラープレートはそのまま残ります。


追加リソース

コメント