Flutterでアニメーション

# はじめに
ぐるなびでサーバーサイドのエンジニアとして活動している前田です。
https://developers.gnavi.co.jp/archive/category/%E3%80%90%E5%9F%B7%E7%AD%86%E8%80%85%E3%80%91%E5%89%8D%E7%94%B0

Flutterがバージョン1.0としてメジャーリリースされましたね。
前からFlutterに興味があったのと、アニメーションの実装に興味があったので、
Flutterでアニメーションを実装してみました。

# 使ったライブラリ
flutter_sequence_animationというライブラリを使います。
https://pub.dartlang.org/packages/flutter_sequence_animation

# 実装
とりあえずなれるために実装した後、
ログインボタン押下→画面遷移のアニメーションを実装してみます。
クロスプラットフォームということなので、AndroidとiOS両方のシュミレータで実行してみます。
## とりあえず実装してみた
```dart
import 'package:flutter/material.dart';
import 'package:flutter_sequence_animation/flutter_sequence_animation.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @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 SingleTickerProviderStateMixin {
  AnimationController controller;
  SequenceAnimation sequenceAnimation;

  @override
  void initState() {
    super.initState();
    controller = new AnimationController(vsync: this);

    sequenceAnimation = SequenceAnimationBuilder()
        .addAnimatable(
            animatable: new EdgeInsetsTween(
              begin: const EdgeInsets.only(bottom: 40.0),
              end: const EdgeInsets.only(bottom: 200.0),
            ),
            from: Duration.zero,
            to: const Duration(milliseconds: 1000),
            curve: Curves.fastOutSlowIn,
            tag: "padding")
        .addAnimatable(
            animatable: new Tween<double>(begin: 0.0, end: 1.0),
            from: Duration.zero,
            to: const Duration(milliseconds: 1000),
            curve: Curves.fastOutSlowIn,
            tag: "opacity")
        .addAnimatable(
            animatable: Tween<double>(begin: 10.0, end: 200.0),
            from: const Duration(milliseconds: 1000),
            to: const Duration(milliseconds: 2000),
            curve: Curves.elasticOut,
            tag: 'width')
        .addAnimatable(
            animatable: Tween<double>(begin: 10.0, end: 200.0),
            from: const Duration(milliseconds: 2000),
            to: const Duration(milliseconds: 3000),
            curve: Curves.elasticOut,
            tag: 'height')
        .addAnimatable(
            animatable: ColorTween(begin: Colors.blue, end: Colors.red),
            from: const Duration(milliseconds: 3100),
            to: const Duration(milliseconds: 5000),
            curve: Curves.bounceOut,
            tag: 'color')
        .addAnimatable(
            animatable: new BorderRadiusTween(
              begin: new BorderRadius.circular(4.0),
              end: new BorderRadius.circular(100.0),
            ),
            from: const Duration(milliseconds: 3100),
            to: const Duration(milliseconds: 5000),
            curve: Curves.bounceOut,
            tag: "borderRadius")
        .animate(controller);
  }

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

  Widget _buildAnimation(BuildContext context, Widget child) {
    return new Container(
      padding: sequenceAnimation["padding"].value,
      alignment: Alignment.bottomCenter,
      child: new Opacity(
        opacity: sequenceAnimation["opacity"].value,
        child: new Container(
          width: sequenceAnimation["width"].value,
          height: sequenceAnimation["height"].value,
          decoration: new BoxDecoration(
            borderRadius: sequenceAnimation["borderRadius"].value,
            color: sequenceAnimation["color"].value,
          ),
        ),
      ),
    );
  }

  Future<Null> _playAnimation() async {
    try {
      await controller.forward().orCancel;
      await controller.reverse().orCancel;
    } on TickerCanceled {
      // the animation got canceled, probably because we were disposed
    }
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("Staggered Animation"),
      ),
      body: new GestureDetector(
        behavior: HitTestBehavior.opaque,
        onTap: () {
          _playAnimation();
        },
        child: new Center(
          child: new Container(
            child: new AnimatedBuilder(
                animation: controller, builder: _buildAnimation),
          ),
        ),
      ),
    );
  }
}
```
<img width="300" alt="flutter_animation_1.gif" src="https://qiita-image-store.s3.amazonaws.com/0/311078/c3b325e3-6ca7-6810-e71a-e5f8457fdd89.gif">

