Flutterの記事を整理し本にしました
- 本稿の記事を含む様々な記事を体系的に整理し本にまとめました
- 今後はこちらを最新化するため、最新情報はこちらをご確認ください
- 20万文字を超える超大作になっています!!
まとめ記事
はじめに
- いままであまりアニメーションに手を出してこなかったのですが、UI/UXを考えると重要なので、GWをの時間を使って、色々動かしながら整理してみました
概要
Flutterにはアニメーションを簡単に取り扱えるWidgetが潤沢に準備されています。
Flutterでアニメーションを扱えるWidgetは大きく2種類あります。
- パラメタを設定するだけで簡単に取り扱えるanimated系
- ontrollerとセットで利用し、細かい制御ができるtransition系
この2種類を解説していきます。
Animated系Widget
簡単にアニメーションがあつかえるanimated系は、AnimatedXXX
という名称のWidgetが該当します
AnimatedOpacity,AnimatedSize,AnimatedAlign
まずは、Opacity(不透明度)、Size(大きさ)、Align(位置)をアニメーションで動かしてみます。
いずれも、AnimatedXXX
というWidgetに対して、duration
でどの程度の期間をかけて変化をするかというパラメタと、対象となるWidgetをchildで指定します。
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, this.title}) : super(key: key);
final String? title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
bool flag = false;
_click() async {
setState(() {
flag = !flag;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title!),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
AnimatedOpacity(
opacity: flag ? 0.1 : 1.0,
duration: Duration(seconds: 3),
child: Text(
"消える文字",
style: Theme.of(context).textTheme.headline4,
)),
AnimatedSize(
vsync: this,
duration: Duration(seconds: 3),
child: SizedBox(
width: flag ? 50 : 200,
height: flag ? 50 : 200,
child: Container(color: Colors.purple))),
AnimatedAlign(
duration: Duration(seconds: 3),
alignment: flag ? Alignment.topLeft : Alignment.bottomRight,
child: SizedBox(
width: 50,
height: 50,
child: Container(color: Colors.green)))
],
),
),
floatingActionButton:
Row(mainAxisAlignment: MainAxisAlignment.end, children: [
FloatingActionButton(onPressed: _click, child: Icon(Icons.add)),
]),
);
}
}
まず、AnimatedOpacity
,AnimatedSize
,AnimatedAlign
の3つがColumn
で設定されています。
次に、ボタンをクリックするたびにフラグが切り替わり、アニメーションの2つの状態を切り替えています。
次に、3つのWidgetの詳細は下記のようになっています。
- Opacity:"消える文字"のテキストの透明度を0.1と1.0を切り替えています
- Size:50×50と200×200のサイズを切り替えています
- Align:配置の左上と右下を切り替えています。
Animation用Widgetのパラメタではなく、childで指定するWidgetの状態を切り替えているものもあります。
AnimatedContainer,AnimatedSwitcher
前述のAnimatedSize
やAnimatedAlign
は基本的で汎用性は高いのですが、もう少しまとめて使いたい場合もあります。このような場合にはAnimatedContainer
やAnimatedSwitcher
が便利です。
AnimatedContainer
は要素をまとめて変更する場合に便利です。AnimatedSwitcher
は2つのWidgetをいい感じに切り替えてくれる便利なアニメーションWidgetです。
void main() { /* 変更なしのため中略 */ }
class MyApp extends StatelessWidget { /* 変更なしのため中略 */ }
class MyHomePage extends StatefulWidget { /* 変更なしのため中略 */ }
class _MyHomePageState extends State<MyHomePage> {
bool flag = false;
_click() async {
setState(() {
flag = !flag;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title!),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
AnimatedContainer(
duration: Duration(seconds: 3),
width: flag ? 100 : 50,
height: flag ? 50 : 100,
padding: flag ? EdgeInsets.all(0) : EdgeInsets.all(30),
margin: flag ? EdgeInsets.all(0) : EdgeInsets.all(30),
transform: flag ? Matrix4.skewX(0.0) : Matrix4.skewX(0.3),
color: flag ? Colors.blue : Colors.grey),
AnimatedSwitcher(
duration: Duration(seconds: 3),
child: flag
? Text("なにもない")
: Icon(Icons.favorite, color: Colors.pink))
],
),
),
floatingActionButton:
Row(mainAxisAlignment: MainAxisAlignment.end, children: [
FloatingActionButton(onPressed: _click, child: Icon(Icons.add)),
]),
);
}
}
今度は、AnimatedContainer
とAnimatedSwitcher
の2つがColumnで設定されています。
ボタンでフラグのON/OFFを切り替えている部分は変わっていません。
-
Container:width,height,padding,marginなどの各要素の幅を変更しながら、transformで形を変え、colorで色も変えています。
-
Switcher:TextのWidgetとIconのWidgetを3秒間で変えています。全く異なるWidgetですが、なめらかにアニメーションでつなぐことができます。
Transition系Widget
細かい制御ができるtransition系は、XXXTransition
という名称のWidgetが該当します
AnimationContoroller
Transition系は、AnimationController
を使って、アニメーションの再生・停止・繰り返し・逆再生などを行います。
基本的な流れは、AnimationController
を作成し、Transition系Widgetにパラメタとして設定します。
void main() { /* 変更なしのため中略 */ }
class MyApp extends StatelessWidget { /* 変更なしのため中略 */ }
class MyHomePage extends StatefulWidget { /* 変更なしのため中略 */ }
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
late AnimationController _animationControler;
_play() async {
setState(() {
_animationControler.forward();
});
}
_stop() async {
setState(() {
_animationControler.stop();
});
}
_reverse() async {
setState(() {
_animationControler.reverse();
});
}
@override
void initState() {
super.initState();
_animationControler =
AnimationController(vsync: this, duration: Duration(seconds: 3));
}
@override
void dispose() {
_animationControler.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title!),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SizeTransition(
sizeFactor: _animationControler,
child: Center(
child: SizedBox(
width: 50,
height: 50,
child: Container(color: Colors.green))),
),
],
),
),
floatingActionButton:
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
FloatingActionButton(
onPressed: _play, child: Icon(Icons.arrow_forward)),
FloatingActionButton(onPressed: _stop, child: Icon(Icons.pause)),
FloatingActionButton(
onPressed: _reverse, child: Icon(Icons.arrow_back)),
]),
);
}
}
AnimationController
は、initState
で生成し、dispose
で破棄します。
dipose
で破棄しない場合は、メモリリークになる可能性があるので注意してください。
再生ボタンで_forward
、停止ボタンで_stop
、逆再生ボタンで_reverse
を呼び出し、対応するAnimationController
のメソッドを呼び出しています。
Animation/Tween
AnimationController
は、Duration
の間に、0.0
から1.0
までvalue
を変化させながら、アニメーションを実現しています。
Tween
を使うと、この範囲や型を自由にカスタマイズしたAnimation
を作成することができます。
なお、AnimationController
とTween
を連携させ、Animation
を作成する方法は、以下の2種類があります。
- TweenのanimateメソッドにAnimationControllerを渡す
- AnimationControllerのdriveメソッドにTweenを渡す
今回は1の方法を採用しています。
void main() { /* 変更なしのため中略 */ }
class MyApp extends StatelessWidget { /* 変更なしのため中略 */ }
class MyHomePage extends StatefulWidget { /* 変更なしのため中略 */ }
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
late AnimationController _animationControler;
late Animation<double> _animationDouble;
Tween<double> _tweenDouble = Tween(begin: 0.0, end: 200.0);
late Animation<Color?> _animationColor;
ColorTween _tweenColor = ColorTween(begin: Colors.green, end: Colors.blue);
_play() async {
setState(() {
_animationControler.forward();
});
}
_stop() async {
setState(() {
_animationControler.stop();
});
}
_reverse() async {
setState(() {
_animationControler.reverse();
});
}
@override
void initState() {
super.initState();
_animationControler =
AnimationController(vsync: this, duration: Duration(seconds: 3));
_animationDouble = _tweenDouble.animate(_animationControler);
_animationDouble.addListener(() {
setState(() {});
});
_animationColor = _tweenColor.animate(_animationControler);
_animationColor.addListener(() {
setState(() {});
});
}
@override
void dispose() {
_animationControler.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title!),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("AnimationController:${_animationControler.value}"),
Text("AnimationDouble:${_animationDouble.value}"),
Text("AnimationColor:${_animationColor.value}"),
SizeTransition(
sizeFactor: _animationControler,
child: Center(
child: SizedBox(
width: _animationDouble.value,
height: _animationDouble.value,
child: Container(color: _animationColor.value))),
),
],
),
),
floatingActionButton:
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
FloatingActionButton(
onPressed: _play, child: Icon(Icons.arrow_forward)),
FloatingActionButton(onPressed: _stop, child: Icon(Icons.pause)),
FloatingActionButton(
onPressed: _reverse, child: Icon(Icons.arrow_back)),
]),
);
}
}
Tween
に型を指定して宣言し、begin
とend
で開始と終了の状態の値を設定します。
その後、_animationController
と紐付けたアニメーションを作成しています。
アニメーションは、サイズ用とカラー用の2つを作成しています。
_animationDouble
と_animationColor
がそれに対応します。Tween
もそれぞれに対応するものを作成しています。
addListener(() {setState(() {});});
は変更を反映させるために行っています。
今回は、サイズ管理用のDoubleを0~200に変化させながら、カラー管理用をグリーンからブルーまで変化させています。
AnimationBuilder
より汎用的に使う方法として、AnimationBuilder
があります。
animation
パラメタにどのようなアニメーションを与え、builder
にアニメーションで動かすWidgetを構築します。
void main() { /* 変更なしのため中略 */ }
class MyApp extends StatelessWidget { /* 変更なしのため中略 */ }
class MyHomePage extends StatefulWidget { /* 変更なしのため中略 */ }
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
late AnimationController _animationControler;
late Animation _animation;
_play() async {
setState(() {
_animationControler.forward();
});
}
_stop() async {
setState(() {
_animationControler.stop();
});
}
_reverse() async {
setState(() {
_animationControler.reverse();
});
}
@override
void initState() {
super.initState();
_animationControler =
AnimationController(vsync: this, duration: Duration(seconds: 1));
_animation = _animationControler.drive(Tween(begin: 0.0, end: 2.0 * pi));
}
@override
void dispose() {
_animationControler.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title!),
),
body: Center(
child: AnimatedBuilder(
animation: _animation,
builder: (context, _) {
return Transform.rotate(
angle: _animation.value, child: Icon(Icons.cached, size: 100));
},
),
),
floatingActionButton:
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
FloatingActionButton(
onPressed: _play, child: Icon(Icons.arrow_forward)),
FloatingActionButton(onPressed: _stop, child: Icon(Icons.pause)),
FloatingActionButton(
onPressed: _reverse, child: Icon(Icons.arrow_back)),
]),
);
}
}
この例では、AnimationController
のdrive
メソッドにTween
を渡す方法でAnimation
を作成しています。
まず、initState
でvalueを0~2πまでを変化させるアニメーションを作成しています。
続いて、AnimatedBuilderを作り、その中でアニメーションを設定し、builderでそのアニメーションを適応するWidgetを指定します。
今回は回転処理をを行うWidgetを使っています。
TickerProviderStateMixinについて
mixinを使っていますが、これはvsync:this
を使うためで、vsyncはアニメーションのフレームごとの更新を伝播するために用いられます。
単一のAnimationControllerで使う場合はSingleTickerProviderStateMixin
を使い、複数で使う場合はTickerProviderStateMixin
を使います。
基本的に自作することはなく、おまじないのように利用して問題ないかと思います。