【Flutter】Animationの基礎から応用まで ~③Intervalと複数のTickerProvider~
こちらの記事はアニメーションに関するシリーズ記事のvol3となります。
Interval
と複数のTickerProvider
vol2では2つのユースケースを見ていきましたが、こちらでは残りのユースケースも見ていきましょう
単体のWidgetに複数のアニメーション効果を同時に充てる単体のWidgetに同一のアニメーションを複数回充てる- 単体のWidgetに異なるアニメーションをそれぞれのタイミングで充てる
- 複数のWidgetを連鎖的にアニメーションさせる
- 単体 or 複数のWidgetを別々にアニメーションさせる
Interval
-
単体のWidgetに異なるアニメーションをそれぞれのタイミングで充てる -vol2の記事はTweenSequence
を使って値を変えながら同一のアニメーションを複数回充ててみました
それでは同一のアニメーション効果ではなく、異なるアニメーションを複数回充てるにはどうしたら良いでしょうか?
この時に使えるのがInterval
です
Interval
クラス
通常、AnimationControllerとTweenでAnimationを生成した場合、AnimationControllerのDurationの時間を丸々使ってTweenで定義した開始値から終了値へ値が変化します
Interval
を使う事でAnimationControllerの進み具合(0から1)の間で指定した時間を切り取り、その時間の中でアニメーションを適用します
使い方
begin
)と終了タイミング(end
)を定義
1. 開始タイミング(Interval
では第一引数と第二引数でAnimationControllerの進み具合の開始タイミングと終了タイミングを指定します
AnimationControllerの進み具合は0から1.0のdouble
なので、Interval
にもdouble値で開始タイミングと終了タイミングを指定します
例えばDuration
が4秒の場合、0と0.5を渡せば、0秒から2秒までの間、紐づいたアニメーションを適用します
Interval(
0, // 開始タイミング
0.5, // 終了タイミング
curve: Curves.ease,
)
また第三引数にcurve
を受け取るので変化に効果を加える事もできます
CurvedAnimation
に渡す
2. Interval
クラスを使う際はCurvedAnimation
というクラスを使って、AnimationController
に紐付けます
CurvedAnimation
はアニメーションに対して、curve
を付与する事が出来るクラスですが、主にAnimationController
に対してcurve
を付与すると言う意図で使います
Interval
を渡す事で、その開始タイミングと終了タイミングの間だけを動くAnimationController
に変化します
CurvedAnimation(
parent: controller,
curve: Interval(
0,
0.5,
curve: Curves.ease,
),
)
AnimationController.drive
メソッドを使って、Animation
を生成
3. このInterval
を付与したAnimationController
でdrive
メソッドを使って、Animation
を生成します
Animation = CurvedAnimation(
parent: controller,
curve: Interval(
0,
0.5,
curve: Curves.ease,
),
).drive(Tween);
4. WidgetにAnimationを紐付ける
後は他のアニメーション同様にWidgetに紐づけるだけです
Interval
を付与して生成されたAnimation
クラスは指定された開始時間と終了時間の間のみ、そのアニメーションをWidgetに適用します
と言うことは?
「単体のWidgetに異なるアニメーションをそれぞれのタイミングで充てる」は、Interval
を使って複数のアニメーションにそれぞれの動くタイミングを指定する事で実現する事が出来ます
class _ChainedAnimationState extends State<ChainedAnimation>
with SingleTickerProviderStateMixin {
late AnimationController controller;
late Tween<Alignment> alignTween;
late Tween<double> rotateTween;
late Tween<double> opacityTween;
late Animation<Alignment> alignAnimation;
late Animation<double> rotateAnimation;
late Animation<double> opacityAnimation;
bool animateCompleted = false;
void initState() {
controller =
AnimationController(duration: const Duration(seconds: 4), vsync: this);
// align、rotation, opacityの3つのアニメーション効果を充てていきます
alignTween = Tween(begin: Alignment.topCenter, end: Alignment.center);
rotateTween = Tween(begin: 0, end: pi * 8);
opacityTween = Tween(begin: 1, end: 0);
alignAnimation = CurvedAnimation(
parent: controller,
// 1. Intervalクラスに開始タイミング、終了タイミングを指定
curve: const Interval(0, 0.5, curve: Curves.ease),
).drive(alignTween);
// 2. CurvedAnimationを使って、IntervalをAnimationControllerに付与
rotateAnimation = CurvedAnimation(
parent: controller,
curve: const Interval(0.5, 0.7, curve: Curves.ease),
).drive(rotateTween);
// 3. AnimationController x TweenでAnimationクラスを生成
opacityAnimation = CurvedAnimation(
parent: controller,
curve: const Interval(0.7, 1, curve: Curves.ease),
).drive(opacityTween);
super.initState();
}
void dispose() {
controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Chained Animation'),
),
drawer: const MainDrawer(),
body: AnimatedBuilder(
animation: controller,
builder: (context, _) {
return Opacity(
opacity: opacityAnimation
.value, // <<< 4. Intervalを付与して生成したAnimationをwidgetに紐づける
child: Align(
alignment: alignAnimation.value, // <<< 4.
child: Transform.rotate(
angle: rotateAnimation.value, // <<< 4.
child: const Text('Hello world!'),
),
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
if (!animateCompleted) {
controller.forward().whenComplete(() {
setState(() => animateCompleted = true);
});
return;
}
controller.reverse().whenComplete(
() {
setState(() => animateCompleted = false);
},
);
},
backgroundColor: Colors.yellow[700],
child: const Icon(
Icons.bolt,
color: Colors.black,
),
),
);
}
}
複数のWidgetを連鎖的にアニメーションさせる
下記の様に複数のWidgetをタイミングをずらしながらアニメーションさせる事をStaggered Animationと呼びます
実はこのStaggered AnimationもInterval
を使うと実現できます
どうやって?
結論から言えば、それぞれWidgetが動くタイミングを定義したInterval
を作り、それを使って生成したAnimation
クラスをそれぞれのWidgetに紐付けてあげれば良いのです
紐付けている対象Widgetが違うだけで、やっている事は前述したInterval
の使い方と一緒です
class _StaggeredAnimationState extends State<StaggeredAnimation>
with SingleTickerProviderStateMixin {
late AnimationController controller;
late Tween<Offset> offsetTween;
late Animation<Offset> offsetAnimation1;
late Animation<Offset> offsetAnimation2;
late Animation<Offset> offsetAnimation3;
bool animateCompleted = false;
void initState() {
controller = AnimationController(
duration: const Duration(milliseconds: 1500), vsync: this);
// 3つのWidgetに対して同じ変化の値を付与していくのでTweenを1つ用意
offsetTween = Tween(begin: const Offset(-1000, 0), end: Offset.zero);
offsetAnimation1 = CurvedAnimation(
parent: controller,
// 1. Intervalクラスに開始タイミング、終了タイミングを指定
curve: const Interval(0, 0.3, curve: Curves.ease),
).drive(offsetTween);
// 2. CurvedAnimationを使って、IntervalをAnimationControllerに付与
offsetAnimation2 = CurvedAnimation(
parent: controller,
curve: const Interval(0.3, 0.7, curve: Curves.ease),
).drive(offsetTween);
// 3. AnimationController x TweenでAnimationクラスを生成
offsetAnimation3 = CurvedAnimation(
parent: controller,
curve: const Interval(0.7, 1, curve: Curves.ease),
).drive(offsetTween);
super.initState();
}
void dispose() {
controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Staggered Animation'),
),
drawer: const MainDrawer(),
body: AnimatedBuilder(
animation: controller,
builder: (context, _) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 200, horizontal: 60),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Transform.translate(
offset: offsetAnimation1
.value, // 4. Intervalを付与して生成したAnimationをwidgetに紐づける
child: const Text('Hello world!'),
),
Transform.translate(
offset: offsetAnimation2.value, // 4.
child: const Text('My name is ...'),
),
Transform.translate(
offset: offsetAnimation3.value, // 4.
child: const Text('heyhey1028!!'),
),
],
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
if (!animateCompleted) {
controller.forward().whenComplete(() {
setState(() => animateCompleted = true);
});
return;
}
controller.reverse().whenComplete(
() {
setState(() => animateCompleted = false);
},
);
},
backgroundColor: Colors.yellow[700],
child: const Icon(
Icons.bolt,
color: Colors.black,
),
),
);
}
}
Interval
に定義した開始タイミングと終了タイミングを調整する事でどのくらいの間隔で連鎖的に動くかを調整する事が出来ます
単体 or 複数のWidgetを別々にアニメーションさせる
今までの例では1つのAnimationController
で複数のWidgetもしくはアニメーション効果を操作していました
なのでそのAnimationController
を再生(forward
)する事で、全てのアニメーションが同時に動いていましたが、では複数のアニメーションを別々に動かしたい場合はどうしたら良いでしょうか?
TickerProviderStateMixinを使う
こういった場合にはAnimationController
を複数用意する必要があります
しかし今まで使ってきたSingleTickerProviderStateMixin
では実は複数のAnimationController
を生成する事が出来ません
こういったケースでは複数のTickerProvider
を供給出来るTickerProviderStateMixin
をState
WidgetにMixin
します
Listenable.mergeを使う
またもう1つのポイントとしては、AnimatedBuilder
のanimation
プロパティにListenable.merge
を使って、2つのAnimationController
を合体させたクラスを渡します
今回の様にAnimationController
が複数ある場合、AnimatedBuilder
がそれら全ての状態を監視し、子Widgetを再描画する必要がある為、Listenable.merge
を使ってAnimationController
を1つのクラスにします
// 複数のTickerProviderを供給出来るTickerProviderStateMixinをmixin
class _MultipleTickerProviderState extends State<MultipleTickerProvider>
with TickerProviderStateMixin {
// 別々にアニメーションさせる為、複数のAnimationControllerを用意
late AnimationController alignController;
late AnimationController rotateController;
late TweenSequence<Alignment> alignTween;
late Tween<double> rotateTween;
late Animation<Alignment> alignmAnimation;
late Animation<double> rotateAnimation;
bool animatingAlign = false;
bool animatingRotation = false;
void initState() {
// AnimationControllerそれぞれにdurationとvsyncを定義
rotateController = AnimationController(
duration: const Duration(milliseconds: 1500), vsync: this);
alignController =
AnimationController(duration: const Duration(seconds: 3), vsync: this);
// それぞれのアニメーションのTweenを用意
rotateTween = Tween(begin: 0, end: 8 * pi);
alignTween = TweenSequence<Alignment>(
[
TweenSequenceItem(
tween: Tween(
begin: Alignment.center,
end: Alignment.topCenter,
),
weight: 0.3,
),
TweenSequenceItem(
tween: Tween(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
weight: 0.4,
),
TweenSequenceItem(
tween: Tween(
begin: Alignment.bottomCenter,
end: Alignment.center,
),
weight: 0.3,
),
],
);
// それぞれ別のAnimationControllerを使ってAnimationを生成
alignmAnimation = alignController.drive(alignTween);
rotateAnimation = rotateController.drive(rotateTween);
super.initState();
}
void dispose() {
rotateController.dispose();
alignController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.brown[300],
title: const Text('Multiple Ticker Provider'),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
drawer: const MainDrawer(),
body: AnimatedBuilder(
// 複数のAnimationControllerを監視する為、マージしたクラスへ変換
animation: Listenable.merge([
rotateController,
alignController,
]),
builder: (context, _) {
return Stack(
fit: StackFit.expand,
children: [
Align(
alignment: alignmAnimation.value, // アニメーションをWidgetに適用
child: Transform.rotate(
angle: rotateAnimation.value,
child: const Text('Hello world!!'),
),
)
],
);
},
),
// 複数のAnimationControllerを別々に発火する
floatingActionButton: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () {
if (!animatingAlign) {
alignController.repeat();
setState(() => animatingAlign = true);
return;
}
alignController.stop();
setState(() => animatingAlign = false);
},
heroTag: 'align',
backgroundColor: Colors.yellow[700],
child: const Icon(
Icons.double_arrow,
color: Colors.black,
),
),
const SizedBox(width: 20),
FloatingActionButton(
onPressed: () {
if (!animatingRotation) {
rotateController.repeat();
setState(() => animatingRotation = true);
return;
}
rotateController.stop();
setState(() => animatingRotation = false);
},
heroTag: 'rotate',
backgroundColor: Colors.yellow[700],
child: const Icon(
Icons.cyclone,
color: Colors.black,
),
),
],
),
),
);
}
}
サンプルコード
以上
以上5つの複雑なアニメーションのユースケースを見ていきました。これらを基本として掛け合わせる事で複雑なアニメーションの多くは実装出来るようになると思います。
ここまで見る限り、知らないクラスはあるかもしれませんが、意外とシンプルに感じるかと思います
しかし実際に実装し始めると非常に複雑だと感じる事が多いのもアニメーションです
次ではそんなアニメーションの分かりづらい所について整理していきます
Discussion