🐙

flutter_animateで複数のWidgetを連続してアニメーションさせる

に公開

flutter_animateを使って、複数のWidgetを連続的に動かしてアニメーションを動かすためのノウハウを共有します。

用語の説明

Flutter

Flutterは、Googleが開発したオープンソースのUIソフトウェア開発キットで、Dart言語のフレームワークです。
単一のDart言語のコードからiOS、Android、Web、デスクトップ(Windows、macOS、Linux)向けのネイティブアプリケーションを作成することができます。

Dart

Dartは、GoogleがJavaScriptの問題点を解決するために開発した、オープンソースのオブジェクト指向型のプログラミング言語です。
Javaによく似た構文になっていて、シンプルで学びやすいことが特徴です。

Widget

Flutterアプリケーションの基本的な構成要素のことです。WidgetはUIを構築するためのボタン、テキスト、画像、また配置を指定するためのレイアウトなど、あらゆるUI要素をWidgetとして表現します。

flutter_animate

flutter_animateは、Flutterアプリケーションでアニメーションを簡単に作成および管理するためのパッケージです。
Widgetに拡張メソッドが追加されるため、メソッドチェーンで簡単にアニメーションを追加することができます。

Text("Hello World!").animate().fade().scale()

前提条件

Dart: 3.5.3
Flutter: 3.24.3
flutter_animate: 4.5.2
を使用しています。

flutter_animateの基本的な使い方は、flutter_animateや他の方のブログをご確認ください。

動作のサンプル


今回はこのようなボタンを押すと、3つのWidgetが順番に右に動くアニメーションを作成していきます。
※flutter_animateを利用して当社が作成

実装

準備

  1. flutter_animateをpubspec.yamlに追加する
dependencies:
    ...
    flutter_animate: ^4.5.2
  1. flutter pub getを実行し、ライブラリをインストールする

アニメーションの状態を定義

まず初めに、実行したいアニメーションの状態を定義します。
アニメーションの状態はenum型や定数で定義します。
今回はenum型で作成します。

enum AnimationState {
  init(0),
  first(1),
  second(2),
  third(3),
  finish(4);

  final int value;

  const AnimationState(this.value);

}

initはアニメーションを始める前の状態。
finishはすべてのアニメーションが終了した状態を表しています。
今回は三段階のアニメーションを実行するので、それぞれをfirst, second, thirdで定義しています。
アニメーションの順番を持たせるために、int型valueを持たせています。

次に、アニメーションの状態の遷移先を定義します。

enum AnimationState {
  init(0),
  first(1),
  second(2),
  third(3),
  finish(4);

  final int value;

  const AnimationState(this.value);

  // 追加
  AnimationState next() {
    return switch (this) {
      AnimationState.init => AnimationState.first,
      AnimationState.first => AnimationState.second,
      AnimationState.second => AnimationState.third,
      AnimationState.third => AnimationState.finish,
      AnimationState.finish => AnimationState.finish,
    };
  }
  // ここまで
}

nextメソッドでは、それぞれのアニメーションの状態が次どの状態に遷移すればよいのかを定義しています。
状態finishでは次がないので、次の遷移先もfinishにしています。

アニメーションさせるWidgetを作成

次に上記で作成した状態を使って実際にアニメーションを作成します。

  AnimationState state = AnimationState.init;
  
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            part(color: Colors.red, targetState: AnimationState.first),
            part(color: Colors.yellow, targetState: AnimationState.second),
            part(color: Colors.green, targetState: AnimationState.third),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: startAnimation,
        tooltip: 'start',
        child: const Icon(Icons.play_arrow),
      ), 
    );
  }

  Widget part({
    required Color color,
    required AnimationState targetState,
  }) {
    return Container(
      height: 64,
      width: 64,
      color: color,
    )
        .animate(
            value: state.value > targetState.value ? 1 :null,
            target: state.value == targetState.value
                ? 1
                : state.value > targetState.value
                    ? null
                    : 0,
            onComplete: (controller) {
              if (state == targetState) {
                nextAnimation();
              }
            })
        .moveX(begin:-120, end: 120);
  }

  void nextAnimation() {
    setState(() {
      state = state.next();
    });
  }

  void startAnimation() {
    setState(() {
      state = state == AnimationState.init ? AnimationState.first : AnimationState.init;
    });
  }

AnimationState state = AnimationState.init;

変数stateはアニメーション実行状態を保持しています。
始めはアニメーションを実行しないので、initを指定します。


part(color: Colors.red, targetState: AnimationState.first),

