2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Flutter animation

2
Last updated at Posted at 2025-10-31

はじめに

Flutter の公式サイトを中心にアニメーションの基礎を理解したい。

Flutter のソースコードは画面右上のアイコンをクリックすることで見ることができる。

Screenshot 2025-10-19 at 8.25.12.png

アニメーションとは

アニメーションは Frame と呼ばれる描画処理を高速に繰り返すことによって実現する。

パラパラ漫画で言う、ひとコマひとコマが Frame であり、一般的に 1 秒間に 60 回 Frame が更新される 60 fps(frame per second)がヌルヌル動くと言われる。

image.png
https://cameragurus.com/30fps-vs-60fps/

image.png
https://32labo.com/framerate/

Flutter には以下の 2 種類のアニメーション対象が存在する。

  • Code-based animation
  • Drawing-based animations

アニメーションのさせ方は以下の 2 種類に分類される。

  • Tween animation
  • Physics-based animation

Code-based animation

Widget のプロパティを変化させることで実現するアニメーション。

※ この記事のほとんどは、Code-based animation に関わるものです。

Drawing-based animations

ゲームのキャラクターのような Canvas に絵を描くようにして描画された対象物を、Frame の度に手動で変化を加えてアニメーションさせるもの。

既存の Widget(Row、Column、textStyle)で表現できない

  • ベクター画像(数式、直線、曲線で表現された画像)
  • ラスター画像(ピクセルの集合で表現された画像)

を使用するものに対して適用する。

  • Rive や Lottie などのサードパーティ製ツールが充実している
  • CustomPaint を使用する

Tween animation

アニメーションの「始点」と「終点」を定義しておき、その間を補間させることによって実現するアニメーション。

Tween は Between に由来している。

※ この記事のほとんどは、Tween animation に関わるものです。

Physics-based animation

物理法則を模倣したモデルによって実現するアニメーション。

参考: Animate a widget using a physics simulation

Flutter のアニメーション

Code-based animation は以下の 2 つに大別される。

  • Implicit Animation(暗黙的アニメーション)
  • Explicit Animation(明示的アニメーション)

実現したいアニメーションが単純であれば Implicit Animation を使う。

難しいアニメーションの場合には Explicit Animation を使用する必要が出てくる。

「難しい」とは、アニメーションが以下の特性を持つ場合を指す。

  • 永続的
  • 連続性
  • 複数の Widget が連携してアニメーションする

Implicit Animation

color などの特定のプロパティを A → B に変更し、setState() を呼ぶだけで実現するアニメーション。

状態 A と状態 B の間の中間の状態は Implicit Animation が内部で生成してくれるので、「始点」と「終点」を定義するだけで良い。

Implicit Animation
AnimatedContainer(
  duration: const Duration(seconds: 2),
  color: isRed ? Colors.red : Colors.blue,  // 👈
)

Explicit Animation

AnimationController を必要とするアニメーション。

AnimationController は forward() / reverse() / stop() など多彩なアニメーションに対するコントローラ機能を持つ。

また AnimationController は Widget ではないため、wrap して使うというよりは、StatefulWidget 内に配置して、オブジェクトの生成、破棄などのライフサイクルを管理する必要がある。

Explicit Animation
class ExplicitAnimation extends StatefulWidget {
  const ExplicitAnimation({super.key});

  @override
  State<ExplicitAnimation> createState() => _ExplicitAnimationState();
}

class _ExplicitAnimationState extends State<ExplicitAnimation> with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this); // 👈 生成
  }

  @override
  void dispose() {
    _controller.dispose(); // 👈 破棄
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return const XXX();
  }
}

Explicit Animation において AnimationController は必須の存在であり、この処理は避けることができない。

このため Explicit Animation のコードは Implicit Animation よりも量や複雑度が増す傾向にある。

Implicit vs Explicit

Animation AnimationController コード量 難易度 できること
Implicit Animation 不要 少ない 低い 少ない
Explicit Animation 必要 多い 高い 多い

Animation Widget の選び方

Implicit / Explicit のどちらを使用すべきかについては、「実現したいこと」に対して適切な選択をする必要がある。

複雑なアニメーションにしようとすればするほど、より低レベルな Flutter ライブラリを使用する必要性がある。

image.png
https://blog.flutter.dev/how-to-choose-which-flutter-animation-widget-is-right-for-you-79ecfb7e72b5

難易度

難易度
簡単 < Implicit < Explicit < CustomPainter < 難しい

image.png
https://blog.flutter.dev/how-to-choose-which-flutter-animation-widget-is-right-for-you-79ecfb7e72b5

暗黙的 / 明示的アニメーションとは

Implicit Animation

Implicit Animation は以下の 2 種類に分類される。

  • Built-in
    • Flutter SDK に含まれる Widget
    • AnimatedFoo
    • AnimatedContainer
    • AnimatedOpacity
    • ...
  • Custom
    • TweenAnimationBuilder を使用して自前で作成する Widget

Built-in から目的の Widget が探せなかった場合、Custom Widget を作成する必要がある。

Explicit Animation

Explicit Animation は以下の 2 種類に分類される。

  • Built-in
    • Flutter SDK に含まれる Widget
    • FooTransition
    • SizeTransition
    • SlideTransition
    • ...
  • Custom
    • AnimatedBuilder を使用して自前で作成する Widget
    • AnimatedWidget のサブクラスとして自前で作成する Widget

Implicit vs Explicit

Implicit Animation を使用するか、Explicit Animation を使用するかの判断基準。

下記のいずれかの必要性がある場合、Explicit Animation が必要。

  • 永続的 か
  • 連続性 があるか
    • 開いて閉じる、開いて閉じる → 連続性あり(全開状態から徐々に未開状態に遷移)
    • 「開く」を繰り返す → 連続性なし(全開状態から未開状態に状態がジャンプするため)
  • 複数の Widget が協調 するか

ただし、Implicit Animation でできる全てのことは、Explicit Animation で実現可能。

上記 3 つのいずれにも該当しないアニメーションは、 Flutter 公式では Basic Animaiton と表現される。

Basic Animation は Implicit Animation で実現することができる。

アニメーションを実装する(導入)

Built-in Implicit Animation

Flutter には Widget 名の先頭が Animated で始まる Built-in Implicit Animation がたくさん用意されている。

これらクラスは全て ImplicityAnimatedWidget を継承したクラス群となっている。

Custom Explicit Animation

Explicit Animation を理解するためには Animation<T> への理解が必須。

Animation<T> (abstract class)

image.png

UI に T value と AnimationStatus status を提供する 抽象クラス。

