[読書会]アニメーション チュートリアル

目次 index【閲覧時間3分】 click !!>>

1.概要

 ここでは、Flutterのアニメーションライブラリから基本的なクラスを使用して、ウィジェットにアニメーションを追加する方法(例:AnimationController 、Tween など)、AnimatedWidget AnimatedBuilder の使い分けについて触れています。

 また、明示的なアニメーション(explicit animations)の構築方法についても触れています。
初めにアニメーションライブラリの重要な概念、クラス、メソッドを紹介し、その後、5つのアニメーション例を通してさまざまなアニメーションの作り方を段階的に説明します。各例は前の例を基にして、アニメーションライブラリの異なる側面を理解できるように設計されています。

 ビルトインの明示的なアニメーション(FadeTransition、SizeTransition、SlideTransition)は、開始点と終了点を設定するだけで簡単に実装でき、カスタムアニメーションよりも実装が容易です。

2.基本的なアニメーションの概念とクラス

(1)アニメーションのポイント

Animation クラス

 Flutterのアニメーションライブラリの中心的なクラスで、アニメーションの進行に従って変化する値を補間します。
このクラスはアニメーションの現在の状態(開始、停止、前進、逆方向など)を把握します
(が、画面上での表示に関しては直接関与しません。)

AnimationController

 Animationオブジェクトを管理するクラスです。
アニメーションの開始、停止、リセット、逆再生などの制御を行い、時間を使ったアニメーションの進行を管理します。AnimationControllerは必須で、アニメーションの全体的な流れを制御する役割を果たします。

CurvedAnimation

 アニメーションの進行を非線形のカーブで定義するクラスです。
(例えば、アニメーションを「ゆっくり始まり、速くなり、再びゆっくり終わる」等、滑らかな動きにする為に、特定のカーブを適用できます。)

Tween

 補間(interpolation)を定義する為のクラスです。
Tweenは、アニメーションが使用するデータの範囲を補間し、開始値から終了値までの値の変化を定義します
(例えば、赤から青への色の変化や、数値の0から255への変化など、異なるデータ型にも適用可能です。)

ListenersStatusListeners

 アニメーションの状態変化を監視するためのリスナーです。
アニメーションの状態(開始停止完了、等)が変わる度に、これらのリスナーを使って通知を受け取れます

(2)アニメーションシステムの仕組み

 Flutterのアニメーションシステムは型付きの Animation ( Animation<T> )オブジェクトに基づいています。
これにより、ウィジェットは現在のアニメーション値を直接読み取り、その状態の変化をリスニングすることができます。
また、ウィジェットはアニメーションを組み込んで、他のウィジェットにより複雑なアニメーションを提供する基盤として使用することも可能です。

(例えば、AnimationController を使ってアニメーション全体を制御し、CurvedAnimation でカーブを追加し、Tween を使って開始値から終了値までの変化を補間します。こうすることで、アニメーションの動きに滑らかさスムーズな変化をもたらすことができます。)

2-1.Animation<double> クラス

 Animation は、Flutter でよく使用されるアニメーションタイプの一つであり、Animation クラスを double 型で拡張したものです。(このクラスは、アニメーションの対象が具体的に何か(ウィジェットなど)を知らない抽象的な存在で、単に値の変化とその状態(完了しているか停止しているか)についてのみ認識しています。)

(1)Animation オブジェクトの基本的な動作

  • 値の補間
    • Animation オブジェクトは、特定の時間内に二つの値の間で連続的に補間された数値を生成します。
      これにより、開始値から終了値に向けてスムーズに変化するアニメーションが作成されます
  • 変化のパターン
    • Animation の出力は、線形(Linear)だけでなく、カーブ、ステップ関数など、様々なパターンに対応できます
      また、アニメーションの途中逆方向に動かしたり方向を変えることも可能です
  • 他のデータ型にも対応
    • Animation<double> 以外にも、Animation<Color> や Animation<Size> など、double 以外の型を補間することもできます。

(2)Animation オブジェクトの状態

  • 状態管理
    • Animation オブジェクトには常にその現在の値 .value プロパティとして格納されています。
      (これにより、アニメーションの進行状況に応じて現在の値いつでも取得できます。)
  • 表示や描画には関与しない
    • Animation は実際のレンダリングや build() 関数とは無関係です。
      表示や描画に関する情報を持たず、単にアニメーションの進行に必要な値の補間と状態管理に特化しています。

2-2.CurvedAnimation クラス

(1)CurvedAnimation クラスの基本的な使い方

 CurvedAnimation は、アニメーションの進行を非線形のカーブ(曲線)に基づいて制御するクラスです。
(これにより、アニメーションの速度やリズムを、一定速度ではなく、加速や減速を伴う自然な動きにできます。)

CurvedAnimation の基本的な使い方(例):
animation = CurvedAnimation(
  //parent:
  // アニメーションの制御を行う親(通常は AnimationController)を指定します。
  //(親のアニメーションの進行度に基づき、カーブに従った進行度を計算します。)
 parent: controller, 
 //curve: 
 //Curves クラスで定義されているカーブを指定します。
 //(この例では Curves.easeIn が使われており、アニメーションがゆっくりと始まり、
 //徐々に加速する動きを表します。)
 curve: Curves.easeIn
);

(2)定義カーブの種類

 Flutter の Curves クラスには、多くの一般的なカーブが定義されています
これにより、直感的に動きのリズムを設定できます。
(例:Curves.easeOut(加速して終了)、 Curves.bounceOut(弾むような動き)など)

(3)カスタムカーブも作成可能

 自分の用途に合わせたカーブも作成可能です。

