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を利用して当社が作成
実装
準備
- flutter_animateをpubspec.yamlに追加する
dependencies:
...
flutter_animate: ^4.5.2
-
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),
),
startAnimation
はFloatingActionButton
のタップをトリガーに実行するように設定しています。
これで、ボタンを押すと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