 Flutterの記事を整理し本にしました
 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を使います。
基本的に自作することはなく、おまじないのように利用して問題ないかと思います。