カスタムカーブ(例):
import 'dart:math';

//このカスタムカーブ ShakeCurve は、sin 関数を使って繰り返しの振動を表現します。
//(このようにカーブの挙動を自由に設計でき、カスタムアニメーションを実現できます。)
class ShakeCurve extends Curve {
  @override
  double transform(double t) => sin(t * pi * 2);
}

(4)CurvedAnimation AnimationController

 CurvedAnimation AnimationController どちらも Animation 型なので、互いに入れ替えて使用することができます。
CurvedAnimation は、AnimationController の進行をもとに曲線で変換を行うラッパーのような役割を持つため、AnimationController をカーブに従わせるために直接サブクラス化する必要はありません。

(5)実用例

 CurvedAnimation を使用することで、ボタンが押されたときにゆっくり始まって速くなる動き(easeIn)や、急に始まりゆっくりと収束する動き(easeOut)など、より魅力的で自然な動きのアニメーション簡単に追加することが可能です。

2-3.AnimationController オブジェクト

(1)AnimationController オブジェクトの基本的な使い方

 AnimationController は、Flutter のアニメーションの中心的な役割を果たす特殊なアニメーションオブジェクトで、ハードウェアが新しいフレームを準備するたびに新しい値を生成します。通常、指定された時間(例えば 2 秒間)に 0.0 から 1.0 までの数値をリニアに生成します。以下のコードでは AnimationController作成しています(が、まだ再生は開始されていません)。

AnimationController の生成(例):
controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);

//AnimationController を作成しています
//(が、まだ再生は開始されていません)。

(2)AnimationController オブジェクトの特徴

  • AnimationController :
    • AnimationController は Animation から派生しています
      故に Animation オブジェクトが必要な場所ならどこでも使用可能です。)
  • アニメーションの制御メソッド :
    • アニメーションの再生.forward() メソッド開始できます
  • フレームレートと連動 :
    • AnimationController スクリーンリフレッシュレートに合わせて 値(1秒間に約 60 回)を生成します。
  • Listener を使用した監視 :
    • 値が生成される度に、関連付けられたリスナーが呼び出されます
  • RepaintBoundary :
    • 各子ウィジェットの為にカスタムの表示リストを作成する場合に参照します。

(3)vsync 引数

 AnimationController 作成時には vsync 引数必須です。
(これは、画面に表示されていないアニメーションが無駄にリソースを消費しないようにする為です。
vsync には SingleTickerProviderStateMixin を追加した StatefulWidget のオブジェクトを指定することが一般的です。)

(4)注意点

  • AnimationController の範囲は通常 0.0 から 1.0 ですが、いくつかのケースでこの範囲を超えることもあります
    • (例:fling() メソッドを使って速度や力を与える場合、位置が 0.0 から 1.0 の範囲を超えることがあります。)
    • CurvedAnimation を使用する場合も、設定されたカーブによっては範囲を超えることがあります。
      特に Curves.elasticIn のような弾性カーブでは、範囲を大きく超える動きを見せることもあります。

(5)まとめ

 AnimationController は、アニメーションを再生・制御するための基本的なオブジェクトです。
vsync による最適化により、リソース効率を確保しつつリスナーによるリアルタイムの監視も可能です。
また、範囲を超える動きが必要な場合、fling() や CurvedAnimation を使うことで柔軟にアニメーションの表現力を高められます。

2-4.Tween クラス

 Tween を使うことで、0.0 から 1.0 以外の範囲や、色やサイズといった異なるデータ型でアニメーションを実現できます。
アニメーションの状態AnimationController 保持し、Tween はそれを視覚的に表現する為の範囲進行方法定義します。

2-4-1.Tween の基本的な使い方

 (通常、AnimationController オブジェクトの範囲は 0.0 から 1.0 ですが)通常の範囲外やデータ型を使いたい場合 Tween を使用しますTween を使うことで、アニメーションが異なる範囲や異なるデータ型に補間されるよう設定できます。
(例えば、以下のコードでは、Tween を使って -200.0 から 0.0 の範囲でアニメーションを設定しています。)

Tween を使って -200.0 から 0.0 の範囲でアニメーションを設定(例):
tween = Tween<double>(begin: -200, end: 0);

(1)Tween の役割と構造

  • Tweenステートレス
    • Tween 状態を持たないオブジェクトで、begin end のみを設定します。
  • 入力と出力の範囲を定義
    • Tween 主な役割は、入力範囲(通常 0.0~1.0)から出力範囲(例: -200~0等)へのマッピングの定義です。

(2)Animatable<T>の継承

 Tween Animatable<T> を継承しておりAnimation<T> からは継承していません
そのため、Tween は必ずしも double 型を出力する必要はありません。
(例えば、ColorTween を使うことで、2つの色の間を進行させることも可能です。)

ColorTween を使うことで、2つの色の間を進行させることができる(例):
colorTween = ColorTween(begin: Colors.transparent, end: Colors.black54);

(3)Tween の evaluate メソッド

 (Tween オブジェクト自体は状態を保持しない為)アニメーションの現在の値を取得するには evaluate メソッドを使用します
このメソッドは、Animation オブジェクトの現在値(通常範囲 0.0~1.0)を使い、実際のアニメーションの値に変換します。

 evaluate メソッドは、アニメーションの値 0.0 の時に begin を、1.0 の時に end 確実に返すよう管理も行っています

2-4-2.Tween の animateメソッド

(1)animate メソッドの基本的な使い方

 Tween オブジェクトを使用するためには、Tween に対して animate() メソッドを呼び出し、コントローラAnimationController オブジェクト)を渡します。このメソッドを呼び出すことで、Tween コントローラに従って値を生成します。

