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

https://cameragurus.com/30fps-vs-60fps/
Flutter には以下の 2 種類のアニメーション対象が存在する。
- Drawing-based animations
- Code-based animation
アニメーションのさせ方は以下の 2 種類に分類される。
- Tween animation
- Physics-based animation
Drawing-based animations
ゲームのキャラクターのような Canvas に絵を描くようにして描画された対象物を、Frame の度に手動で変化を加えてアニメーションさせるもの。
既存の Widget(Row、Column、textStyle)で表現できない
- ベクター画像(数式、直線、曲線で表現された画像)
- ラスター画像(ピクセルの集合で表現された画像)
を使用するものに対して適用する。
-
RiveやLottieなどのサードパーティ製ツールが充実している -
CustomPainterを使用する
Code-based animation
Widget のプロパティを変化させることで実現するアニメーション。
※ この記事のほとんどは、Code-based animation に関わるものです。
Tween animation
アニメーションの「始点」と「終点」を定義しておき、その間を補間させることによって実現するアニメーション。
Tween は Between に由来している。
※ この記事のほとんどは、Tween animation に関わるものです。
Physics-based animation
現実世界の物理法則を模倣したモデルによって実現するアニメーション。
参考: Animate a widget using a physics simulation
【解像度 1】 Flutter のアニメーション
Code-based animation は以下の 2 つに大別される。
- Implicit Animation(暗黙的アニメーション)
- Explicit Animation(明示的アニメーション)
実現したいアニメーションが単純であれば Implicit Animation を使う。
「難しい」アニメーションの場合には Explicit Animation を使用する必要が出てくる。
「難しい」とは、アニメーションが以下の特性を持つ場合を指す。
- 永続的
- 連続性
- 複数の
Widgetが連携してアニメーションする
Implicit Animation
Widget の Color など、特定のプロパティを A → B に遷移させるようなアニメーション。
A → B に変えたときは setState() を呼ぶだけ。
class ImplicitAnimation extends StatefulWidget {
const ImplicitAnimation({super.key});
@override
State<ImplicitAnimation> createState() => _ImplicitAnimationState();
}
class _ImplicitAnimationState extends State<ImplicitAnimation> {
bool isRed = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => setState(() => isRed = !isRed), // 👈
child: AnimatedContainer(
duration: const Duration(seconds: 2),
width: 100,
height: 100,
color: isRed ? Colors.red : Colors.blue, // 👈
),
);
}
}
Explicit Animation
AnimationController を必要とするアニメーション。
AnimationController.forward() による「明示的な」アニメーションの開始が必要。
AnimationController は Widget ではないため、StatefulWidget 内に配置 する必要があり、AnimationController オブジェクトのライフサイクルを管理する必要がある(initState() 内でインスタンス生成、dispose() 内での破棄)。
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
難易度
easier < Implicit < Explicit < CustomPainter < harder

https://blog.flutter.dev/how-to-choose-which-flutter-animation-widget-is-right-for-you-79ecfb7e72b5
【解像度 2】 暗黙的 / 明示的アニメーションとは
Implicit Animation
Implicit Animation は以下の 2 種類に分類される。
-
Built-in
- Flutter SDK に含まれる
Widget AnimatedFooAnimatedContainerAnimatedOpacity- ...
- Flutter SDK に含まれる
-
Custom
-
TweenAnimationBuilderを使用して自前で作成するWidget
-
Built-in(Flutter SDK に含まれる Widget)から目的の Widget が探せなかった場合、カスタム 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 で実現することができる。
【解像度 3】 アニメーションを実装する(導入)
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 を提供する 抽象クラス。
-
T value- ハードウェアが新たな Frame を用意する度に生成される
- 60 fps(frame per seconds: 1 秒間に 60 回 frame を更新する)の環境においては、1 秒間に 60 回生成値を返す
- 生成される値
- 与えられた始点と終点の間を 線形補間 or 非線形補間 した値
- ハードウェアが新たな Frame を用意する度に生成される
-
AnimationStatus status- アニメーションの状態
-
dismissed-
0.0→1.0に進行中
-
-
forward-
1.0→0.0に進行中
-
-
completed-
1.0に到達
-
-
dismissed-
0.0に到達
-
-
- アニメーションの状態
UI に value を提供するが、UI(どのように表示されているか)についての知識は持たない。
型引数 T は double が最もよく利用されるが、それ以外にも Color、Size などのオブジェクトに関しても補間することができる。
Animation<T> は UI に関する情報を持たない
AnimationController
抽象クラス Animation<double>を継承した具象クラス。
線形補間された値が生成される。

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