Flutter (1).png

  • value
    • ハードウェアが新たな Frame を用意する度に生成される
      • 60 fps の環境においては、1 秒間に 60 回生成
    • 進行度 t(0.0 ~ 1.0) をインプットにしてアウトプットされる生成値
      • 生成値 value はアウトプット時に数学的関数によって 線形補間 or 非線形補間 される
  • status
    • forward
      • t が 0.0 → 1.0 に進行中
    • reverse
      • t が 1.0 → 0.0 に進行中
    • completed
      • t が 1.0 に到達
    • dismissed
      • t が 0.0 に到達

Animation は UI に value を提供するが、UI(どのように利用、表示されているか)についての知識は持たない。

Flutter.png

型引数 T は double が最もよく利用されるが、それ以外にも Color、Size などのオブジェクトに関しても補間することができる。

Animation<T> は UI に関する知識を持たないクラス

AnimationController

image.png

抽象クラス Animation<double>を継承した具象クラス。

線形補間 された値(value)が生成される。

image.png
https://api.flutter.dev/flutter/animation/Curves/linear-constant.html

デフォルトでは生成値(value)の取りうる範囲は 0.0 ~ 1.0。

「進行度 t」 と「生成値(value)」の範囲が同じ 0.0 ~ 1.0 なので混同しがちだが、両者は異なるものであることに注意。

Flutter (2).png

0.0 ~ 1.0 以外の範囲の値を使用したい場合、Tween を使用する。

AnimationController は加えて アニメーションを開始、停止するためのコントローラ機能も併せ持つ。

  • forward()
  • stop()
  • reverse()
  • repeat()

このコントローラ機能は アニメーションを drive(駆動する、操縦する) と表現されることがある。

CurvedAnimation

image.png

AnimationController 同様に、抽象クラス Animation<double> を継承した具象クラス。

進行度 t を 非線形補間(non-linear curve)によって double value に変換する。

非線形補間においては、時間(t)の進行に対して生成値(value / x)の変化速度が一定でない。

image.png
https://api.flutter.dev/flutter/animation/Curves-class.html

AnimationController と同様に value を提供する機能を持つ一方で、CurvedAnimation は コントローラとしての機能を持たない。

コントローラ機能は外部から parent に注入する必要がある。

CurvedAnimation にコントローラを注入する
final Animation<double> animation = CurvedAnimation(
  parent: controller, // 👈 コントローラ機能を注入
);

デフォルトでは value は 0.0 ~ 1.0 の範囲を持つ。

CurvedAnimation は 0.0 ~ 1.0 を非線形補間している

Animation vs AnimationController vs CurvedAnimation

image.png

AnimationController と CurvedAnimation はどちらも Animation<duble> を継承しているため、型としては互換性を持ち、同じ API(value、addListener()) が共有できるが、コントローラ機能の互換性は無い。

クラス名 継承関係 入力値(t) 出力値(value) 補間 コントローラ機能
Animation<T> 抽象クラス 0.0 ~ 1.0 T value - -
AnimationController 具象クラス 0.0 ~ 1.0 0.0 ~ 1.0 線形補間 あり
CurvedAnimation 具象クラス 0.0 ~ 1.0 0.0 ~ 1.0(デフォルト) 非線形補間 ない

アニメーションのコントローラ機能を有すると言う点で AnimationController はかなり特別な存在で、Explicit Animation において欠かせない存在と言える。

AnimationController はアニメーションを動かす(drive する)唯一の動力源

個人的には AnimationController と CurvedAnimation はどちらも Animation<double> 型の変数に代入できるのに、変数名が animation だったり contorller だったりするのはなぜだろうと疑問に思っていたが、どうやらコントローラ機能の有無の違いによって使い分けられている様子。

CurvedAnimation は必ず親(parent)として AnimationController を必要とする。

AnimationController vs CurvedAnimation
final Animation<double> controller = AnimationController(
  duration: const Duration(seconds: 2),
  vsync: this,
);

final Animation<double> animation = CurvedAnimation(
  parent: controller,
  curve: Curves.ease,
);

アニメーションを実装する(実践)

Built-in Implicit Animation

【実践 1】 基本的なアニメーション

Implicit Animation は AnimatedFoo ウィジェットのプロパティを setState() 内で変更するだけで実現する。

Implicit Animation
class _ImplicitAnimationState extends State<ImplicitAnimation> {
  bool visible = false;
  late Timer timer;

  @override
  void initState() {
    super.initState();
    timer = Timer.periodic(
      const Duration(seconds: 2),
      (Timer timer) => setState(() => visible = !visible),  // 👈
    );
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedOpacity(
      opacity: visible ? 1.0 : 0.0,  // 👈

Screen Recording 2025-10-24 at 23.07.42.gif

ソースコード
Implicit Animation
import 'dart:async';

import 'package:flutter/material.dart';

class ImplicitAnimation extends StatefulWidget {
  const ImplicitAnimation({super.key});

  @override
  State<ImplicitAnimation> createState() => _ImplicitAnimationState();
}

class _ImplicitAnimationState extends State<ImplicitAnimation> {
  bool visible = false;
  late Timer timer;

  @override
  void initState() {
    super.initState();
    timer = Timer.periodic(
      const Duration(seconds: 2),
      (Timer timer) => setState(() {
        visible = !visible;
      }),
    );
  }

  @override
  void dispose() {
    timer.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedOpacity(
      opacity: visible ? 1.0 : 0.0,
      duration: const Duration(seconds: 2),
      child: Container(
        width: 100,
        height: 100,
        color: Colors.red,
      ),
    );
  }
}

Custom Explicit Animation

【実践 1】 基本的なアニメーション

image.png

AnimationController は value が変更された際に実行される listener を addListener() で登録することができる。

この addListener() では setState() を呼び出すことで、value の変更時に再描画を実行させることができる。

Explicit Animation
controller = AnimationController(
  duration: const Duration(seconds: 2),
  vsync: this,
)
..addListener(() => setState(() {}),

同様に status が変更された際の listener も addStatusListener() で登録することができる。

Explicit Animation
..addStatusListener(
  (status) {
    if (status == AnimationStatus.completed) {
      controller.reverse();
    } else if (status == AnimationStatus.dismissed) {
      controller.forward();
    }
  },
)

これによって、最も基本的な Explicit Animation を構築できる。

Screen Recording 2026-01-29 at 21.18.07.gif

ソースコード
Explicit Animation
import 'package:flutter/material.dart';

class ExplicitAnimation extends StatefulWidget {
  const ExplicitAnimation({super.key});

  @override
  State<ExplicitAnimation> createState() => _ExplicitAnimationState();
}

class _ExplicitAnimationState extends State<ExplicitAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    )
      ..addListener(
        () => setState(() {}),
      )
      ..addStatusListener(
        (status) {
          if (status == AnimationStatus.completed) {
            controller.reverse();
          } else if (status == AnimationStatus.dismissed) {
            controller.forward();
          }
        },
      )
      ..forward();
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100 * controller.value,
      height: 100 * controller.value,
      color: Colors.red,
    );
  }
}