(例えば、次のコードでは、500ミリ秒の間に 0 から 255 迄の整数値を生成するアニメーションを作成しています。)

500ミリ秒の間に 0 から 255 迄の整数値を生成するアニメーションを作成(例):
AnimationController controller = AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(controller);

(2)animate メソッドの注意点

 animate() メソッドは Animation を返しますが、これは Animatable ではありません。
つまり、Tween.animate を使用することで、Animation 型のオブジェクトが得られることに注意して下さい。

(3)カーブと Tween を組み合わせる例

 以下の例では、AnimationController カーブ(CurvedAnimation)を使用し、よりスムーズな動きにする為の easeOut カーブを追加しています。このカーブは、アニメーションの進行をコントローラで指定した範囲で非線形に補間します。

カーブと Tween を組み合わせる(例):
//AnimationController:
//アニメーションの時間や制御を行うための基本的なコントローラです。
//(ここでは、500ミリ秒の間にアニメーションが完了します。)
AnimationController controller = AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this
);

//CurvedAnimation:
//カーブを使用して非線形の進行を追加します。
//(ここでは、easeOut カーブを使いアニメーションの速度が後半で緩やかになります。)
final Animation<double> curve =
    CurvedAnimation(parent: controller, curve: Curves.easeOut);
    
//IntTween:
//指定範囲(0~255)で整数値を補間します。
//animate()メソッド:
//カーブ付きコントローラに接続させます。
//(結果)Animation<int>オブジェクトは、アニメーション中に整数の変化を提供します。
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(curve);

 この構成により、アニメーションの開始と終了の値や、進行の速さ・遅さカスタマイズすることができます。
CurvedAnimation によってアニメーションが滑らかで自然に見える効果が得られます。

2-5.アニメーション通知(Animation notifications)

(1)2種類のリスナー(Listener と StatusListener)

 Animation オブジェクトには、Listener StatusListener 2種類のリスナー追加することができます。
これらは各々 addListener() と addStatusListener() メソッドを使って設定します。

  1. Listener
    • addListener() メソッドを使って設定します。
      アニメーションの値が変わる度に呼び出される関数指定できます。
      最も一般的な使い方としては、setState() を呼び出してウィジェットの再描画(リビルド)を行います。
      これにより、アニメーションの進行に応じてUIが更新されます
  2. StatusListener
    • addStatusListener() メソッドを使って設定します。
      アニメーションの状態が変わる度に呼び出されます
      状態AnimationStatus 定義されています。
      (例えばアニメーションの開始時、終了時、前進時、逆向時、等に対応しています。)

(2)使用例

 次のセクションには、addListener() メソッドの使用例が紹介されています。
また、「Monitoring the progress of the animation」章では、addStatusListener() の使用例が示されています。

 この様に、リスナーを使うことでアニメーションの進行を監視し、アニメーションの途中や終了時などに特定のアクションを実行できる様になります。

3.アニメーションの例

 ここでは、5 つのアニメーションの例を紹介します。
(各章には、その例のソース コードへのリンクが用意されています。)

3-1.アニメーションのレンダリング

 以下の様なアニメーションに関するポイントを理解することで、Flutterのアニメーション機能の利用による、動的で視覚的に魅力的なアプリを作成できる様になります。

3-1-1.ポイント

(1)基本的なアニメーションの追加

 addListener()setState() を使い、ウィジェットに基本的なアニメーションを追加する方法について説明しています。

 Animation が新しい数値を生成する度に addListener() が呼び出され、その中で setState() を実行することで、UIの再描画が行われ、アニメーションの動きを表現します

(2)AnimationController と vsync パラメータ

 AnimationController 定義時vsync パラメータが必要です。
vsync により、アニメーションが画面に描画されていない時に無駄なリソース消費を防ぎます
通常、SingleTickerProviderStateMixin を使って自分のクラスを vsync に設定します。)

(3)Dartのカスケード記法(..記法)

 .. Dart のカスケード記法と呼ばれ、同じオブジェクトに対して連続してメソッドを呼び出す際に便利です。
(例えば ..addListener と記述することで、addListener() メソッドを続けて呼び出せます。)

(4)プライベートクラスの定義

 クラス名をアンダースコア(_)で始めると、そのクラスはプライベート扱いになります。
(他のファイルや外部から直接アクセスできない様にするための Dart 言語の仕様です。)

(5)アニメーションオブジェクトの描画

 これまでの説明では、アニメーションで生成された数値をシーケンスとして利用する方法に焦点が置かれています。
 実際に画面にアニメーションを描画するためには、ウィジェットのメンバーとして Animation オブジェクトを保持し、その value プロパティを使って描画内容を決定します
これにより、アニメーションに合わせてウィジェットのプロパティ(位置、大きさ、色など)を動的に変化させられます。

3-1-2.アニメーションの実装について

(1)Flutter ロゴを普通に描画する基本的なアプリコード(アニメーション未適用)

 このサンプルコードは、アニメーションを適用せずに、Flutterロゴを描画する基本的なアプリのコードです。

アニメーションを適用せずに、Flutterロゴを描画する基本的なアプリのコード(例):
import 'package:flutter/material.dart';

void main() => runApp(const LogoApp());

class LogoApp extends StatefulWidget {
  const LogoApp({super.key});

  @override
  State<LogoApp> createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        margin: const EdgeInsets.symmetric(vertical: 10),
        height: 300,
        width: 300,
        child: const FlutterLogo(),
      ),
    );
  }
}