今回はpartメソッドでWidgetを作成しています。
Widget自体は色と自分がアニメーションを実行する状態を指定します。


  Widget part({
    required Color color,
    required AnimationState targetState,
  }) {
    return Container(
      height: 64,
      width: 64,
      color: color,
    )
        .animate(
            value: state.value > targetState.value ? 1 :null,
            target: state.value == targetState.value
                ? 1
                : state.value > targetState.value
                    ? null
                    : 0,
            onComplete: (controller) {
              if (state == targetState) {
                nextAnimation();
              }
            })
        .moveX(begin:-120, end: 120);
  }

次にpartメソッドの中身に移ります。
animateメソッドで、アニメーションの設定を行います。

  • value
    0を初期状態。1を終了状態として、表示するアニメーションの段階を指定します。
    今のアニメーションの状態が、自分のアニメーションの状態より後になっていれば1(終了状態を表示)、そうでなければnull(状態を変更しない)を指定します。
    nullでなく0にすると、後述のstartAnimationでリセットしたとき、アニメーションのせず初期状態に戻ります。

  • target
    0を初期状態。1を終了状態として、指定された値にアニメーションを実行します。

    • 今のアニメーションの状態が、自分のアニメーションの状態と等しければ1(アニメーションを実行)
    • 今のアニメーションの状態が、自分のアニメーションの状態より後であればnull(なにもしない)
    • 今のアニメーションの状態が、自分のアニメーションの状態より前であれば0(valueが0以上であれば逆再生でアニメーションを実行)
  • onComplete
    アニメーションが終了したら呼び出されます。仕様上初期化時にonCompleteが実行されるので、if文で今のアニメーション状態が、自分のアニメーションの状態であることを確認しnextAnimationでアニメーションの状態を次の状態に移します。

moveXはWidgetをX軸方向に移動させるアニメーションです。


void nextAnimation() {
    setState(() {
      state = state.next();
    });
  }

nextAnimationメソッドでは、アニメーションの状態を次の状態に設定して、再描画を行います。


 void startAnimation() {
    setState(() {
      state = state == AnimationState.init ? AnimationState.init.next() : AnimationState.init;
    });
  }

startAnimationメソッドではアニメーションの状態が初期状態initであれば、次の状態に設定してアニメーションを開始し、
init以外であれば、状態をinitにしてアニメーションをリセットします。


floatingActionButton: FloatingActionButton(
    onPressed: startAnimation,
    tooltip: 'start',
    child: const Icon(Icons.play_arrow),
  ), 

startAnimationFloatingActionButtonのタップをトリガーに実行するように設定しています。

これで、ボタンを押すと3つのWidgetが連続してアニメーションを実行し、再度ボタンを押すと初期状態にアニメーションを実行させることができます。

コード全文

import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  AnimationState state = AnimationState.init;

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            part(color: Colors.red, targetState: AnimationState.first),
            part(color: Colors.yellow, targetState: AnimationState.second),
            part(color: Colors.green, targetState: AnimationState.third),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: startAnimation,
        tooltip: 'start',
        child: const Icon(Icons.play_arrow),
      ), 
    );
  }

  Widget part({
    required Color color,
    required AnimationState targetState,
  }) {
    return Container(
      height: 64,
      width: 64,
      color: color,
    )
        .animate(
            value: state.value > targetState.value ? 1 :null,
            target: state.value == targetState.value
                ? 1
                : state.value > targetState.value
                    ? null
                    : 0,
            onComplete: (controller) {
              if (state == targetState) {
                nextAnimation();
              }
            })
        .moveX(begin:-120, end: 120);
  }

  void nextAnimation() {
    setState(() {
      state = state.next();
    });
  }

  void startAnimation() {
    setState(() {
      state = state == AnimationState.init ? AnimationState.init.next() : AnimationState.init;
    });
  }
}

enum AnimationState {
  init(0),
  first(1),
  second(2),
  third(3),
  finish(4);

  final int value;

  const AnimationState(this.value);

  AnimationState next() {
    return switch (this) {
      AnimationState.init => AnimationState.first,
      AnimationState.first => AnimationState.second,
      AnimationState.second => AnimationState.third,
      AnimationState.third => AnimationState.finish,
      AnimationState.finish => AnimationState.finish,
    };
  }
}

おわりに

アニメーションを使いこなすことができれば、アプリの操作説明が不要になったり、表示の流れをスムーズに見せることができたり、よりユーザーに優れたUI/UXを提供することが可能になります。

それを実現するためにflutter_animateは手軽にアニメーションを付けることができておススメのライブラリです。

なお、掲載したソースコードはサンプルになります。本ソースコードを使用することで発生するいかなる損害や不利益について、当社は一切の責任を負いませんので自己の責任においてご利用ください。

Discussion