https://api.flutter.dev/flutter/animation/Curves-class.html
AnimationController と同様に Animation<double> を継承する一方で、CurvedAnimation は コントローラとしての機能を持たない。
コントローラ機能は外部から注入する必要がある。
final Animation<double> animation = CurvedAnimation(
parent: controller, // 👈 コントローラ機能を注入
curve: Curves.ease,
);
デフォルトでは 0.0 ~ 1.0 の範囲を持つ。
CurvedAnimation は 0.0 ~ 1.0 を非線形(曲線 = Curve)補間している
Animation vs AnimationController vs CurvedAnimation
AnimationController と CurvedAnimation はどちらも Animation<duble> を継承しているため、型としては互換性を持ち、同じ API(value、addListener()) が共有できるが、コントローラ機能の互換性は無い。
| クラス名 | 継承関係 | 出力値 | 補間 | コントローラ機能 |
|---|---|---|---|---|
Animation<T> |
抽象クラス | T value |
- | - |
AnimationController |
具象クラス |
0.0 ~ 1.0
|
線形補間 | あり |
CurvedAnimation |
具象クラス |
0.0 ~ 1.0(デフォルト) |
非線形補間 | - |
アニメーションのコントローラ機能を有する と言う点では、明示的な(Explicit)アニメーションで重要な役割を担う AnimationController は、唯一無二の特別な存在である。
アニメーションを明示的に開始したり、停止できるコントローラ機能を持ったクラスは他には存在しない。
AnimationController はアニメーションを動かす(drive する)唯一の動力
AnimationController と CurvedAnimation はどちらも Animation<double> 型の変数に代入できるのに、変数名が animation だったり contorller だったりするのは、コントローラ機能の有無の違いによるものである。
CurvedAnimation 自体はコントローラを持たず、親(parent)に指定された AnimationController によって駆動(drive)される。
final Animation<double> controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
final Animation<double> animation = CurvedAnimation(
parent: controller,
curve: Curves.ease,
);
【解像度 4】 アニメーションを実装する(実践)
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 が変更された際に実行されるリスナを addListener() で登録することができる。
この addListener() では setState() を呼び出すことで、value の変更時に再描画を実行させることができる。
controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
)
..addListener(() => setState(() {}),
同様に status が変更された際のリスナも 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,
),
),
],
);
}
}
【解像度 5】 応用的なアニメーションを実装する(導入)
Animatable<T>(abstract class)
0.0 ~ 1.0 の double 値(進行度)を受け取り、実際にアニメーションさせたい型(T)の値を出力する変換器。
値への変換処理は、内部で T evaluate(Animation<double> animation) によって行われる。
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 は 任意のデータ型 を 任意の範囲 で 線形補間 することができる。
-
任意のデータ型
-
double以外のデータ型(Color、Size...)
-
final doubleTween = Tween<double>(begin: -200, end: 0);
-
任意の範囲
-
0.0~1.0の範囲のdouble tを使用してT begin~T endの範囲内の値、もしくはオブジェクトに変換
-
final sizeTween = Tween<Size>(begin: const Size(100, 100), end: const Size(200, 200));
final colorTween = Tween<Color>(begin: Colors.red, end: Colors.blue);
AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(controller);
Tween<> は Animatable<T>(Animation<T> とは異なる)を継承している。
AnimationController や CurvedAnimation は Animation<double> を継承している。
Tween<T> は T が double に限定されない。
Tween は 0.0 ~ 1.0 の入力値を T begin ~ T end に線形補間している
Animation vs AnimationController vs CurvedAnimation vs Tween
| クラス名 | 継承関係 | 入力値 | 出力値 | 補間 |
|---|---|---|---|---|
Animation<T> |
抽象クラス | - |
T valueAnimationStatus status
|
- |
AnimationController |
具象クラス | 時間(vsync) |
0.0 ~ 1.0
|
線形補間 |
CurvedAnimation |
具象クラス | double t |
0.0 ~ 1.0(※) |
非線形補間 |
Animatable<T> |
抽象クラス | double t |
- | - |
Tween<T> |
具象クラス | double t |
T begin ~ T end |
線形補間 |
※ 一部の Curve は範囲外の値をとる
Animatable<T> と Animation<T> は 双方向から接続することができる。
final animation = controller.drive(Tween(begin: 0.0, end: 100.0));
final animation = Tween(begin: 0.0, end: 100.0).animate(controller);
【解像度 6】 応用的なアニメーションを実装する
【実践】 double 型のアニメーション
AnimationController や CurvedAnimation は Animation<double> を継承し、value は double 型の値を返却する。
Tween<T extends Object?> を用いることで 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<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,
);
}
}
【解像度 7】 Listenable を理解する
Flutter のアニメーションはすべて、
Listenable
→ ChangeNotifier
→ AnimationController
という通知の連鎖の上に成り立っている。
AnimatedBuilder や AnimatedWidget は、この通知を UI に反映するための構築パターンにすぎず、アニメーションの本質は 値の時間変化をリッスンして再描画すること と言える。
Listenable
状態の変化を「リスナ(コールバック関数)の call()」を通じて通知するための手段(インターフェース)を提供する。
addListener()removeListener()
Listenable 自体は値を保持しておらず、値の更新通知も行わない。単一責務の原則に従い、これらは下記のクラスが担当するためである。
-
値の保持
-
ValueListenable- 監視対象:
T value
- 監視対象:
-
Animation- 監視対象:
T value
- 監視対象:
-
-
値の更新通知
-
ChangeNotifier- 通知手段:
notifyListener()
- 通知手段:
-
ValueListenable<T>
リスナが監視する対象の T value を保持、提供する。
Animation<T>
リスナが監視する対象の T value、AnimationStatus status を保持、提供する。
Listenable から監視手段である addListener() を継承しているため 2 つの機能を併せ持つ。
- 監視手段を提供する
-
addListener()/removeListener()
-
- 監視対象を保持する
T value
ChangeNotifier(mixin)
監視対象の変更を検知した際に、リスナに通知する mixin。
実際の動作としては、値の変更時にリスナを 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 の具象クラス。
-
監視手段を提供 する
-
addListener()/removeListener()
-
-
監視対象を保持 する
T value
-
value更新時に登録された listener をcall()するnotifyListener()
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(); // 👈
}
}
ValueNotifier は「値(T value)の差し替え」を通知する仕組みであるため、値が mutable な場合でも使用できるが、推奨されない。
例えば List<int> を保持した場合、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()
double 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 が時間的に連続的に変化する点が異なる。
これは内部で Ticker によって Frame ごとに value が更新されるためである。
【解像度 8】 AnimatedBuilder を理解する
ListenableBuilder
インターフェース Listenable を実装した以下の具象クラスを listenable パラメータで受け取り、listenable の通知がある度に builder 関数を実行し、UI を再描画する。
-
abstract Listenableの実装-
mixin ChangeNotifierの実装ValueNotifier
-
abstract Animationの実装AnimationController-
CurvedAnimation-
CurvedAnimationはnotifyListener()を持たない -
notifyListener()はコンストラクタが受け取るAnimationControllerのparentに依存
-
-
これらと ListenableBuilder と組み合わせて使用することで、Listenable のリスナが発火した際、ListenableBuilder を適用した Widget のみが再描画される。
child パラメータはオプションとなっているため、必ず必要なものではない。
child パラメータは、再描画を必要としない Widget が builder に含まれている場合に、Frame の度に再描画されることを避け、パフォーマンスを向上させるために利用される(child に渡したインスタンスが使いまわされる)。
ただし、Animation をリッスンする場合は、読みやすさを向上させるために AnimatedBuilder が推奨されている。
Although they have identical implementations, if an Animation is being listened to, consider using an AnimatedBuilder instead for better readability.
実装は同じですが、Animation をリッスンしている場合は、読みやすさを向上させるために、代わりに AnimatedBuilder を使用することを検討してください。
https://api.flutter.dev/flutter/widgets/ListenableBuilder-class.html
AnimatedBuilder
ListenableBuilder のアニメーション用。
(ListenableBuilder の Listanable listenable は、AnimatedBuilder では Listenable animation となっているが、中の実装は同じものとなっている。)
class AnimatedBuilder extends ListenableBuilder {
const AnimatedBuilder({
required Listenable animation,
}) : super(listenable: animation);
Listenable get animation => super.listenable;
【解像度 9】 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 の参照を再利用するだけで再構築は行われないため、静的要素を child に渡すとパフォーマンスが向上する。
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: controller,
child: const Icon( // 👈
Icons.brush,
size: 100,
),
builder: (context, child) {
// 👈 で渡した Widget が 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,
);
});
}
}
【解像度 10】 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() を呼び出すリスナを自動登録していて、これによってアニメーションが実現する。
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 の builder が巨大化した来た際のリファクタリングの手段でもある。
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
【実践1】 Custom Explicit Animation(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,
);
}
}
【解像度 11】 異なるデータ型を扱う(double, Color)
これまでの実装例では 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);
final sizeTween = Tween<double>(begin: 0, end: 100);
【実践】
ソースコード
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,
),
);
});
}
}
【解像度 12】 TweenAnimationBuilder を理解して、使う
Tween<T extend Object?>
Animatable<T> を継承するクラス。
Animatable の evaluate() はアニメーションの進行度 t(実体は AnimationController の double value = 0.0 ~ 1.0)に対応する T value を返却する。
内部では T transoform() が利用されている。
abstract class Animatable<T> {
T transform(double t);
T evaluate(Animation<double> animation) => transform(animation.value);
}
T transform(double t) は Tween で実装されている。ここで呼ばれるのは lerp() である。
実装を見ると、lerp() は T begin ~ T end の間の、進行度 t に応じた値を計算していることが分ける。
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() で実行されているのは以下の計算。
T lerp(double t) {
return (begin as dynamic) + ((end as dynamic) - (begin as dynamic)) * t as T;
}
begin + (end - begin) * t
上記を以下の条件で計算してみる。
-
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 はこれを内部で利用している(というより、FooTween は、ほぼこれしか仕事をしていない)。
-
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);
}
◯◯Tween の仕事は、変更対象のクラス <T> に定義された lerp() を呼び出すこと
Tween は mutable なオブジェクトだが、変更されない場合には static final で定義しておくと再描画の度に生成されることがない。
static final tween = Tween<double>(begin: 0.0, end: 100.0);
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),
);
}
}
【解像度 13】 Staggered animations
複数の異なるアニメーションが連続して起こるようなアニメーションは Staggered animations と呼ばれる。
- 複数の
Animationを使う-
AnimationではIntervalを指定する
-
AnimationControllerは 1 つ- アニメーションさせるプロパティごとに
Tweenを作成する