(2)ロゴを何もない状態からフルサイズまでアニメーションさせる(アニメーション適用)

 上記(1)のコードではロゴのサイズが固定されていましたが、以下のコードではアニメーションを通じてサイズが徐々に大きくなるように設定されています。

アニメーションを通じてサイズが徐々に大きくなる設定(例):
//このコードではアニメーションを通じてサイズが徐々に大きくなるように設定されています。
import 'package:flutter/material.dart';
void main() => runApp(const LogoApp());

class LogoApp extends StatefulWidget {
  const LogoApp({super.key});

  @override
  State<LogoApp> createState() => _LogoAppState();
}

//修正↓ class _LogoAppState extends State<LogoApp> {
// 1. SingleTickerProviderStateMixinの追加
// ( このミックスインを追加することで、AnimationControllerにvsyncを提供できます。
//   vsyncは、アニメーションが画面外で実行されることを防ぎ、効率的にリソースを管理します。 )
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  //追加↓
  // 2. AnimationControllerとAnimationの定義
  // ( AnimationControllerはアニメーションの進行を制御し、Animationオブジェクトはアニメーションの状態を保持します。
  //   この例では、Animation<double>を使って、ロゴのサイズを管理しています。 )
  late AnimationController controller;
  late Animation<double> animation;

  //追加↓
  // 3. initStateメソッドのオーバーライド
  @override
  void initState() {
    super.initState();

    // 4. AnimationControllerの初期化
    controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 2),
    );

    // 5. Tween と animate() メソッドを使って Animation を初期化
    animation = Tween<double>(begin: 0.0, end: 300.0).animate(controller);

    // 6. addListener で setState を呼び出し、アニメーション進行に合わせてウィジェットを再描画する
    animation.addListener(() {
      setState(() {
        // アニメーションが進行するたびに再描画
      });
    });

    // 7. アニメーションの開始
    controller.forward();
  }

  //追加↓
  // 8. disposeメソッドのオーバーライド
  @override
  void dispose() {
    // AnimationController を解放
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        margin: const EdgeInsets.symmetric(vertical: 10),
        //修正↓ height: 300, width: 300,
        // 9. アニメーションの値を使ってロゴのサイズを更新
        height: animation.value,
        width: animation.value,
        //color: Colors.blue,
        child: const FlutterLogo(),
      ),
    );
  }
}

3-1-3.アニメーション実装におけるポイント

 以下、Flutterでアニメーションを実装する際のポイントや、Dartの言語機能(カスケード記法)についてです。

(1)アニメーションの仕組みとaddListener()

  • addListener() メソッド setState() を呼び出している為、アニメーションが新しい値を生成する度に、現在のフレームが「汚れた状態(再描画が必要な状態)」とマークされます。
  • 上記により、build() メソッドが再度呼ばれるため、build() 内での Containerのサイズがアニメーションの値に基づいて動的に変化します。
    (ここでは height と width が animation.value を使って設定されている為、アニメーションの進行に応じてサイズが変わる様になります。)

(2)メモリリーク防止

 State オブジェクトの破棄時には、dispose()メソッドで controller を解放する必要があります
(これを行わないと、メモリリークが発生し、アプリのパフォーマンスに悪影響を及ぼします。)

(3)最初のFlutterアニメーションの作成

 上記の変更を加えるだけで、Flutterで基本的なアニメーションが実現できました。
アニメーション Tween によって定義され、controller が進行状況を管理しています。
この基本的な仕組みを応用することで、様々なアニメーションを実装できます。

(4)Dartのカスケード記法

 カスケード記法(..)は Dart 言語の便利な機能で、メソッドチェーンを簡潔に書くために使います。

 以下は通常のメソッドチェーンのコードです。

通常のメソッドチェーン(例):
animation = Tween<double>(begin: 0, end: 300).animate(controller);
animation.addListener(() {
    // ···
});

 上記を、カスケード記法(..)により簡潔に書くと以下の様になります。

カスケード記法(..)により簡潔に書かれたメソッドチェーン(例):
animation = Tween<double>(begin: 0, end: 300).animate(controller)
  ..addListener(() {
    // ···
  });

 このカスケード記法により、animate(controller)の返り値に対してさらにaddListener()を呼び出すことができます。
これによりコードが短く、読み易くなります。

3-2.AnimatedWidget によるアニメーションの簡略化

 AnimatedWidget ヘルパークラスを使用すると、addListener() setState() を使わずに、アニメーションを持つウィジェットを作成できます
(このクラスを使うことで、アニメーションコードがより簡潔になり、addListener() で毎回 setState() を呼び出す手間が省けます。)

3-2-1.AnimatedWidgetの使用方法

  • AnimatedWidget を使うとアニメーションを実行するウィジェット再利用可能な形作成できます
    (例えば、同じアニメーション効果を異なるウィジェットに適用したい場合に便利です。)
  • AnimatedWidget は、Animationオブジェクトプロパティとして受け取りその値を基にウィジェットの描画を制御します。

3-2-2.AnimatedWidget の活用によるアニメーションとウィジェットの分離

 AnimatedWidget 基底クラスを利用することで、ウィジェットのコア部分アニメーションの処理部分分離できます。AnimatedWidgetを使うと、State オブジェクトでアニメーションを保持する必要がなくなり、コードが簡潔になります

 AnimatedBuilder は、アニメーションの更新部分(具体的には、Animation オブジェクトの値の変更)を、ウィジェットのビルドから分離するために使われます。アニメーションの更新部分ウィジェットのビルドの部分を分離することで、コードが更に整理されます。