Screen Recording 2025-10-26 at 11.32.13.gif

↑ソースコード(ステータス表示)
import 'package:flutter/material.dart';

class ExplicitAnimation extends StatefulWidget {
  const ExplicitAnimation({super.key});

  @override
  State<ExplicitAnimation> createState() => _ExplicitAnimationState();
}

class _ExplicitAnimationState extends State<ExplicitAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      duration: const Duration(seconds: 5),
      vsync: this,
    )
      ..addListener(
        () => setState(() {}),
      )
      ..addStatusListener(
        (status) {
          if (status == AnimationStatus.completed) {
            controller.reverse();
          } else if (status == AnimationStatus.dismissed) {
            controller.forward();
          }
        },
      )
      ..forward();
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    const style = TextStyle(
      fontSize: 40,
    );
    return Stack(
      children: [
        Positioned(
          right: 100,
          top: 100,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.end,
            children: [
              Text(
                'value: ${controller.value.toStringAsFixed(2)}',
                style: style,
              ),
              Text(
                'status: ${controller.status.name}',
                style: style,
              ),
              Text(
                'velocity: ${controller.velocity.toStringAsFixed(2)}',
                style: style,
              ),
            ],
          ),
        ),
        Align(
          alignment: AlignmentGeometry.center,
          child: Container(
            width: 100 * controller.value,
            height: 100 * controller.value,
            color: Colors.red,
          ),
        ),
      ],
    );
  }
}

【実践 2】 non-linear なアニメーション(Curve)

前述の通り、AnimationController は線形補間した value を提供する。

image.png
https://api.flutter.dev/flutter/animation/Curves-class.html

これを CurvedAnimation を用いて 非線形補間(no-linear curve)に変換する。

Explicit Animation
animation = CurvedAnimation(parent: controller, curve: Curves.easeInCirc);

image.png

https://api.flutter.dev/flutter/animation/Curves-class.html

Widget は CurvedAnimation が提供する double value によって描画を行う。

Screen Recording 2025-10-24 at 19.49.41.gif

ソースコード
Explicit Animation
import 'package:flutter/material.dart';

class ExplicitAnimation extends StatefulWidget {
  const ExplicitAnimation({super.key});

  @override
  State<ExplicitAnimation> createState() => _ExplicitAnimationState();
}

class _ExplicitAnimationState extends State<ExplicitAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController controller;
+  late Animation<double> animation;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    )
      ..addListener(
        () => setState(() {}),
      )
      ..addStatusListener(
        (status) {
          if (status == AnimationStatus.completed) {
            controller.reverse();
          } else if (status == AnimationStatus.dismissed) {
            controller.forward();
          }
        },
      );
+    animation = CurvedAnimation(parent: controller, curve: Curves.easeInCirc);
    controller.forward();
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
+      width: 100 * animation.value,
+      height: 100 * animation.value,
-      width: 100 * controller.value,
-      height: 100 * controller.value,
      color: Colors.red,
    );
  }
}

Screen Recording 2025-10-26 at 11.34.58.gif

ソースコード(ステータス表示)
import 'package:flutter/material.dart';

class ExplicitAnimation extends StatefulWidget {
  const ExplicitAnimation({super.key});

  @override
  State<ExplicitAnimation> createState() => _ExplicitAnimationState();
}

class _ExplicitAnimationState extends State<ExplicitAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController controller;
  late Animation<double> animation;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      duration: const Duration(seconds: 5),
      vsync: this,
    )
      ..addListener(
        () => setState(() {}),
      )
      ..addStatusListener(
        (status) {
          if (status == AnimationStatus.completed) {
            controller.reverse();
          } else if (status == AnimationStatus.dismissed) {
            controller.forward();
          }
        },
      );
    animation = CurvedAnimation(parent: controller, curve: Curves.easeInCirc);
    controller.forward();
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    const style = TextStyle(
      fontSize: 40,
    );
    return Stack(
      children: [
        Positioned(
          right: 100,
          top: 100,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.end,
            children: [
              Text(
                't: ${controller.value.toStringAsFixed(2)}',
                style: style,
              ),
              Text(
                'value: ${animation.value.toStringAsFixed(2)}',
                style: style,
              ),
              Text(
                'status: ${controller.status.name}',
                style: style,
              ),
              Text(
                'velocity: ${controller.velocity.toStringAsFixed(2)}',
                style: style,
              ),
            ],
          ),
        ),
        Align(
          alignment: AlignmentGeometry.center,
          child: Container(
            width: 100 * animation.value,
            height: 100 * animation.value,
            color: Colors.red,
          ),
        ),
      ],
    );
  }
}

応用的なアニメーションを実装する

Animatable<T>(abstract class)

image.png

0.0 ~ 1.0 の double 値(進行度 t)を受け取り、実際にアニメーションさせたい型(T)の値(value)を出力する変換器。

変換処理は、内部で evaluate() によって行われる。

Animatable
abstract class Animatable<T> {
  // 0.0〜1.0 の入力を受け取り、対応する値を返す
  T transform(double t);

  // animation.value を使って transform() を呼ぶ
  T evaluate(Animation<double> animation) => transform(animation.value);

  // Animatable を与えられた Animation に接続し、新しい Animation を返す
  Animation<T> animate(Animation<double> parent) {
    return _AnimatedEvaluation<T>(parent, this);
  }
}

class _AnimatedEvaluation<T> extends Animation<T> with AnimationWithParentMixin<double> {
  _AnimatedEvaluation(this.parent, this._evaluatable);

  @override
  final Animation<double> parent;

  final Animatable<T> _evaluatable;

  @override
  T get value => _evaluatable.evaluate(parent);
}

image.png

Tween<T extends Object?>

image.png

AnimationContoroller が double の 0.0 ~ 1.0 までの値を補間するのに対して、Tween は 任意のデータ型 を 任意の範囲 で 線形補間 することができる。

任意のデータ型
final sizeTween = Tween<Size>(begin: const Size(100, 100), end: const Size(200, 200));
final colorTween = Tween<Color>(begin: Colors.red, end: Colors.blue);
final alpha = IntTween(begin: 0, end: 255);
任意の範囲
final doubleTween = Tween<double>(begin: -200, end: 0);

Tween は 0.0 ~ 1.0 の進行度を T begin ~ T end に線形補間している

Tween.evaluate() / Tween.lerp()

Animatable の evaluate() はアニメーションの進行度 t に対応する T value を生成するが、生成時に内部では T transoform() が利用されている。

Animatable
abstract class Animatable<T> {

  T evaluate(Animation<double> animation) => transform(animation.value);

  T transform(double t);
}

T transform(double t) は Animatable では抽象メソッドとなっていて、具体的な処理は Tween で実装される。

