はじめに
Flutter の公式サイトを中心にアニメーションの基礎を理解したい。
Flutter のソースコードは画面右上のアイコンをクリックすることで見ることができる。
アニメーションとは
アニメーションは Frame と呼ばれる描画処理を高速に繰り返すことによって実現する。
パラパラ漫画で言う、ひとコマひとコマが Frame であり、一般的に 1 秒間に 60 回 Frame が更新される 60 fps(frame per second)がヌルヌル動くと言われる。

https://cameragurus.com/30fps-vs-60fps/
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 が内部で生成してくれるので、「始点」と「終点」を定義するだけで良い。
AnimatedContainer(
duration: const Duration(seconds: 2),
color: isRed ? Colors.red : Colors.blue, // 👈
)
Explicit Animation
AnimationController を必要とするアニメーション。
AnimationController は forward() / reverse() / stop() など多彩なアニメーションに対するコントローラ機能を持つ。
また AnimationController は Widget ではないため、wrap して使うというよりは、StatefulWidget 内に配置して、オブジェクトの生成、破棄などのライフサイクルを管理する必要がある。
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 の選び方
- Introduction to animations
- How to choose which Flutter Animation Widget is right for you? - Flutter in Focus
Implicit / Explicit のどちらを使用すべきかについては、「実現したいこと」に対して適切な選択をする必要がある。
複雑なアニメーションにしようとすればするほど、より低レベルな Flutter ライブラリを使用する必要性がある。

https://blog.flutter.dev/how-to-choose-which-flutter-animation-widget-is-right-for-you-79ecfb7e72b5
難易度
簡単 < Implicit < Explicit < CustomPainter < 難しい

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 AnimatedFooAnimatedContainerAnimatedOpacity- ...
- Flutter SDK に含まれる
-
Custom
-
TweenAnimationBuilderを使用して自前で作成するWidget
-
Built-in から目的の Widget が探せなかった場合、Custom Widget を作成する必要がある。
Explicit Animation
Explicit Animation は以下の 2 種類に分類される。
-
Built-in
- Flutter SDK に含まれる
Widget FooTransitionSizeTransitionSlideTransition- ...
- Flutter SDK に含まれる
-
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 を継承したクラス群となっている。
- Size
- Align
- Container
- DefaultTextStyle
- Opacity
- Padding
- PhysicalModel
- Positioned
- PositionedDirectional
- Theme
Custom Explicit Animation
Explicit Animation を理解するためには Animation<T> への理解が必須。
Animation<T> (abstract class)
UI に T value と AnimationStatus status を提供する 抽象クラス。
-
value- ハードウェアが新たな Frame を用意する度に生成される
- 60 fps の環境においては、1 秒間に 60 回生成
-
進行度
t(0.0~1.0) をインプットにしてアウトプットされる生成値- 生成値
valueはアウトプット時に数学的関数によって 線形補間 or 非線形補間 される
- 生成値
- ハードウェアが新たな Frame を用意する度に生成される
-
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(どのように利用、表示されているか)についての知識は持たない。
型引数 T は double が最もよく利用されるが、それ以外にも Color、Size などのオブジェクトに関しても補間することができる。
Animation<T> は UI に関する知識を持たないクラス
AnimationController
抽象クラス Animation<double>を継承した具象クラス。
線形補間 された値(value)が生成される。

https://api.flutter.dev/flutter/animation/Curves/linear-constant.html
デフォルトでは生成値(value)の取りうる範囲は 0.0 ~ 1.0。
「進行度 t」 と「生成値(value)」の範囲が同じ 0.0 ~ 1.0 なので混同しがちだが、両者は異なるものであることに注意。
0.0 ~ 1.0 以外の範囲の値を使用したい場合、Tween を使用する。
AnimationController は加えて アニメーションを開始、停止するためのコントローラ機能も併せ持つ。
forward()stop()reverse()repeat()
このコントローラ機能は アニメーションを drive(駆動する、操縦する) と表現されることがある。
CurvedAnimation
AnimationController 同様に、抽象クラス Animation<double> を継承した具象クラス。
進行度 t を 非線形補間(non-linear curve)によって double value に変換する。
非線形補間においては、時間(t)の進行に対して生成値(value / x)の変化速度が一定でない。