(この分離により、アニメーションが効率的に行われ、ウィジェットツリー全体のリビルドを避け、最小限の範囲で更新できるようになります。)

 整理すると・・・

  • AnimatedBuilder の役割
    • Animationの更新を監視関連する一部のウィジェットのみを再構築します。
      (これにより、無駄なリビルドが減り、効率的にアニメーションが実行されます。)
  • 使いどころ
    • 別のウィジェットにネスト(埋め込み)して使用する場合や、複数の部分を効率的にアニメーション化したい場合に適しています。

3-2-3.Flutter APIにおけるAnimatedWidgetの例

3-2-3-2.AnimatedWidget をベースにしたアニメーションウィジェット

 Flutter APIには、AnimatedWidget をベースにしたいくつかのアニメーションウィジェットが提供されています。
(これらのウィジェットを使うことで、特定のアニメーション効果簡単に実装できます。以下はその例です。)

(1)AnimatedBuilder

 ウィジェットの更新を効率的に行うためのビルダー

(2)AnimatedModalBarrier

 モーダルバリアの透明度アニメーション化

(3)DecoratedBoxTransition

 DecoratedBox ウィジェットの装飾アニメーション化

(4)FadeTransition

 ウィジェットの透明度(フェード)アニメーション化

(5)PositionedTransition

 子ウィジェットの位置アニメーション化

(6)RelativePositionedTransition

 親ウィジェットに対する相対的な位置アニメーション化

(7)RotationTransition

 ウィジェットの回転アニメーション化

(8)ScaleTransition

 ウィジェットのスケール(大きさ)アニメーション化

(9)SizeTransition

 ウィジェットのサイズアニメーション化

(10)SlideTransition

 ウィジェットの位置スライドでアニメーション化

3-2-3-3.AnimatedLogoクラス

 以下のクラス(ここでは AnimatedLog() )は、AnimatedWidget を拡張して作られています
(このクラスでは、アニメーションの現在の値を利用してロゴを描画します。)

AnimatedWidgetの活用により分離されたアニメーション実行部分(例):
class AnimatedLogo extends AnimatedWidget {
  const AnimatedLogo({super.key, required Animation<double> animation})
      : super(listenable: animation);

  @override
  Widget build(BuildContext context) {
    final animation = listenable as Animation<double>;
    return Center(
      child: Container(
        margin: const EdgeInsets.symmetric(vertical: 10),
        height: animation.value,
        width: animation.value,
        child: const FlutterLogo(),
      ),
    );
  }
}
(1)アニメーション管理の分離

 AnimationController や Tween の設定は、LogoApp クラスで引き続き管理されます。
そしてAnimatedLogo クラスに Animation オブジェクトを渡すことで、アニメーションの進行に合わせ自動的に描画が更新されます

AnimatedWidgetの活用によるアニメーション実行部分の分離(例):
//AnimatedWidgetの活用によるアニメーション実行部分の分離
// ( このように、AnimatedWidgetを使うことでアニメーションの処理が簡潔になり、
//   再利用可能なアニメーションウィジェットを作成できます。 )

import 'package:flutter/material.dart';

void main() => runApp(const LogoApp());

// 1. AnimatedWidget 継承による アニメーションコードの分離
// ( AnimatedWidget基底クラスを利用することで、
//   ウィジェットのコア部分とアニメーションの処理部分を分離できます。
//   AnimatedWidgetを使うと、Stateオブジェクトでアニメーションを保持する必要が
//   なくなり、コードが簡潔になります。 )
class AnimatedLogo extends AnimatedWidget {
  const AnimatedLogo({
    super.key, 
    required Animation<double> animation

    //( listenableプロパティ( Animationオブジェクト )を利用して、
    //  ウィジェットのサイズ( heightとwidth )をアニメーションの現在の値に基づいて設定しています。
    //  この方法により、addListener() や setState() を直接使用する必要がありません。 )
  }) : super(listenable: animation);

  @override
  Widget build(BuildContext context) {
    final animation = listenable as Animation<double>;
    return Center(
      child: Container(
        margin: const EdgeInsets.symmetric(vertical: 10),
        height: animation.value,
        width: animation.value,
        child: const FlutterLogo(),
      ),
    );
  }
}

class LogoApp extends StatefulWidget {
  const LogoApp({super.key});

  @override
  State<LogoApp> createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  late Animation<double> animation;
  late AnimationController controller;

  @override
  void initState() {
    super.initState();

    // ( AnimationController や Tween の設定は、元の(LogoApp)クラスで引き続き管理されます。 )
    controller = AnimationController(
      duration: const Duration(seconds: 2), vsync: this
    );
    animation = Tween<double>(begin: 0, end: 300).animate(controller);

    controller.forward();
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  // ( 再利用可能な形で新規追加したアニメーション実行部分( AnimatedLogo クラス )に
  //   Animationオブジェクトを渡すことで、アニメーションの進行に合わせて自動的に描画が更新されます。 )
  @override
  Widget build(BuildContext context) => AnimatedLogo(animation: animation);

}

3-3.アニメーション進行状況の監視

 ここでは、アニメーションの状態の変化を監視する方法について解説します。

3-3-1.ポイント

(1)addStatusListener()の使用

 addStatusListener()メソッドを使うことで、アニメーションの状態に関する通知を受け取ることができます。
(具体的には、アニメーションが開始、停止、または逆方向に進む時に、このリスナーが呼ばれます。)

(2)無限ループでのアニメーションの実行(例1)

