6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Flutterのアニメーション(animated/transition)を色々動かして整理してみる

Posted at

:book: Flutterの記事を整理し本にしました :book:

  • 本稿の記事を含む様々な記事を体系的に整理し本にまとめました
  • 今後はこちらを最新化するため、最新情報はこちらをご確認ください
  • 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で指定します。

main.dart
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の状態を切り替えているものもあります。

pic21.png

AnimatedContainer,AnimatedSwitcher

前述のAnimatedSizeAnimatedAlignは基本的で汎用性は高いのですが、もう少しまとめて使いたい場合もあります。このような場合にはAnimatedContainerAnimatedSwitcherが便利です。

AnimatedContainerは要素をまとめて変更する場合に便利です。AnimatedSwitcherは2つのWidgetをいい感じに切り替えてくれる便利なアニメーションWidgetです。

main.dart
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)),
      ]),
    );
  }
}

今度は、AnimatedContainerAnimatedSwitcherの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にパラメタとして設定します。

main.dart
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のメソッドを呼び出しています。

pic41.png

Animation/Tween

AnimationControllerは、Durationの間に、0.0から1.0までvalueを変化させながら、アニメーションを実現しています。

Tweenを使うと、この範囲や型を自由にカスタマイズしたAnimationを作成することができます。

なお、AnimationControllerTweenを連携させ、Animationを作成する方法は、以下の2種類があります。

  1. TweenのanimateメソッドにAnimationControllerを渡す
  2. AnimationControllerのdriveメソッドにTweenを渡す

今回は1の方法を採用しています。

main.dart
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に型を指定して宣言し、beginendで開始と終了の状態の値を設定します。
その後、_animationControllerと紐付けたアニメーションを作成しています。

アニメーションは、サイズ用とカラー用の2つを作成しています。
_animationDouble_animationColorがそれに対応します。Tweenもそれぞれに対応するものを作成しています。

addListener(() {setState(() {});});は変更を反映させるために行っています。

pic42.png

今回は、サイズ管理用のDoubleを0~200に変化させながら、カラー管理用をグリーンからブルーまで変化させています。

AnimationBuilder

より汎用的に使う方法として、AnimationBuilderがあります。
animationパラメタにどのようなアニメーションを与え、builderにアニメーションで動かすWidgetを構築します。

main.dart
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)),
      ]),
    );
  }
}

この例では、AnimationControllerdriveメソッドにTweenを渡す方法でAnimationを作成しています。

まず、initStateでvalueを0~2πまでを変化させるアニメーションを作成しています。

続いて、AnimatedBuilderを作り、その中でアニメーションを設定し、builderでそのアニメーションを適応するWidgetを指定します。
今回は回転処理をを行うWidgetを使っています。

pic43.png

TickerProviderStateMixinについて

mixinを使っていますが、これはvsync:thisを使うためで、vsyncはアニメーションのフレームごとの更新を伝播するために用いられます。

単一のAnimationControllerで使う場合はSingleTickerProviderStateMixinを使い、複数で使う場合はTickerProviderStateMixinを使います。

基本的に自作することはなく、おまじないのように利用して問題ないかと思います。

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?