Tween は tranform() が呼ばれた時に lerp() を使用して value を生成する。

image.png

実装を見ると、lerp() は T begin ~ T end の間の、進行度 t に応じた value を計算していることが分ける。

lerp は Linear IntERPolation(線形補間)の略語。

lerp()
class Tween<T extends Object?> extends Animatable<T> {

  T lerp(double t) {
    return (begin as dynamic) + ((end as dynamic) - (begin as dynamic)) * t as T;
  }
  
  @override
  T transform(double t) {
    if (t == 0.0) {
      return begin as T;
    }
    if (t == 1.0) {
      return end as T;
    }
    return lerp(t); // 👈 lerp() を使用して線形補間した value を生成する
  }
}

lerp() で実行されているのは以下の計算。

lerp()
T lerp(double t) {
  return (begin as dynamic) + ((end as dynamic) - (begin as dynamic)) * t as T;
}

キャスト部分を取り除いてみる。

lerp()
begin + (end - begin) * t

end - begin は始点と終点の差分。t は進行度 0.0 ~ 1.0。

上記を以下の条件で計算してみる。

  • begin = 100
  • end = 200
  • t = 0.5
lerp() の計算
100 + (200 - 100) + 0.5
= 150

ちゃんと 100 と 200 の間(進行度: 0.5)になっていることがわかった。

begin + (end - begin) * t のうちの

線形補間
(end - begin) * t

が begin と end の差分を進行度 t に応じて計算している箇所になっている。

T に 指定することができるクラスには、通常 static な lerp() が定義されていて、FooTween はこれを内部で利用している(というより、XXXTween は、lerp() の実装を提供するという仕事しかほとんどしていない)。

AlignmentTween
class AlignmentTween extends Tween<Alignment> {
  @override
  Alignment lerp(double t) => Alignment.lerp(begin, end, t)!;
}
BorderRadiusTween
class BorderRadiusTween extends Tween<BorderRadius?> {
  @override
  BorderRadius? lerp(double t) => BorderRadius.lerp(begin, end, t);
}

XXXTween の仕事は線形補間の実体である lerp() 関数を提供すること

Tween は mutable なオブジェクトだが、変更されない場合には static final で定義しておくと再描画の度にインスタンス生成されないので少しパフォーマンス的に良い。

Tween
static final tween = Tween<double>(begin: 0.0, end: 100.0);

Animation vs AnimationController vs CurvedAnimation vs Tween

クラス名 継承関係 入力値(t) 出力値(value) 補間
Animation<T> 抽象クラス 0.0 ~ 1.0 T -
AnimationController 具象クラス 0.0 ~ 1.0 0.0 ~ 1.0 線形補間
CurvedAnimation 具象クラス 0.0 ~ 1.0 0.0 ~ 1.0(※) 非線形補間
Animatable<T> 抽象クラス 0.0 ~ 1.0 T = evaluate() -
Tween<T> 具象クラス 0.0 ~ 1.0 T = evaluate() 線形補間

※ 一部の Curve は範囲外の値をとる

Animatable<T> と Animation<T> は 双方向から接続することができる。

AnimationController から Tween へ接続する
animation = controller.drive(Tween(begin: 0.0, end: 100.0));
CurvedAnimation から Tween へ接続する
controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = CurvedAnimation(parent: controller, curve: Curves.easeInCirc)
              .drive(Tween(begin: 0.0, end: 100.0));
Tween から Animation へ接続する
animation = Tween(begin: 0.0, end: 100.0).animate(controller);
Tween から CurvedAnimation へ接続する
controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
curve = CurvedAnimation(parent: controller, curve: Curves.easeInCirc);
animation = Tween(begin: 0.0, end: 100.0).animate(curve);

image.png

Tween x CurvedAnimation

任意のデータ型 を 任意の範囲 で → Tween の仕事
非線形補完 → CurvedAnimation の仕事

以下のように animation を作ることで、これらを合わせることができる。

Tween x CurvedAnimation
final controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    );
final animation = Tween<double>(
  begin: 0,
  end: 100,
).animate(
  CurvedAnimation(
    parent: controller,
    curve: Curves.easeInCirc,
  ),
);

Curve を理解する

Animation や AnimationController や Tween は線形補間した value を提供する。

Curve は非線形補完した value を提供する。

ParametricCurve<T>(abstract class)

parametric = パラメータの~、パラメータに関する~

0.0 ~ 1.0 の入力値(進捗 t)を受け取り、補間した value を返すためのインターフェース。

image.png

用途はアニメーションに限定されない。

transform() は内部で transformInternal() 使用している。transformInternal() は override されることを前提とした作りになっている。

ParametricCurve
abstract class ParametricCurve<T> {

  T transform(double t) {
    return transformInternal(t);
  }

  T transformInternal(double t) {
    throw UnimplementedError();
  }
}

Curve(abstract class)

image.png

抽象クラス ParametricCurve<double> をさらに継承した抽象クラス。

0.0 ~ 1.0 の進行度 t を transformInterval() によって 非線形補間 する。

transformInternal() は 前述 の通りオーバーライドを前提とした関数なので、カスタムなロジックをここに埋め込めば、オリジナルな非線形補間(Cureve)を作成することができる。

カスタムの Cureve
class MyCurve extends Curve {

  double transform(double t) {
    return transformInternal(t);
  }

  @override
  double transformInternal(double t) =>  カスタムのロジック; // 0.0 <= t <= 1.0
}

CurvedAnimation のコンストラクタにパラメータとして渡して使用される。

Curve
CurvedAnimation(
  parent: controller,
  curve: MyCurve(), // 👈
)

Cubic

cubic = 三次元

image.png

Curve を継承した具象クラス。

4 つの制御点で定義される三次 ベジェ曲線 を使って、滑らかな補間を行う。

DeCasteljau_cubic.gif

  • a : 最初の制御点の x 座標
  • b : 最初の制御点の y 座標
  • c : 2 番目の制御点の x 座標
  • d : 2 番目の制御点の y 座標

Curves で多く利用されている。

Cubic
class Cubic extends Curve {
  const Cubic(this.a, this.b, this.c, this.d);

  final double a, b, c, d;

  @override
  double transformInternal(double t) {
    // ベジェ方程式の逆関数を求めて対応する y 値を返す
  }
}

Threshold

threshold = 閾値

image.png

進行度 t が閾(しきい)値を超えたら 1.0 にジャンプする離散的な Curve。

image.png
https://api.flutter.dev/flutter/animation/Threshold-class.html

Threshold
class Threshold extends Curve {
  const Threshold(this.threshold);

  final double threshold;

  @override
  double transformInternal(double t) {
    return t < threshold ? 0.0 : 1.0;
  }
}

Curves(abstract and utility class)