 アニメーションが完了したり、開始状態に戻った場合に方向を逆転させることで、アニメーションを無限ループで実行することができます。これにより、ユーザに継続的な動きを提供することが可能です。

(以下では、AnimatedBuilder を使っているので、 ..addListener 実装は不要です。)

アニメーションの状態を監視し、無限にループさせるための基本的な実装(例1):以下では、AnimatedBuilder を使っているので、 ..addListener 実装は不要です。
// アニメーションの状態を監視し、無限にループさせるための基本的な実装例

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Infinite Animation Demo',
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Infinite Animation Demo'),
        ),
        body: const Center(
          child: MyAnimation(),
        ),
      ),
    );
  }
}

class MyAnimation extends StatefulWidget {
  const MyAnimation({super.key});

  @override
  MyAnimationState createState() => MyAnimationState();
}

class MyAnimationState extends State<MyAnimation> with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    );

    // addStatusListener():
    // ( Animationの状態を監視するリスナーを追加 )
    // ( addStatusListener()メソッドを使うことで、
    //   アニメーションの状態に関する通知を受け取ることができます。
    //   具体的には、アニメーションが開始、停止、または逆方向に進む時に、
    //   このリスナーが呼ばれます。 )
    _controller.addStatusListener((status) {
      // 無限ループでのアニメーションの実行
      // ( アニメーションが完了したり、開始状態に戻った場合に方向を逆転させることで、
      //   アニメーションを無限ループで実行することができます。
      //   これにより、ユーザーに継続的な動きを提供することが可能です。 )
      if (status == AnimationStatus.completed) {
        // アニメーションが完了した場合、逆方向に再生
        _controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        // アニメーションが開始状態に戻った場合、再度前進
        _controller.forward();
      }
    });

    // アニメーションを開始
    _controller.forward();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return Container(
          // アニメーションに応じて高さを変更
          height: _controller.value * 100,
          width: 100,
          color: Colors.blue,
        );
      },
    );
  }
}

3-3-2.無限ループでのアニメーションの実行(例2)

 (以下では、AnimatedBuilder を使っていないので、 アニメーション変更によるビルドの為には、..addListener 実装が必要です。)

以下では、AnimatedBuilder を使っていないので、 アニメーション変更によるビルドの為には、..addListener 実装が必要です。(例2(animate3参照)):
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

// アプリ全体のエントリーポイントとして、MaterialApp をラップする MyApp クラス
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Infinite Animation Demo',
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Infinite Animation Demo'),
        ),
        body: const Center(
          child: MyAnimation(), // アニメーションウィジェットをここで表示
        ),
      ),
    );
  }
}

// アニメーションを制御する StatefulWidget
class MyAnimation extends StatefulWidget {
  const MyAnimation({super.key});

  @override
  State<MyAnimation> createState() => _MyAnimationState();
}

class _MyAnimationState extends State<MyAnimation> with SingleTickerProviderStateMixin {
  late Animation<double> animation;
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    );
    animation = Tween<double>(begin: 50, end: 500).animate(controller)
      ..addStatusListener((status) {
        // アニメーションが終了したら逆再生、逆再生が終了したら再び再生
        if (status == AnimationStatus.completed) {
          controller.reverse();
        } else if (status == AnimationStatus.dismissed) {
          controller.forward();
        }
      })

      // アニメーションの変化に伴う再ビルド!
      // ( build 内の return で、(Center ではなく) 
      //   AnimatedBuilder を使用していれば、
      //   この ..addListener と setState は不要となります! )
      ..addListener(() {
        setState(() {});
      });
      
    controller.forward();
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        margin: const EdgeInsets.symmetric(vertical: 10),
        height: animation.value,
        width: animation.value,
        child: const FlutterLogo(),
      ),
    );
  }
}

3-4.AnimatedBuilder によるリファクタリング

3-4-1.概要

(1)目的

① (アニメーション進行に応じたビルダー関数の)再描画の仕組みの提供

 AnimatedBuilder は、アニメーションの進行に応じてビルダー関数(builder)を再描画する仕組みを提供します。
(しかしウィジェットの描画やアニメーションオブジェクトの設定には直接関与しません。)

 アニメーションの値を使って実際に何を描画するか、どのように描画するかは、builder に渡された関数内で定義します。
つまり、AnimatedBuilder は「アニメーションの状態変化に合わせてビルダーを再構築する」という責任はあるものの、「何をどのように描画するか(具体的なUI)」には関与しないしません。

② 他のウィジェットの build メソッドにネスト(埋め込んで使用)できる

 他のウィジェットの build メソッドの一部として(アニメーションを)記述 したい場合AnimatedBuilder を使用します。
(逆に、再利用可能なアニメーションを持つ単一のウィジェットを定義したい場合、AnimatedWidget を使用する方が適しています(詳細は後述の「Simplifying with AnimatedWidget」セクションを参照)。)

(2)AnimatedBuilder が埋め込まれている標準ウィジェット

 AnimatedBuilder は、いくつかの Flutter 標準ウィジェットにも組み込まれています
(これらのウィジェットは、アニメーションの適用や状態変化において効率的に動作します。)
 以下は、AnimatedBuilder を利用している代表的なウィジェットの例です。

① BottomSheet(ボトムシート)
② ExpansionTile(展開タイル)
③ PopupMenu(ポップアップメニュー)
④ ProgressIndicator(プログレスインジケーター)
⑤ RefreshIndicator(リフレッシュインジケーター)
⑥ Scaffold(画面の基本構造を提供するウィジェット)
⓻ SnackBar(スナックバー)
⑧ TabBar(タブバー)
⑨ TextField(テキストフィールド)