https://api.flutter.dev/flutter/animation/Curves-class.html
AnimationController と同様に value を提供する機能を持つ一方で、CurvedAnimation は コントローラとしての機能を持たない。
コントローラ機能は外部から parent に注入する必要がある。
final Animation<double> animation = CurvedAnimation(
parent: controller, // 👈 コントローラ機能を注入
);
デフォルトでは value は 0.0 ~ 1.0 の範囲を持つ。
CurvedAnimation は 0.0 ~ 1.0 を非線形補間している
Animation vs AnimationController vs CurvedAnimation
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 を必要とする。
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() 内で変更するだけで実現する。
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, // 👈
ソースコード
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】 基本的なアニメーション
AnimationController は value が変更された際に実行される listener を addListener() で登録することができる。
この addListener() では setState() を呼び出すことで、value の変更時に再描画を実行させることができる。
controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
)
..addListener(() => setState(() {}),
同様に status が変更された際の listener も addStatusListener() で登録することができる。
..addStatusListener(
(status) {
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
},
)
これによって、最も基本的な 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,
);
}
}
↑ソースコード(ステータス表示)
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 を提供する。

https://api.flutter.dev/flutter/animation/Curves-class.html
これを CurvedAnimation を用いて 非線形補間(no-linear curve)に変換する。
animation = CurvedAnimation(parent: controller, curve: Curves.easeInCirc);
Widget は CurvedAnimation が提供する double value によって描画を行う。
ソースコード
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,
);
}
}
ソースコード(ステータス表示)
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)
0.0 ~ 1.0 の double 値(進行度 t)を受け取り、実際にアニメーションさせたい型(T)の値(value)を出力する変換器。
変換処理は、内部で evaluate() によって行われる。
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);
}
Tween<T extends Object?>
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() が利用されている。
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 を生成する。
実装を見ると、lerp() は T begin ~ T end の間の、進行度 t に応じた value を計算していることが分ける。
lerp は Linear IntERPolation(線形補間)の略語。
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() で実行されているのは以下の計算。
T lerp(double t) {
return (begin as dynamic) + ((end as dynamic) - (begin as dynamic)) * t as T;
}
キャスト部分を取り除いてみる。
begin + (end - begin) * t
end - begin は始点と終点の差分。t は進行度 0.0 ~ 1.0。
上記を以下の条件で計算してみる。
-
begin= 100 -
end= 200 -
t= 0.5
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 -
BorderTween -
BoxConstraintsTween -
ColorTween -
DecorationTween -
IntTweendouble.round()- 近似値
-
MaterialPointArcTween -
Matrix4Tween -
RectTween -
RelativeRectTween -
ShapeBorderTween -
SizeTween -
TextStyleTween -
ThemeDataTween
class AlignmentTween extends Tween<Alignment> {
@override
Alignment lerp(double t) => Alignment.lerp(begin, end, t)!;
}
class BorderRadiusTween extends Tween<BorderRadius?> {
@override
BorderRadius? lerp(double t) => BorderRadius.lerp(begin, end, t);
}
XXXTween の仕事は線形補間の実体である lerp() 関数を提供すること
Tween は mutable なオブジェクトだが、変更されない場合には static final で定義しておくと再描画の度にインスタンス生成されないので少しパフォーマンス的に良い。
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> は 双方向から接続することができる。
animation = controller.drive(Tween(begin: 0.0, end: 100.0));
controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = CurvedAnimation(parent: controller, curve: Curves.easeInCirc)
.drive(Tween(begin: 0.0, end: 100.0));
animation = Tween(begin: 0.0, end: 100.0).animate(controller);
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);
Tween x CurvedAnimation
任意のデータ型 を 任意の範囲 で → Tween の仕事
非線形補完 → CurvedAnimation の仕事
以下のように animation を作ることで、これらを合わせることができる。
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 を返すためのインターフェース。
用途はアニメーションに限定されない。
transform() は内部で transformInternal() 使用している。transformInternal() は override されることを前提とした作りになっている。
abstract class ParametricCurve<T> {
T transform(double t) {
return transformInternal(t);
}
T transformInternal(double t) {
throw UnimplementedError();
}
}
Curve(abstract class)
抽象クラス ParametricCurve<double> をさらに継承した抽象クラス。
0.0 ~ 1.0 の進行度 t を transformInterval() によって 非線形補間 する。
transformInternal() は 前述 の通りオーバーライドを前提とした関数なので、カスタムなロジックをここに埋め込めば、オリジナルな非線形補間(Cureve)を作成することができる。
class MyCurve extends Curve {
double transform(double t) {
return transformInternal(t);
}
@override
double transformInternal(double t) => カスタムのロジック; // 0.0 <= t <= 1.0
}
CurvedAnimation のコンストラクタにパラメータとして渡して使用される。
CurvedAnimation(
parent: controller,
curve: MyCurve(), // 👈
)
Cubic
cubic = 三次元
Curve を継承した具象クラス。
4 つの制御点で定義される三次 ベジェ曲線 を使って、滑らかな補間を行う。
-
a: 最初の制御点の x 座標 -
b: 最初の制御点の y 座標 -
c: 2 番目の制御点の x 座標 -
d: 2 番目の制御点の y 座標
Curves で多く利用されている。
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 = 閾値
進行度 t が閾(しきい)値を超えたら 1.0 にジャンプする離散的な Curve。

