🦔

【Flutter】Animationの基礎から応用まで ~①基礎編~

2022/11/12に公開

こちらの記事はアニメーションに関するシリーズ記事のvol1となります。

アプリの開発ではUIは当然作らなければなりませんが、アニメーションに関しては無くても機能要件は満たせる為、
「余裕があれば入れたい」と思いつつ、結局入らないなんて事も多いかと思います

しかし効果的なアニメーションを入れる事でアプリの完成度が一気に上がり、同じ機能でもユーザーに与える印象は
ガラッと変わります

今回はそんなアニメーションについて複数記事で基本から応用まで整理していきます

まず初めに

大前提として「Widgetが動く(アニメーションする)」仕組みを明確にしておきましょう

アニメの仕組みにも通じますが、アニメーションとは「パラパラ漫画」です

parapara_manga

ほんの少し位置をずらした静止画を高速で切り替える事によって絵が動いている様に見せています

Widgetのアニメーションも同じです

デバイスには固有のフレームレートが存在し、そのフレームレートに合わせてWidgetを少しずつ変化させながらを高速で再描画する事でWidgetをアニメーションしている様に見せています

つまりアニメーションとは、 「デバイス固有のフレームレートに合わせて、パラパラ漫画のようにWidgetを再描画し続ける操作」 の事です

Animationの基本

アニメーションの主要な登場人物(クラス・Widget)は以下の4つです

  1. AnimationController
  2. Tween
  3. Curve
  4. 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に適用するアニメーションそのもの
  • AnimationControllerTweenCurveを掛け合わせて作る
   _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に適用していきます

  1. StatefulWidgetSingleTickerProvidermixinする
  2. そのクラス自身をAnimationControllerに渡す
  3. 何の値に変換するかと始点と終点の値を決めてTweenを作る
  4. 変化のカーブを変えたいならCurveを適用する
  5. AnimationControllerTweenを掛け合わせてAnimationを作る
  6. それをWidgetに紐付ける
  7. 任意のタイミングで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つあります。

  1. AnimatedBuilderでWidgetをラップする
  2. AnimatedWidgetを継承したWidgetとして切り出す

AnimatedBuilder

AnimatedBuilderは裏でAnimatedWidgetを継承してるので、実際にやっている事は同じなのですが、アニメーションさせたいWidgetをラップしてあげる事でWidgetとして切り出さずにアニメーションさせる事が出来ます。

animationパラメータには再描画のタイミングを通知するクラスを定義します。animationを渡しても良いですし、それを作る時に使ったAnimationControllerでも構いません。

これは元を辿ればAnimationControllerが再描画のタイミングをTickerProvidervsync: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を使い回したい場合はこちらの方法を使うと良いでしょう。

AnimatedBuilderanimationパラメータと同様にlistenableパラメータに再描画のタイミングを通知するクラスを渡します。

ただAnimatedWidgetではAnimationlistenableに渡し、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がアニメーションしている様に見えます。

サンプルコード

https://github.com/heyhey1028/flutter_samples/tree/main/samples/master_animation

ここまで

ここまでがアニメーションの基本になります。この次はもっと複雑なアニメーションに挑戦してみましょう。

https://zenn.dev/heyhey1028/articles/8752d61f522f50

参考

Flutter大学

Discussion