(3)概要のまとめ

  • AnimatedBuilder により、アニメーションUI の分離が可能となり、より効率的にトランジションを管理できます。
  • アニメーションを他のウィジェットに埋め込む(ネストする)際、アニメーション値を利用して画面を更新する必要がある場合に非常に便利です。
  • AnimatedBuilder により、アニメーションの為のコードが簡略化され、他ビルダーやウィジェットと連携し易くなります。

3-4-2.AnimatedBuilder を使ったリファクタリングの課題と解決策

(1)課題

 animate3 のコードには、アニメーションを変更するたびにロゴをレンダリングするウィジェットそのものを変更する必要がある、という問題がありました。これは、責任が1つのクラスに集中してしまい、アニメーションの変更が複雑になる原因となっています。

(2)改善の為の分離

 この問題を解決する為に、各役割を別々のクラスに分割するのが推奨されます。
具体的には、次の様な分離を行います。

① ロゴのレンダリング

 ロゴ自体の表示に専念するクラスを作成します。

② アニメーションオブジェクトの定義

 アニメーションの設定や制御は専用のクラスで行い、他の部分と独立させます。

③ トランジションのレンダリング

 トランジション(アニメーションの動き)を管理するクラスを用意し、ロゴのレンダリングと分離します。

(3)解決策としての AnimatedBuilder の利用

 AnimatedBuilder を使用すると、アニメーションの責任分離がより簡単に実現できます。
AnimatedBuilder はレンダーツリー内で独立したクラスとして動作し、Animation オブジェクトからの通知を自動的にリッスンします。また、アニメーションに変更があるとウィジェットツリーを「汚れた状態(更新が必要な状態)」に自動でマークしてくれるため、addListener() を明示的に呼び出す必要がありません

 以下のサンプルでは、ざっくりと次の様にモジュール分割しています。

  1. Container
    • 最も外側のコンテナで、他のウィジェットを包含します。
  2. GrowTransition
    • アニメーションのトランジションを管理するウィジェット。
      AnimatedBuilder を利用して、ロゴのサイズを成長させるアニメーションを実現します。
  3. AnimatedBuilder
    • GrowTransition 内で使用され、アニメーションの進行に合わせて子ウィジェットを更新します。
  4. AnonymousBuilder
    • AnimatedBuilder が生成する匿名のビルダー。
      これにより、アニメーションの値に応じた動的な UI 更新が行われます。
  5. LogoWidget
    • 実際にロゴ(Flutter ロゴ)を表示するためのウィジェットです。
      アニメーションの影響を受けてサイズや位置が変更されますが、ロゴ自体の表示機能に専念しています。

 この分離によって、コードがよりモジュール化され、変更に強い構造になります。
(例えば、アニメーションの詳細やトランジションの変更を GrowTransition 内で行うことができ、ロゴのレンダリング部分である LogoWidget には影響を与えません。)
 このように AnimatedBuilder を活用すると、アニメーションのロジックと UI 表示のロジックが分離され、保守性と再利用性が向上します。

AnimatedBuilder を使用すると、アニメーションの責任分離できる(例):
import 'package:flutter/material.dart';

void main() => runApp(const LogoApp());

class LogoApp extends StatefulWidget {
  const LogoApp({super.key});

  @override
  State<LogoApp> createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  late Animation<double> animation;
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 0, end: 300).animate(controller);
    controller.forward();
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return GrowTransition(
      animation: animation,
      child: const LogoWidget(),
    );
  }
}

//(2) GrowTransition :
// ( アニメーションのトランジションを管理するウィジェット。
//   AnimatedBuilder を利用して、ロゴのサイズを成長させるアニメーションを実現します。 )
class GrowTransition extends StatelessWidget {
  const GrowTransition({
    required this.child,
    required this.animation,
    super.key,
  });

  final Widget child;
  final Animation<double> animation;

  @override
  Widget build(BuildContext context) {
    return Center(
      //(3) AnimatedBuilder() :
      // ( GrowTransition 内で使用され、アニメーションの進行に合わせて子ウィジェットを更新します。 )
      child: AnimatedBuilder(
        animation: animation,
        //(4) ( AnonymousBuilder ) :
        // ( AnimatedBuilder の builder プロパティに渡される匿名関数です。
        //   アニメーションに応じてウィジェットを更新する役割を持っています。 )
        builder: (context, child) {
          return SizedBox(
            height: animation.value,
            width: animation.value,
            child: child,
          );
        },
        child: child,
      ),
    );
  }
}

//(5) LogoWidget() :
// ( 実際にロゴ(Flutter ロゴ)を表示するためのウィジェットです。
//   アニメーションの影響を受けてサイズや位置が変更されますが、ロゴ自体の表示機能に専念しています。 )
class LogoWidget extends StatelessWidget {
  const LogoWidget({super.key});

  @override
  Widget build(BuildContext context) {
    //(1) Container() :
    // ( 最も外側のコンテナで、他のウィジェットを包含します )
    return Container(
      margin: const EdgeInsets.symmetric(vertical: 10),
      //FlutterLogo() :
      // ( Flutter SDK に組み込まれているウィジェット(Flutterの公式ロゴを表示します) )
      child: const FlutterLogo(),
    );
  }
}

3-3.同時アニメーション

 この説明では、「同時に複数のアニメーションを実行する方法」について解説しています。
(具体的には、サイズを変えるアニメーションと、透明度(opacity)を変えるアニメーション同時に行う方法を示しています。)

 以降では、次の修正前コード(animate3)を元に、修正等を行っていく流れになっています。

修正前コード(animate3):
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

void main() => runApp(const LogoApp());

class AnimatedLogo extends AnimatedWidget {
  const AnimatedLogo({super.key, required Animation<double> animation})
      : super(listenable: animation);