https://api.flutter.dev/flutter/animation/Threshold-class.html
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 のリポジトリ)。

https://api.flutter.dev/flutter/animation/Curves-class.html
Interval
開始点(t = 0.0)からある時点までは 0.0 を返し、終了点(t = 1.0)より前に 1.0 に到達しその後 1.0 を返し続ける Curve。

https://api.flutter.dev/flutter/animation/Curves-class.html
アニメーションを遅延させたりする際に利用できる。
Curve vs Cubic vs Threshold vs Curves vs Interval
Cubic、Threshold、Interval は全て Curve である。
CurvedController vs CurvedAnimation vs Curve
AnimationController が保持するアニメーションの進行度 t(0.0 ~ 1.0)は Curve によって非線形補間され、Widget(UI)に伝達される。
class CurvedAnimation extends Animation<double> with AnimationWithParentMixin<double> {
CurvedAnimation({
required this.parent,
required this.curve,
});
final Animation<double> parent;
Curve curve;
}
controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
animation = CurvedAnimation(
parent: controller, // controller
curve: Curves.easeInCirc // curve
);
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);
}
}
【実践】
ソースコード
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
文字通り「listen できる」ことを表すインターフェース。
状態の変化を「listener の call()」を通じて通知するための手段(インターフェース)を提供する。
addListener()removeListener()
Listenable 自体は値を保持しておらず、値の更新通知も行わない(単一責務の原則)。
| 提供するもの | listener の登録手段 | listen 対象の保持 | listener の発火手段 |
|---|---|---|---|
| 担当 | Listenable |
ValueListenableAnimation
|
ChangeNotifier |
| API | addListener() |
T value(listen 対象) |
notifyListener() |
ValueListenable<T>
文字通り「value を listen できる」機能を提供するインターフェース。
listener の監視対象である T value を保持する。
Animation<T>
listener の監視対象 T value、AnimationStatus status を保持する。
Listenable から監視手段である addListener() を継承しているため 2 つの機能を併せ持つ。
- 監視手段を提供する
- 監視対象を保持する
ChangeNotifier(mixin)
listener の発火手段である notifyListener() を提供する mixin。
notifyListener() によって listener に対して listen 対象の更新を通知する、とも表現される。
notifyListeners() の内部実装は listener を call() するだけ。
mixin class ChangeNotifier implements Listenable {
void notifyListeners() {
final int end = _count;
for (int i = 0; i < end; i++) {
_listeners[i]?.call();
}
}
}
ValueNotifier<T>
ChangeNotifier の具象クラス。
setter によって value が変更されたタイミングで notifyListener() を実行する。
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 内部の要素変更は検知できない。
List<int> value = List.empty();
notifier.value = newList; // 検知できる
notifier.value.add(item); // 検知できない
AnimationController
mixin の AnimationLocalListenersMixin から notifyListeners() の実装を引き継いでいる。
ValueNotifier 同様に以下の機能を併せ持つ。
-
監視手段を提供 する
addListener()
-
監視対象を保持 する
-
double value(ValueNotifierではT型)
-
-
value更新時に登録された listener をcall()するnotifyListeners()
value が変更されたタイミングで notifyListeners() を実行する。
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
純粋な 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
ListenableBuilder のアニメーション用。
中の実装は ListenableBuilder と同じ。
class AnimatedBuilder extends ListenableBuilder {
const AnimatedBuilder({
required Listenable animation,
}) : super(listenable: animation);
Listenable get animation => super.listenable;
AnimatedBuilder を使う
【実践1】
ソースコード
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())が行われない。
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: controller,
child: const Icon( // 👈
Icons.brush,
size: 100,
),
builder: (context, child) { // 👈 で渡した Icon が child に渡される
ソースコード
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
AnimatedWidget の実体は StatefulWidget を継承した抽象クラス。
build() が抽象メソッドになっているので、サブクラスで build() をオーバーライドする必要がある。
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 を自動登録していて、これによってアニメーションが実現する。
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
-
AnimatedWidgetSizeTransitionFadeTransitionAlignTransitionScaleTransitionSlideTransitionPositionedTransitionDecoratedBoxTransitionDefaultTextStyleTransitionRelativePositionedTransitoin
【実践】
Flutter SDK にあるような FooTransition を自前で作成する。
ソースコード
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() を利用する。
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> を使用する。
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);
【実践】
ソースコード
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
TweenAnimationBuilder は Custom Implicit Animation であるため、AnimationController は既に内包されている。
また Flutter は Built-in Implicit Animation として以下を用意しており、TweenAnimationBuilder は主にこれらで対応できないアニメーションを作成する際に利用される。
AnimatedSizeAnimatedAlignAnimatedContainerAnimatedDefaultTextStyleAnimatedOpacityAnimatedPaddingAnimatedPhysicalModelAnimatedPositionedAnimatedPositionedDirectionalAnimatedTheme
【実践】
ソースコード
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 つ

