【Flutter】Animationの基礎から応用まで ~①基礎編~
こちらの記事はアニメーションに関するシリーズ記事のvol1となります。
アプリの開発ではUIは当然作らなければなりませんが、アニメーションに関しては無くても機能要件は満たせる為、
「余裕があれば入れたい」と思いつつ、結局入らないなんて事も多いかと思います
しかし効果的なアニメーションを入れる事でアプリの完成度が一気に上がり、同じ機能でもユーザーに与える印象は
ガラッと変わります
今回はそんなアニメーションについて複数記事で基本から応用まで整理していきます
まず初めに
大前提として「Widgetが動く(アニメーションする)」仕組みを明確にしておきましょう
アニメの仕組みにも通じますが、アニメーションとは「パラパラ漫画」です
ほんの少し位置をずらした静止画を高速で切り替える事によって絵が動いている様に見せています
Widgetのアニメーションも同じです
デバイスには固有のフレームレートが存在し、そのフレームレートに合わせてWidgetを少しずつ変化させながらを高速で再描画する事でWidgetをアニメーションしている様に見せています
つまりアニメーションとは、 「デバイス固有のフレームレートに合わせて、パラパラ漫画のようにWidgetを再描画し続ける操作」 の事です
Animationの基本
アニメーションの主要な登場人物(クラス・Widget)は以下の4つです
- AnimationController
- Tween
- Curve
- Animation
これ以外にも実際には多くのクラスが関わっていますが、実際にアニメーションを実装していく上で
必ず実装する事になるのはこの4つのクラスです
AnimationController
- アニメーションの進行度を0.0から1.0の
double
型で管理する - この値の中で再生、停止や逆再生などの操作を担当
- アニメーションのアクセルとブレーキ
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1000),
)
Tween
- 始点と終点の型と値を定義
- アニメーションの進行度に合わせて現在の進行度に対応した値をOffsetやColorなど
double
以外の値に変換する - 進行度と値の変換装置
_tween = Tween<Offset>(
begin: const Offset(0, -1000),
end: Offset.zero,
);
Curve
- Tweenで変換された値はデフォルトでは直線的に変化する
- その変化の曲線の形を変える
-
Tween
に対して適用する - 直線的な変化で良ければ
Curve
は使わなくても良い
_tween = Tween<Alignment>(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
).chain(
CurveTween(
curve: Curves.fastLinearToSlowEaseIn,
),
);
Animation
- アニメーションさせたいWidgetに適用するアニメーションそのもの
-
AnimationController
とTween
とCurve
を掛け合わせて作る
_animation = AnimationController(
vsync: this,
duration: const
Duration(milliseconds: 1000),
).drive(
Tween<Alignment>(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
).chain(
CurveTween(
curve:
Curves.fastLinearToSlowEaseIn,
),
),
);
つまり
AnimationController
x Tween
(x Curve
) = Animation
!!!
Animationの流れ
アニメーションは以下のステップでWidgetに適用していきます
-
StatefulWidget
にSingleTickerProvider
をmixin
する - そのクラス自身を
AnimationController
に渡す - 何の値に変換するかと始点と終点の値を決めて
Tween
を作る - 変化のカーブを変えたいなら
Curve
を適用する -
AnimationController
とTween
を掛け合わせてAnimation
を作る - それをWidgetに紐付ける
- 任意のタイミングで
forward
する
import 'package:flutter/material.dart';
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin { // <<< 1. StatefulWidgetにSingleTickerProviderStateMixinをMixinする
late AnimationController controller;
late Tween<Alignment> tween;
final Curve curve = Curves.ease;
late Animation<Alignment> animation;
void initState() {
controller = AnimationController(duration: Duration(seconds: 3),vsync: this); // <<< 2. このクラス自身(this)をAnimationControllerに渡す
tween = Tween(begin: Alignment.topCenter,end: Alignment.bottomCenter); // <<< 3. 何の値に変換するかと始点と終点の値を決めてTweenを作る
tween.chain(CurveTween(curve:curve)); // <<< 4. TweenにCurveを適用して変化の曲線を変える
animation = controller.drive(tween); // <<< 5. AnimationControllerとTweenを掛け合わせてAnimationを作る
super.initState();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter app'),
),
body: AnimatedBuilder(
animation: animation,
builder: (context, _){
return Align(
alignment: animation.value, // <<< 6. Animationを変化させたいWidgetに紐付ける
child: Text('Hello world!'),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
controller.forward(); // <<< 7. forwardメソッドでアニメーションを開始する
},
backgroundColor: Colors.yellow[700],
child: Icon(
Icons.bolt,
color: Colors.black,
),
),
);
}
}
少し解説
SingleTickerProvider
とは?
- 端末固有のフレームレートを教えてくれる
Ticker
クラスをWidgetに渡してくれる - これにより与えられた
Duration
の中でどれくらいの頻度でWidgetを再描画するかが分かる
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
Widgetへの紐付け
AnimationをWidgetに紐づける方法は2つあります。
-
AnimatedBuilder
でWidgetをラップする -
AnimatedWidget
を継承したWidgetとして切り出す
AnimatedBuilder
AnimatedBuilder
は裏でAnimatedWidget
を継承してるので、実際にやっている事は同じなのですが、アニメーションさせたいWidgetをラップしてあげる事でWidgetとして切り出さずにアニメーションさせる事が出来ます。
animation
パラメータには再描画のタイミングを通知するクラスを定義します。animationを渡しても良いですし、それを作る時に使ったAnimationControllerでも構いません。
これは元を辿ればAnimationController
が再描画のタイミングをTickerProvider
をvsync:this
で受け取っているからです。
AnimatedBuilder(
animation: animation, // <<< もしくはAnimationControllerでも良い
builder: (context, _){
return Align(
alignment: animation.value, // <<< 6. Animationを変化させたいWidgetに紐付ける
child: Text('Hello world!'),
);
},
)
AnimatedWidget
一方、AnimatedWidget
ではアニメーションさせたいWidgetにAnimatedWidget
を継承させる事でWidgetとして切り出す事が出来ます。アニメーションするWidgetを使い回したい場合はこちらの方法を使うと良いでしょう。
AnimatedBuilder
のanimation
パラメータと同様にlistenable
パラメータに再描画のタイミングを通知するクラスを渡します。
ただAnimatedWidget
ではAnimation
をlistenable
に渡し、getterを通してしか引数にアクセス出来ないため、AnimationController
ではなくAnimation
を渡さなければならない
class AnimatingText extends AnimatedWidget {
const AnimatingText({
super.key,
required Animation<Alignment> animation,
}) : super(listenable: animation);
Animation<Alignment> get _alignment => listenable as Animation<Alignment>; // <<< lisetenableに一度渡しgetterでアクセスしなければ引数animationにアクセス出来ない
Widget build(BuildContext context) {
return Align(
alignment: _aligment.value, // <<< 直接引数のanimationを渡す事ができない
child: Text('Hello world!'),
);
}
}
どちらの方法でもAnimation.value
が現在の値をWidgetに伝えています
このどちらかの方法でアニメーションさせたいWidgetをラップするもしくは切り出す事で渡したAnimation
のタイミングに合わせて再描画をWidgetに指示します。その結果、Widgetがアニメーションしている様に見えます。
サンプルコード
ここまで
ここまでがアニメーションの基本になります。この次はもっと複雑なアニメーションに挑戦してみましょう。
Discussion