transformInternal() が生成する非線形補間がたくさん定義されたユーティリィティクラス(Curve のリポジトリ)。

Screenshot 2025-10-19 at 15.19.35.png
https://api.flutter.dev/flutter/animation/Curves-class.html

Interval

image.png

開始点(t = 0.0)からある時点までは 0.0 を返し、終了点(t = 1.0)より前に 1.0 に到達しその後 1.0 を返し続ける Curve。

image.png
https://api.flutter.dev/flutter/animation/Curves-class.html

アニメーションを遅延させたりする際に利用できる。

Curve vs Cubic vs Threshold vs Curves vs Interval

Cubic、Threshold、Interval は全て Curve である。

image.png

CurvedController vs CurvedAnimation vs Curve

AnimationController が保持するアニメーションの進行度 t(0.0 ~ 1.0)は Curve によって非線形補間され、Widget(UI)に伝達される。

image.png

CurvedAnimation
class CurvedAnimation extends Animation<double> with AnimationWithParentMixin<double> {
  CurvedAnimation({
    required this.parent,
    required this.curve,
  });

  final Animation<double> parent;
  Curve curve;
}
animation に controller, curve が渡される
controller = AnimationController(
  duration: const Duration(seconds: 2),
  vsync: this,
);

animation = CurvedAnimation(
  parent: controller, // controller
  curve: Curves.easeInCirc // curve
);
CurvedAnimation.transform()
class CurvedAnimation extends Animation<double> with AnimationWithParentMixin<double> {

  final Animation<double> parent;
  Curve curve;
  
  double get value {
    final double t = parent.value;
    curve.transform(parent.value);
  }
}

【実践】

Screen Recording 2025-10-24 at 21.48.49.gif

ソースコード
import 'package:flutter/material.dart';

class ExplicitAnimation extends StatefulWidget {
  const ExplicitAnimation({super.key});

  @override
  State<ExplicitAnimation> createState() => _ExplicitAnimationState();
}

class _ExplicitAnimationState extends State<ExplicitAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController controller;
  late Animation<Color?> animation;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    )
      ..addListener(
        () => setState(() {}),
      )
      ..addStatusListener(
        (status) {
          if (status == AnimationStatus.completed) {
            controller.reverse();
          } else if (status == AnimationStatus.dismissed) {
            controller.forward();
          }
        },
      );
    animation = ColorTween(
      begin: Colors.red,
      end: Colors.blue,
    ).animate(controller);
    controller.forward();
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100,
      height: 100,
      color: animation.value,
    );
  }
}

Listenable を理解する

Flutter のアニメーションはすべて、

Listenable
→ ChangeNotifier
→ AnimationController

という通知の連鎖の上に成り立っている。

AnimatedBuilder や AnimatedWidget は、この通知を UI に反映するためのパターンにすぎず、アニメーションの本質は 進行度(t)の時間変化を listenして再描画すること と言える。

Listenable

image.png

文字通り「listen できる」ことを表すインターフェース。

状態の変化を「listener の call()」を通じて通知するための手段(インターフェース)を提供する。

  • addListener()
  • removeListener()

Listenable 自体は値を保持しておらず、値の更新通知も行わない(単一責務の原則)。

提供するもの listener の登録手段 listen 対象の保持 listener の発火手段
担当 Listenable ValueListenable
Animation
ChangeNotifier
API addListener() T value(listen 対象) notifyListener()

image.png

ValueListenable<T>

image.png

文字通り「value を listen できる」機能を提供するインターフェース。

listener の監視対象である T value を保持する。

Animation<T>

image.png

listener の監視対象 T value、AnimationStatus status を保持する。

Listenable から監視手段である addListener() を継承しているため 2 つの機能を併せ持つ。

  • 監視手段を提供する
  • 監視対象を保持する

ChangeNotifier(mixin)

image.png

listener の発火手段である notifyListener() を提供する mixin。

notifyListener() によって listener に対して listen 対象の更新を通知する、とも表現される。

notifyListeners() の内部実装は listener を call() するだけ。

ChangeNotifier
mixin class ChangeNotifier implements Listenable {

  void notifyListeners() {
    final int end = _count;
    for (int i = 0; i < end; i++) {
      _listeners[i]?.call();
    }
  }
}

ValueNotifier<T>

image.png

ChangeNotifier の具象クラス。

setter によって value が変更されたタイミングで notifyListener() を実行する。

ValueNotifier
class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {
  
  @override
  T get value => _value;
  
  T _value;
  
  set value(T newValue) {
    if (_value == newValue) {
      return;
    }
    _value = newValue;
    notifyListeners(); // 👈
  }
}

アニメーションにおいては、進行度 t の変化によって生成された value の変更を検知し、liseter として登録された setState() を呼び、画面を更新する。画面側は生成された value を利用して、何らかの見た目の変更する。

ValueNotifier の listen 対象は immutable であることが推奨されている。

例えば List<int>(mutable)を保持した場合、List オブジェクト自体の差し替えは検知できるが、List 内部の要素変更は検知できない。

監視対象は immutable 推奨
List<int> value = List.empty();

notifier.value = newList;  // 検知できる
notifier.value.add(item);  // 検知できない

AnimationController

image.png

mixin の AnimationLocalListenersMixin から notifyListeners() の実装を引き継いでいる。

ValueNotifier 同様に以下の機能を併せ持つ。

  • 監視手段を提供 する
    • addListener()
  • 監視対象を保持 する
    • double value(ValueNotifier では T 型)
  • value 更新時に登録された listener を call() する
    • notifyListeners()

value が変更されたタイミングで notifyListeners() を実行する。

AnimationController
class AnimationController extends Animation<double> with AnimationLocalListenersMixin {

  @override
  double get value => _value;
  late double _value;

  set value(double newValue) {
    notifyListeners();
  }
}

AnimationController は ChangeNotifier に似た仕組みを持つが、value が時間と共に連続的に変化する点が異なる。

AnimationController の value は 内部の Ticker が進行度 t を更新するのに連動して更新される。

AnimatedBuilder を理解する

AnimatedBuilder は Animation を渡せば setState() を自動で呼んでくれる。

ListenableBuilder

image.png

純粋な Dart クラスの Listenable と Widget を紐づける役割を持つ。

Listenable の実装を listenable パラメータで受け取り、notifyListener() の度に builder を実行し、UI を再描画する。

開発者(私たち)が value を listen して setState() する必要がない。必要なのは listenable に Listenabe な Animation を渡すだけ。

child はオプションとなっている。

child は、アニメーション中に再描画を必要としない Widget が builder に含まれている場合に、不要な再描画を避け、パフォーマンスを向上させるために利用される(child に渡した Widget インスタンスが使い回される)。

ただし Animation を listen する場合は、読みやすさを向上させるために AnimatedBuilder が推奨されている。