https://docs.flutter.dev/ui/animations/staggered-animations
Staggered animations に必要な AnimationController は 1 つ
【実践】
ソースコード
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>
複数の Tween を連結して 1 つの Animation として扱えるようにするクラス。
これにより「最初は拡大」「次に縮小」といった 段階的な動きを 1 つの AnimationController で自然に表現できる。
コンストラクタは List<TweenSequenceItem<T>> を受け取る。
final TweenSequence<double> serquence = TweenSequence<double>(
<TweenSequenceItem<double>>[ ],
);
TweenSequenceItem の weight に指定した値が duraton に対する割合になる(順番はリスト順)。
weight は 相対的な割合 を示す。
例えば [4, 4, 2] とすれば、全体を 100% としたときの、それぞれが 40% : 40% : 20% の時間配分として扱われる。
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%
),
]);
【実践】
これまでは「アニメーションが完了したら逆再生する」といった制御を、
addStatusListener() で AnimationStatus を監視して forward() や reverse() を呼び出す必要があった。
TweenSequence を使うと、1つの AnimationController で往復や段階的な動きを自然に表現できるため、これらの listener による制御が不要になる。
ソースコード
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 オブジェクトを渡さないといけない。
controller = AnimationController(
vsync: this, // 👈 this が TickerProvider
duration: const Duration(seconds: 2),
);
渡された TickerProvider は Ticker を生成して、AnimationController に提供する。
Tikcer はアニメーションに対して進行度 t を時間経過に応じて変化させる機能、いわば時間のカチカチ(tick)を提供する。
vsync
AnimationController のコンストラクタ引数。
controller = AnimationController(
vsync: this, // 👈 this が TickerProvider
duration: const Duration(seconds: 2),
);
Vertical Synchronization の略。
画面の Frame とアニメーションの更新を同期させるという意味。
TickerProvider は mixin で State に対して mix in される。Flutter は State が画面上に見えている間だけ Ticker を有効化する。
Ticker
Frame ごとにコールバック関数を呼び出す オブジェクト。
用途はアニメーションに限定されない。
final ticker = Ticker((elapsed) {
// 処理 (elapsed は経過時間)
});
ticker.start()
start() によって有効化される(生成されるのみでは有効化されない)。
dispose() 処理を必要とする(dispose() しないと、延々とコールバック関数が呼ばれ続ける)。
muted フラグを OFF することで、一時停止が可能。
ミュート中、コールバック関数は実行されない状態となる。
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() が利用されている。
class Ticker {
void scheduleTick({bool rescheduling = false}) {
assert(!scheduled);
assert(shouldScheduleTick);
_animationId = SchedulerBinding.instance.scheduleFrameCallback(
_tick,
rescheduling: rescheduling,
);
}
}
TickerProvider(abstract class)
Ticker のファクトリ。
Ticker は直接インスタンス化もできるが、通常は TickerProvider を介して利用される(主に利用するのは AnimationController)。
SingleTickerProviderStateMixin<T extends StatefulWidget>(mixin)
1 つの Ticker をもつ State に vsync を提供する。
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 が更新されているかが緑色で囲まれるため、視覚的に不要な領域が更新されていないかを確認することができる。
void main() {
debugRepaintRainbowEnable = true;
runApp(MyApp());
}
アニメーション中に変化しない Widget が、この緑の領域に含まれていた場合、余分な Frame 処理のためのオーバーヘッドが発生していることを意味する。
Flutter 3.0 付近で削除された API です
Flutter 3.35.6 では DevTools に内包されていた。
これをみると、アニメーション中赤い四角形だけでなく、画面全体が再描画されてしまっていた。
RepaintBoundary ウィジェットでラップすると再描画の対象が最適化されるとのことだったが、緑の枠線が二重で表示されるようになった。
最適化は child パラメータを意識しておくと良さそう。

























