## ボタン押下時から画面遷移するまでのアニメーション
```dart
import 'package:flutter/material.dart';
import 'package:flutter_sequence_animation/flutter_sequence_animation.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @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 SingleTickerProviderStateMixin {
  AnimationController controller;
  SequenceAnimation sequenceAnimation;

  @override
  void initState() {
    super.initState();
    controller = new AnimationController(vsync: this);

    sequenceAnimation = SequenceAnimationBuilder()
        .addAnimatable(
            animatable: new EdgeInsetsTween(
              begin: const EdgeInsets.only(bottom: 80.0),
              end: const EdgeInsets.only(bottom: 400.0),
            ),
            from: Duration.zero,
            to: const Duration(milliseconds: 3000),
            curve: Curves.elasticInOut,
            tag: "padding")
        .addAnimatable(
            animatable: new BorderRadiusTween(
              begin: new BorderRadius.circular(4.0),
              end: new BorderRadius.circular(100.0),
            ),
            from: Duration.zero,
            to: const Duration(milliseconds: 1000),
            curve: Curves.ease,
            tag: "borderRadius")
        .addAnimatable(
            animatable: Tween<double>(begin: 200.0, end: 40.0),
            from: Duration.zero,
            to: const Duration(milliseconds: 1000),
            curve: Curves.ease,
            tag: 'width')
        .addAnimatable(
            animatable: ColorTween(begin: Colors.pink, end: Colors.blue),
            from: const Duration(milliseconds: 1000),
            to: const Duration(milliseconds: 3000),
            curve: Curves.easeInOut,
            tag: 'color')
        .addAnimatable(
            animatable: new EdgeInsetsTween(
              begin: const EdgeInsets.only(bottom: 400.0),
              end: const EdgeInsets.only(bottom: 0.0),
            ),
            from: const Duration(milliseconds: 3000),
            to: const Duration(milliseconds: 4000),
            curve: Curves.ease,
            tag: "padding")
        .addAnimatable(
            animatable: new Tween<double>(begin: 1.0, end: 0.0),
            from: const Duration(milliseconds: 4000),
            to: const Duration(milliseconds: 5500),
            curve: Curves.ease,
            tag: "opacity")
        .addAnimatable(
            animatable: new BorderRadiusTween(
              begin: new BorderRadius.circular(100.0),
              end: new BorderRadius.circular(0.0),
            ),
            from: const Duration(milliseconds: 4000),
            to: const Duration(milliseconds: 4100),
            curve: Curves.ease,
            tag: "borderRadius")
        .addAnimatable(
            animatable: Tween<double>(begin: 40.0, end: 1000.0),
            from: const Duration(milliseconds: 4000),
            to: const Duration(milliseconds: 5000),
            curve: Curves.ease,
            tag: 'height')
        .addAnimatable(
            animatable: Tween<double>(begin: 40.0, end: 1000.0),
            from: const Duration(milliseconds: 4000),
            to: const Duration(milliseconds: 5000),
            curve: Curves.ease,
            tag: 'width')
        .animate(controller);
  }

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

  Widget _buildAnimation(BuildContext context, Widget child) {
    return new Container(
      padding: sequenceAnimation["padding"].value,
      alignment: Alignment.bottomCenter,
      child: new Opacity(
        opacity: sequenceAnimation["opacity"].value,
        child: new Container(
          width: sequenceAnimation["width"].value,
          height: sequenceAnimation["height"].value,
          decoration: new BoxDecoration(
            borderRadius: sequenceAnimation["borderRadius"].value,
            color: sequenceAnimation["color"].value,
          ),
        ),
      ),
    );
  }

  Future<Null> _playAnimation() async {
    try {
      await controller.forward().orCancel;
    } on TickerCanceled {
      // the animation got canceled, probably because we were disposed
    }
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("Flutter Demo"),
      ),
      body: new GestureDetector(
        behavior: HitTestBehavior.opaque,
        onTap: () {
          _playAnimation();
        },
        child: new Center(
          child: new Container(
            child: new AnimatedBuilder(
                animation: controller, builder: _buildAnimation),
          ),
        ),
      ),
    );
  }
}
```
<img width="300" alt="flutter_animation_2.gif" src="https://qiita-image-store.s3.amazonaws.com/0/311078/6098e3d1-92d0-e3fa-237f-01215918c850.gif">