Although they have identical implementations, if an Animation is being listened to, consider using an AnimatedBuilder instead for better readability.
実装は同じですが、Animation を listen している場合は、読みやすさを向上させるために、代わりに AnimatedBuilder を使用することを検討してください。
https://api.flutter.dev/flutter/widgets/ListenableBuilder-class.html

AnimatedBuilder

image.png

ListenableBuilder のアニメーション用。

中の実装は ListenableBuilder と同じ。

AnimatedBuilder
class AnimatedBuilder extends ListenableBuilder {
  const AnimatedBuilder({
    required Listenable animation,
  }) : super(listenable: animation);
  
  Listenable get animation => super.listenable;

AnimatedBuilder を使う

【実践1】

Screen Recording 2025-10-26 at 11.25.58.gif

ソースコード
AnimatedBuilder
import 'package:flutter/material.dart';

class ExplicitAnimation extends StatefulWidget {
  const ExplicitAnimation({super.key});

  @override
  State<ExplicitAnimation> createState() => _ExplicitAnimationState();
}

class _ExplicitAnimationState extends State<ExplicitAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    )
      ..addStatusListener(
        (status) {
          if (status == AnimationStatus.completed) {
            controller.reverse();
          } else if (status == AnimationStatus.dismissed) {
            controller.forward();
          }
        },
      )
      ..forward();
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
        animation: controller,
        builder: (context, _) {
          return Container(
            width: 100 * controller.value,
            height: 100 * controller.value,
            color: Colors.red,
          );
        });
  }
}

【実践 2】 child パラメータを利用する

アニメーションする Widget 内に変化しない静的な Widget が含まれる場合、child パラメータに渡す方法が推奨されている。

builder 関数の中では child を再利用できるので、不要な再構築(build())が行われない。

child
Widget build(BuildContext context) {
  return AnimatedBuilder(
      animation: controller,
      child: const Icon( // 👈
        Icons.brush,
        size: 100,
      ),
      builder: (context, child) { // 👈 で渡した Icon が child に渡される

Screen Recording 2025-10-26 at 21.37.34.gif

ソースコード
ExplicitAnimation
import 'package:flutter/material.dart';

class ExplicitAnimation extends StatefulWidget {
  const ExplicitAnimation({super.key});

  @override
  State<ExplicitAnimation> createState() => _ExplicitAnimationState();
}

class _ExplicitAnimationState extends State<ExplicitAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    )
      ..addStatusListener(
        (status) {
          if (status == AnimationStatus.completed) {
            controller.reverse();
          } else if (status == AnimationStatus.dismissed) {
            controller.forward();
          }
        },
      )
      ..forward();
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
        animation: controller,
        child: const Icon(
          Icons.brush,
          size: 100,
        ),
        builder: (context, child) {
          return Container(
            width: 100 * controller.value + 100,
            height: 100 * controller.value + 100,
            color: Colors.red,
            child: child,
          );
        });
  }
}

AnimatedWidget を理解する

StatelessWidget や StatefulWidget のように、継承して独自のサブクラスとして利用する Widget クラス。

AnimatedBuilder の builder が肥大化した時に AnimatedWidget として独立した Widget に切り出すことができる。

AnimatedWidget は AnimatedBuilder 同様に、Animation を渡すだけで setState() を自動で呼んでくれる。

Flutter SDK に含まれる既存の AnimatedWidget は FooTransiton という命名になっている(Explicit Animation)。

AnimatedWidget

image.png

AnimatedWidget の実体は StatefulWidget を継承した抽象クラス。

build() が抽象メソッドになっているので、サブクラスで build() をオーバーライドする必要がある。

AnimatedWidget
abstract class AnimatedWidget extends StatefulWidget {

  // 私たちが普段よく実装しているのは、この build() ではなく _AnimatedState 内の build() です
  Widget build(BuildContext context);

  @override
  State<AnimatedWidget> createState() => _AnimatedState();
}

class _AnimatedState extends State<AnimatedWidget> {

  @override
  Widget build(BuildContext context) => widget.build(context); // 継承先の build() がここで使われる
}

// サブクラス
class MyFooTransition extends AnimatedWidget {
  @override
  Widget build(BuildContext context) {
    return XXX();
  }
}

Listenable(通常は Animaiton もしくは ChangeNotifier)を受け取り、内部で setState() を呼び出す listener を自動登録していて、これによってアニメーションが実現する。

AnimatedWidget
abstract class AnimatedWidget extends StatefulWidget {
  const AnimatedWidget({required this.listenable});

  final Listenable listenable;
}

class _AnimatedState extends State<AnimatedWidget> {

  @override
  void initState() {
    widget.listenable.addListener(_handleChange);
  }

  void _handleChange() {
    if (!mounted) return;
    setState(() {});  // 👈
  }
}

この仕組みはすでに、前述の章 で実装しており、実はそこまで難しいことをしているわけではない。

AnimatedWidget は AniamtedBuilder の build() が巨大化した来た際に、関数ではなく クラスとして切り出すリファクタリングの手段でもある。

So, we have our animation, but the build method that contains the AnimatedBuilder code is a little large. If your build method is starting to get hard to read, it’s time to refactor your code!
アニメーションは完成しましたが、AnimatedBuilderコードを含むビルドメソッドが少し大きくなっています。ビルドメソッドが読みにくくなってきたら、コードをリファクタリングするタイミングです。
https://blog.flutter.dev/when-should-i-useanimatedbuilder-or-animatedwidget-57ecae0959e8

FooTransition の実体

AnimatedWidget を継承した Widget は Custom Explicit Animation であり、Built-in Explicit Widgt の実体は AnimatedWidget のサブクラス(FooTransition)である。

Built-in Explicit Animation

  • AnimatedWidget
    • SizeTransition
    • FadeTransition
    • AlignTransition
    • ScaleTransition
    • SlideTransition
    • PositionedTransition
    • DecoratedBoxTransition
    • DefaultTextStyleTransition
    • RelativePositionedTransitoin

【実践】

Flutter SDK にあるような FooTransition を自前で作成する。

Screen Recording 2025-10-26 at 12.43.45.gif

ソースコード
AnimatedWidget
import 'package:flutter/material.dart';

class ExplicitAnimation extends StatefulWidget {
  const ExplicitAnimation({super.key});

  @override
  State<ExplicitAnimation> createState() => _ExplicitAnimationState();
}

class _ExplicitAnimationState extends State<ExplicitAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    )
      ..addStatusListener(
        (status) {
          if (status == AnimationStatus.completed) {
            controller.reverse();
          } else if (status == AnimationStatus.dismissed) {
            controller.forward();
          }
        },
      )
      ..forward();
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MySizeTransition(listenable: controller);
  }
}