https://docs.flutter.dev/ui/animations/staggered-animations
Staggered animations に必要な AnimationController は 1 つ
ParametricCurve<T>(abstract class)
0.0 ~ 1.0 の入力値(進捗 t)を受け取り、補間した出力値を返すインターフェース を持つクラス。
用途はアニメーションに限定されない。
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 を別の 0.0 ~ 1.0 に変換することによって、線形補間(linear curve)を transform(double t) 内で非線形補間(non-linear curve)に変換する役割を持つ。
transformInternal(double t) を override することで、カスタムのロジックを使って非線形補間(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
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
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)
transform(double t) が生成する Curve がたくさん定義されたユーティリィティクラス(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);
}
}
【実践】 Interval を使って 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,
),
),
);
},
);
}
}
【解像度 14】 TweenSequence を理解して、使う
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%
),
]);
【実践】 TweenSequence を使って実装する
これまでは「アニメーションが完了したら逆再生する」といった制御を、
addStatusListener() で AnimationStatus を監視して forward() や reverse() を呼び出す必要があった。
TweenSequence を使うと、1つの AnimationController で往復や段階的な動きを自然に表現できるため、これらのリスナによる制御が不要になる。
ソースコード
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,
);
});
}
}
【解像度 15】 Ticker を理解する
AnimationController のコンストラクタには、必ず TickerProvider オブジェクトを渡さないといけない。
渡された TickerProvider は Ticker を生成して、AnimationController に提供する。
Tikcer はアニメーションに対して「時間のカチカチ(tick)」を提供する。
vsync
AnimationController のコンストラクタ引数。
controller = AnimationController(
vsync: this, // 👈 this が TickerProvider
duration: const Duration(seconds: 2),
);
Vertical Synchronization の略。
画面の Frame とアニメーションの更新を同期させるという意味。
TickerProvider は mixin で State に対してミックスインされる。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 パラメータを意識しておくと良さそう。






















