  @override
  Widget build(BuildContext context) {
    final animation = listenable as Animation<double>;
    return Center(
      child: Container(
        margin: const EdgeInsets.symmetric(vertical: 10),
        height: animation.value,
        width: animation.value,
        child: const FlutterLogo(),
      ),
    );
  }
}

class LogoApp extends StatefulWidget {
  const LogoApp({super.key});

  @override
  State<LogoApp> createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  late Animation<double> animation;
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 0, end: 300).animate(controller)
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          controller.reverse();
        } else if (status == AnimationStatus.dismissed) {
          controller.forward();
        }
      })

      ..addStatusListener((status) {
        if (kDebugMode) {
          print('$status');
        }
      });
    controller.forward();
  }

  @override
  Widget build(BuildContext context) => AnimatedLogo(animation: animation);

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
}

以下は、変更後のサンプルコード(大きさ、色が同時に変更されるアニメーション(+無限ループ))です。

変更後のサンプルコード(大きさ、色が同時に変更されるアニメーション(+無限ループ))(例):
import 'package:flutter/material.dart';

void main() => runApp(const LogoApp());

//ロゴのサイズと透明度のアニメーションを同時に実行します。
class AnimatedLogo extends AnimatedWidget {
  const AnimatedLogo({super.key, required Animation<double> animation})
      : super(listenable: animation);

  //透明度を制御するTween: 透明(0.1)から不透明(1.0)まで
  static final _opacityTween = Tween<double>(begin: 0.1, end: 1);

  //サイズを制御するTween: 小さい(0)から大きい(300)まで
  static final _sizeTween = Tween<double>(begin: 0, end: 300);

  @override
  Widget build(BuildContext context) {
    final animation = listenable as Animation<double>;
    return Center(
      child: Opacity(
        //透明度をアニメーションの現在の値で評価
        opacity: _opacityTween.evaluate(animation),
        child: Container(
          margin: const EdgeInsets.symmetric(vertical: 10),
          //サイズをアニメーションの現在の値で評価
          height: _sizeTween.evaluate(animation),
          width: _sizeTween.evaluate(animation),
          child: const FlutterLogo(), //アニメーション対象のFlutterロゴ
        ),
      ),
    );
  }
}

class LogoApp extends StatefulWidget {
  const LogoApp({super.key});

  @override
  State<LogoApp> createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  late Animation<double> animation;
  late AnimationController controller;

  @override
  void initState() {
    super.initState();
    //AnimationControllerの初期化: アニメーションを2秒間で完了
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);

    //カーブ付アニメーション(CurvedAnimation):アニメーション進行を非線形にする
    animation = CurvedAnimation(
      parent: controller, curve: Curves.easeIn
    )
      ..addStatusListener((status) {
        //アニメーションの状態が変更されるたびに呼び出されるリスナー
        if (status == AnimationStatus.completed) {
          //アニメーションが完了したら逆再生
          controller.reverse();
        } else if (status == AnimationStatus.dismissed) {
          //アニメーションが最初の状態に戻ったら再度前進
          controller.forward();
        }
      });
    //アニメーションを開始
    controller.forward();
  }

  @override
  void dispose() {
    //メモリリークを防ぐためにコントローラを破棄
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => AnimatedLogo(animation: animation);
  //AnimatedLogoウィジェットにアニメーションを渡して、ロゴのアニメーションを開始
}

(1)Curvesクラスを使ってアニメーションのカーブを指定

 Curves クラスには、よく使われる様々なアニメーションカーブ(例: easeIn, easeOut, bounce など)が定義されています。
これらのカーブを CurvedAnimation 組み合わせることで、アニメーションの進行具合を調整し、より滑らかでリアルな動きを作ることができます。

(2)前の例(animate3)を元にした例

 この例では、animate3(アニメーションの進行状況を監視する例)を基にしています。
この例では、サイズが小さくなったり大きくなったりを繰り返すアニメーションを AnimatedWidget で実装していました。

 新しい例では、これに加えて透明度もアニメーションさせ、オブジェクトが透明から不透明へと変わる動きを追加します。

(3)複数のTweenを同じAnimationControllerで使用

 この例では、1つの AnimationController を使って複数の Tween(アニメーション範囲)制御します。
具体的には、1つの Tweenサイズの変化を、もう1つTween 透明度の変化を扱います。

ポイント:
同じ AnimationController 共有することで、複数のプロパティを同期してアニメーションさせることができます。
サイズと透明度が同じタイミングで変わるため、見た目に一貫性が出ます

(4)注意点

 この例は説明用であり、実際のコードでは FadeTransition SizeTransition を使うことが推奨されます。
これらのウィジェットはそれぞれ透明度サイズの変化に特化しているため、コードが簡潔で読み易くなります。
(例えば、FadeTransition 透明度のアニメーションを、SizeTransitionサイズのアニメーションを行うのに便利です。)

(5)上記のまとめ

 これにより、複数のプロパティを調整した複合的なアニメーションを簡単に作成できます。
(しかし、シンプルさと再利用性の観点からは、専用のウィジェットを使う方が良い場合もあることが理解できます。)

  • 同時にアニメーションを実行する

     サイズと透明度のアニメーションを同時に行い、オブジェクトが透明から不透明、そして小さくなったり大きくなったりする効果を実現します。
  • 複数のTweenを使用

     1つのAnimationControllerで異なるプロパティを制御する複数のTweenを利用します。
  • 実運用コードでは FadeTransition と SizeTransition が推奨

     同じアニメーションをより簡単に実装するためには、専用のウィジェットを使う方が良いです。

コメントを残す