class MySizeTransition extends AnimatedWidget {
  const MySizeTransition({super.key, required super.listenable});

  Animation<double> get _animation => listenable as Animation<double>;

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 100 * _animation.value,
      height: 100 * _animation.value,
      color: Colors.red,
    );
  }
}

複数のアニメーションを同時に動かす

これまでの実装例では double や Color をそれぞれ単体でアニメーションさせてきたが、両者を同時にアニメーションさせたい場合には、Animation が持つ T value ではなく、Animatable の evaluate() を利用する。

Animatable
abstract class Animatable<T> {
  // 0.0〜1.0 の入力を受け取り、対応する値を返す
  T transform(double t);

  // animation.value を使って transform() を呼ぶ
  T evaluate(Animation<double> animation) => transform(animation.value);
}

実際には abstract Animatable<T> のサブクラス Tween<T> を使用する。

image.png

evaluate()
final opacityTween = Tween<double>(begin: 0.0, end: 1);
opacity = opacityTween.evaluate(controller);

final sizeTween = Tween<double>(begin: 0, end: 100);
size = sizeTween.evaluate(controller);

【実践】

Screen Recording 2025-10-26 at 22.11.29.gif

ソースコード
import 'package:flutter/material.dart';

class ExplicitAnimation extends StatefulWidget {
  const ExplicitAnimation({super.key});

  @override
  State<ExplicitAnimation> createState() => _ExplicitAnimationState();
}

class _ExplicitAnimationState extends State<ExplicitAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController controller;

  // 変更しないので、static 宣言している
  static final opacityTween = Tween<double>(begin: 0.0, end: 1);
  static final sizeTween = Tween<double>(begin: 0, end: 100);

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    )
      ..addStatusListener(
        (status) {
          if (status == AnimationStatus.completed) {
            controller.reverse();
          } else if (status == AnimationStatus.dismissed) {
            controller.forward();
          }
        },
      )
      ..forward();
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
        animation: controller,
        builder: (context, _) {
          return Opacity(
            opacity: opacityTween.evaluate(controller), // 👈
            child: Container(
              width: sizeTween.evaluate(controller), // 👈
              height: sizeTween.evaluate(controller), // 👈
              color: Colors.red,
            ),
          );
        });
  }
}

TweenAnimationBuilder を理解する

TweenAnimationBuilder は Implicit Animation に分類されるので、AnimationController を必要としない。

Flutter SDK から提供される AnimatedXXX 系のアニメーション Widget は、こん TweenAnimationBuidler を使用して実装されている。

TweenAnimatedBuilder は、アニメーションを実装するために使用するというよりは、「アニメーション機能を持つカスタムコンポーネントを自前で作成する」みたいな用途に向いている。

TweenAnimationBuilder

image.png

TweenAnimationBuilder は Custom Implicit Animation であるため、AnimationController は既に内包されている。

また Flutter は Built-in Implicit Animation として以下を用意しており、TweenAnimationBuilder は主にこれらで対応できないアニメーションを作成する際に利用される。

【実践】

Screen Recording 2025-10-28 at 21.58.00.gif

ソースコード
TweenAnimationBuilder
import 'package:flutter/material.dart';

class ImplicitAnimation extends StatefulWidget {
  const ImplicitAnimation({super.key});

  @override
  State<ImplicitAnimation> createState() => _ImplicitAnimationState();
}

class _ImplicitAnimationState extends State<ImplicitAnimation> {
  bool forward = true;

  @override
  Widget build(BuildContext context) {
    return TweenAnimationBuilder(
      tween: Tween<double>(
        begin: forward ? 0.0 : 100.0,
        end: forward ? 100.0 : 0.0,
      ),
      duration: const Duration(seconds: 2),
      builder: (_, value, __) {
        return Container(
          width: value,
          height: value,
          color: Colors.red,
        );
      },
      onEnd: () => setState(() => forward = !forward),
    );
  }
}

Staggered animations

複数の異なるアニメーションが連続して起こるようなアニメーションは Staggered animations と呼ばれる。

  • アニメーションさせるプロパティごとに Tween を作成する
    • 複数の Animation を使う
    • Animation は Interval を使う
  • AnimationController は 1 つ

image.png
https://docs.flutter.dev/ui/animations/staggered-animations

Staggered animations に必要な AnimationController は 1 つ

【実践】

Screen Recording 2025-10-30 at 22.47.12.gif

ソースコード
Staggered animations
import 'package:flutter/material.dart';

class ExplicitAnimation extends StatefulWidget {
  const ExplicitAnimation({super.key});

  @override
  State<ExplicitAnimation> createState() => _ExplicitAnimationState();
}

class _ExplicitAnimationState extends State<ExplicitAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController controller;
  late Animation<double> size;
  late Animation<double> opacity;
  late Animation<Color?> color;
  late Animation<BorderRadius> borderRadius;

  static final sizeTween = Tween<double>(begin: 100.0, end: 200.0);
  static final opacityTween = Tween<double>(begin: 0.0, end: 1.0);
  static final colorTween = ColorTween(begin: Colors.red, end: Colors.blue);
  static final borderRadiusTween = Tween<BorderRadius>(
    begin: const BorderRadius.all(Radius.circular(0.0)),
    end: const BorderRadius.all(Radius.circular(50.0)),
  );

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      duration: const Duration(seconds: 2),
      reverseDuration: const Duration(seconds: 2),
      vsync: this,
    )
      ..addStatusListener(
        (status) {
          if (status == AnimationStatus.completed) {
            controller.reverse();
          } else if (status == AnimationStatus.dismissed) {
            controller.forward();
          }
        },
      )
      ..forward();

    size = sizeTween.animate(
      CurvedAnimation(
        parent: controller,
        curve: const Interval(
          0.0,
          0.25,
        ),
      ),
    );
    opacity = opacityTween.animate(
      CurvedAnimation(
        parent: controller,
        curve: const Interval(
          0.25,
          0.5,
        ),
      ),
    );
    color = colorTween.animate(
      CurvedAnimation(
        parent: controller,
        curve: const Interval(0.50, 0.75),
      ),
    );
    borderRadius = borderRadiusTween.animate(
      CurvedAnimation(
        parent: controller,
        curve: const Interval(0.75, 1.0),
      ),
    );
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: controller,
      builder: (context, _) {
        return Opacity(
          opacity: opacity.value,
          child: Container(
            width: size.value,
            height: size.value,
            decoration: BoxDecoration(
              color: color.value,
              borderRadius: borderRadius.value,
            ),
          ),
        );
      },
    );
  }
}

TweenSequence を理解する

TweenSequence は Staggered animations のような同時並行的な複数アニメーションではないが、一つのアニメーション中に「ある地点までは A のTween を使って、それ以降は B の Tween を使う」のような、連続的な Tween を生成することができる。

TweenSequence<T>

image.png

複数の Tween を連結して 1 つの Animation として扱えるようにするクラス。

これにより「最初は拡大」「次に縮小」といった 段階的な動きを 1 つの AnimationController で自然に表現できる。

コンストラクタは List<TweenSequenceItem<T>> を受け取る。

final TweenSequence<double> serquence = TweenSequence<double>(
  <TweenSequenceItem<double>>[ ],
);

image.png

TweenSequenceItem の weight に指定した値が duraton に対する割合になる(順番はリスト順)。

weight は 相対的な割合 を示す。

例えば [4, 4, 2] とすれば、全体を 100% としたときの、それぞれが 40% : 40% : 20% の時間配分として扱われる。

TweenSequence
final tween = TweenSequence<double>([
  TweenSequenceItem(
    tween: Tween(begin: 0.0, end: 100.0),
    weight: 40.0, // 0 ~ 40%
  ),
  TweenSequenceItem(
    tween: Tween(begin: 100.0, end: 50.0),
    weight: 40.0, // 40 ~ 80%
  ),
  TweenSequenceItem(
    tween: Tween(begin: 50.0, end: 200.0),
    weight: 20.0, // 80 ~ 100%
  ),
]);

image.png

【実践】

これまでは「アニメーションが完了したら逆再生する」といった制御を、
addStatusListener() で AnimationStatus を監視して forward() や reverse() を呼び出す必要があった。

TweenSequence を使うと、1つの AnimationController で往復や段階的な動きを自然に表現できるため、これらの listener による制御が不要になる。

Screen Recording 2025-11-01 at 6.27.19.gif

ソースコード
import 'package:flutter/material.dart';

class ExplicitAnimation extends StatefulWidget {
  const ExplicitAnimation({super.key});

  @override
  State<ExplicitAnimation> createState() => _ExplicitAnimationState();
}

class _ExplicitAnimationState extends State<ExplicitAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController controller;
  late Animation<double> animation;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    )..repeat();

    animation = TweenSequence<double>(
      <TweenSequenceItem<double>>[
        TweenSequenceItem<double>(
          tween: Tween<double>(begin: 0.0, end: 100.0),
          weight: 50.0,
        ),
        TweenSequenceItem<double>(
          tween: Tween<double>(begin: 100.0, end: 0.0),
          weight: 50.0,
        ),
      ],
    ).animate(controller);
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
        animation: animation,
        builder: (context, _) {
          return Container(
            width: animation.value,
            height: animation.value,
            color: Colors.red,
          );
        });
  }
}

Ticker を理解する

AnimationController のコンストラクタには、必ず TickerProvider オブジェクトを渡さないといけない。

vsync
controller = AnimationController(
  vsync: this, // 👈 this が TickerProvider
  duration: const Duration(seconds: 2),
);

渡された TickerProvider は Ticker を生成して、AnimationController に提供する。

Tikcer はアニメーションに対して進行度 t を時間経過に応じて変化させる機能、いわば時間のカチカチ(tick)を提供する。

vsync

AnimationController のコンストラクタ引数。

vsync
controller = AnimationController(
  vsync: this, // 👈 this が TickerProvider
  duration: const Duration(seconds: 2),
);

Vertical Synchronization の略。

画面の Frame とアニメーションの更新を同期させるという意味。

TickerProvider は mixin で State に対して mix in される。Flutter は State が画面上に見えている間だけ Ticker を有効化する。

Ticker

image.png

Frame ごとにコールバック関数を呼び出す オブジェクト。

用途はアニメーションに限定されない。

Ticker
final ticker = Ticker((elapsed) {
  // 処理 (elapsed は経過時間)
});
ticker.start()

start() によって有効化される(生成されるのみでは有効化されない)。

dispose() 処理を必要とする(dispose() しないと、延々とコールバック関数が呼ばれ続ける)。

muted フラグを OFF することで、一時停止が可能。

ミュート中、コールバック関数は実行されない状態となる。

Ticker
class Ticker {
  
  bool get muted => _muted;
  bool _muted = false;

  set muted(bool value) {
    if (value == muted) {
      return;
    }
    _muted = value;
    if (value) {
      unscheduleTick();
    } else if (shouldScheduleTick) {
      scheduleTick();
    }
  }
}

start()、stop() は AnimationController によって利用される。

muted は TickerProvier によって利用される。

内部では SchedulerBinding.instance.scheduleFrameCallback() が利用されている。

Ticker
class Ticker {

  void scheduleTick({bool rescheduling = false}) {
    assert(!scheduled);
    assert(shouldScheduleTick);
    _animationId = SchedulerBinding.instance.scheduleFrameCallback(
      _tick,
      rescheduling: rescheduling,
    );
  }
}

TickerProvider(abstract class)

image.png

Ticker のファクトリ。

Ticker は直接インスタンス化もできるが、通常は TickerProvider を介して利用される(主に利用するのは AnimationController)。

SingleTickerProviderStateMixin<T extends StatefulWidget>(mixin)

image.png

1 つの Ticker をもつ State に vsync を提供する。

SingleTickerProviderStateMixin
mixin SingleTickerProviderStateMixin<T extends StatefulWidget> on State<T>
    implements TickerProvider {

  Ticker? _ticker;

  @override
  Ticker createTicker(TickerCallback onTick) {
    _ticker = Ticker(onTick);
    return _ticker!;
  }

Ticker を管理する手間を省いてくれる。

StatefulWidget の State に mixin すると、AnimationController は Ticker を要求できるようになる。

TickerProviderStateMixin<T extends StatefulWidget>(mixin)

複数の Ticker をもつ State に vsync を提供する。

【デバッグ】

RepaintBoundary

Flutter が用意するデバッグ用のフラグ debugRepaintRainbowEnable を ON にすることで、アニメーションの最中にどの Frame が更新されているかが緑色で囲まれるため、視覚的に不要な領域が更新されていないかを確認することができる。

RepaintBoudary
void main() {
    debugRepaintRainbowEnable = true;
    runApp(MyApp());
}

アニメーション中に変化しない Widget が、この緑の領域に含まれていた場合、余分な Frame 処理のためのオーバーヘッドが発生していることを意味する。

Flutter 3.0 付近で削除された API です

Flutter 3.35.6 では DevTools に内包されていた。

Screenshot 2025-11-01 at 6.45.53.png

Screen Recording 2025-11-01 at 6.48.30.gif

これをみると、アニメーション中赤い四角形だけでなく、画面全体が再描画されてしまっていた。

RepaintBoundary ウィジェットでラップすると再描画の対象が最適化されるとのことだったが、緑の枠線が二重で表示されるようになった。

Screen Recording 2025-11-01 at 6.55.49.gif

最適化は child パラメータを意識しておくと良さそう。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?