LoginSignup
9

More than 3 years have passed since last update.

【Flutter】Implicit animationsのサンプル集

Last updated at Posted at 2020-12-03

本記事はFlutter #2 Advent Calendar 2020の4日目の記事です。

はじめに

Flutterのアニメーションを実装するとしたらさまざまなアプローチがありますが、その中でもImplicit animationsは比較的楽に実装できます。
Implicit animationsを用いたアニメーションのサンプルを実装をしたので今回はその紹介をしたいと思います。

また紹介するアニメーションのサンプルはこちらのリポジトリで公開しています。

Implicit Animationsとは

Implicit Animationsは日本語に直訳すると、暗黙的なアニメーションです。
Flutterのアニメーションには、暗黙的なアニメーションと明示的なアニメーションがあります。

暗黙的なアニメーションは、Widgetに対してアニメーションさせたいプロパティ(幅や高さや色など)の値を設定するだけで、現在の値から新しい値までのアニメーションを処理してくれます。
これらのウィジェットはまとめて暗黙のアニメーション、または暗黙的にアニメーション化されたウィジェットと呼ばれます。(これ以降ではImplicit Animationsと明記します)

対して明示的なアニメーションでは、AnimationControllerクラスを用いて実装する必要があります。
この記事では明示的なアニメーションの説明は割愛します。

Implicit Animationsの種類

Implicit AnimationsのWidgetの多くは標準でフレームワークに組み込まれています。
Implicit AnimationsのWidgetのクラス名は、「AnimatedXXX」という名前で、「XXX」にはアニメーションされていないクラス名が入ることが多いです。
公式ドキュメントによると、Implicit Animationsには下記の種類があります。

  • AnimatedAlign
  • AnimatedContainer
  • AnimatedDefaultTextStyle
  • AnimatedOpacity
  • AnimatedPadding
  • AnimatedPhysicalModel
  • AnimatedPositioned
  • AnimatedPositionedDirectional
  • AnimatedTheme
  • AnimatedCrossFade
  • AnimatedSize
  • AnimatedSwitcher

それではそれぞれどんなアニメーションができるのか見ていきます。

AnimatedAlign

AnimatedAlignはAlignのchildに設定したWidgetのalignmentをアニメーションさせることができるImplicit Animationsです。

下記のサンプルでは、FloatingActionButtonを押下すると、ContainerをCenterからBottom(またはその逆)へアニメーションさせています。

animated_align.gif


class AnimatedAlignScreen extends StatefulWidget {
  @override
  _AnimatedAlignScreenState createState() => _AnimatedAlignScreenState();
}

class _AnimatedAlignScreenState extends State<AnimatedAlignScreen> {
  Alignment _alignment = Alignment.center;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('AnimatedAlign Sample'),
      ),
      body: Center(
          child: AnimatedAlign(
        alignment: this._alignment,
        duration: Duration(seconds: 1),
        child: Container(color: Colors.red, width: 200, height: 200),
      )),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.play_arrow),
        onPressed: () {
          setState(() {
            if (this._alignment == Alignment.center) {
              this._alignment = Alignment.bottomLeft;
            } else {
              this._alignment = Alignment.center;
            }
          });
        },
      ),
    );
  }
}

アニメーションの途中で停止したい場合など詳細に制御したい場合は、AlignTransitionクラスを使いましょう。

AnimatedContainer

AnimatedContainerはContainerクラスのプロパティをアニメーションさせることができます。
アニメーションさせたいプロパティの値が変更されると、Durationの時間でアニメーションされます。

下記のサンプルではFloatingActionButtonを押下すると、1~300までのランダムな値をAnimatedContainerの幅と高さに設定して、アニメーションさせています。

animated_container.gif

class AnimatedContainerScreen extends StatefulWidget {
  @override
  _AnimatedContainerScreenState createState() =>
      _AnimatedContainerScreenState();
}

class _AnimatedContainerScreenState extends State<AnimatedContainerScreen> {
  double _width = 50;
  double _height = 50;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('AnimatedContainer Sample'),
      ),
      body: Center(
        child: AnimatedContainer(
          color: Colors.red,
          width: this._width,
          height: this._height,
          duration: Duration(seconds: 1),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.play_arrow),
        onPressed: () {
          setState(() {
            this._width = Random().nextInt(300).toDouble();
            this._height = Random().nextInt(300).toDouble();
          });
        },
      ),
    );
  }
}

プロパティにnullを設定するとアニメーションされません。
また、AnimatedContainerのchildプロパティに設定したWidgetにはアニメーションの影響はありません。

AnimatedDefaultTextStyle

AnimatedDefaultTextStyleはDefaultTextStyleクラスのImplicit Animationsです。

下記のサンプルでは、FloatingActionButtonを押下すると、TextのTextStyleが1秒かけてアニメーションされています。

animated_default_text_style.gif

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class AnimatedDefaultTestStyleScreen extends StatefulWidget {
  @override
  _AnimatedDefaultTestStyleScreenState createState() =>
      _AnimatedDefaultTestStyleScreenState();
}

class _AnimatedDefaultTestStyleScreenState
    extends State<AnimatedDefaultTestStyleScreen> {
  TextStyle _textStyle1 =
      TextStyle(fontSize: 30, color: Colors.blue, fontWeight: FontWeight.w400);

  TextStyle _textStyle2 =
      TextStyle(fontSize: 15, color: Colors.green, fontWeight: FontWeight.bold);

  TextStyle _textStyle3 =
      TextStyle(fontSize: 24, color: Colors.red, fontWeight: FontWeight.w900);

  int _textStyleType = 0;

  TextStyle get _currentTextStyle {
    switch (this._textStyleType) {
      case 0:
        return this._textStyle1;
      case 1:
        return this._textStyle2;
      case 2:
        return this._textStyle3;
      default:
        return this._textStyle1;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('AnimatedAlign Sample'),
      ),
      body: Center(
          child: AnimatedDefaultTextStyle(
        style: this._currentTextStyle,
        duration: Duration(seconds: 1),
        child: Center(
          child: const Text('TextStyleが変わります'),
        ),
      )),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.play_arrow),
        onPressed: () {
          setState(() {
            int nextTextStyleType = this._textStyleType += 1;
            if (nextTextStyleType > 2) {
              nextTextStyleType = 0;
            }
            this._textStyleType = nextTextStyleType;
          });
        },
      ),
    );
  }
}

サンプルでは、TextStyleのfontSizecolorfontWeightをアニメーションさせていますが、下記のプロパティはアニメーションできず、設定したらすぐ変更されます。

アニメーションの途中で停止したい場合など詳細に制御したい場合は、DefaultTextStyleTransitionクラスを使いましょう。

AnimatedOpacity

AnimatedOpacityは Opacity のImplicit Animationsです。Widgetの透明度をアニメーションさせることができます。
下記のサンプルではFloatingActionButtonを押下すると、ContainerのOpacityを1.0から0.0(またはその逆)にアニメーションさせています。

animated_opacity.gif

class AnimatedOpacityScreen extends StatefulWidget {
  @override
  _AnimatedOpacityScreenState createState() => _AnimatedOpacityScreenState();
}

class _AnimatedOpacityScreenState extends State<AnimatedOpacityScreen> {
  double _opacity = 1.0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('AnimatedOpacity Sample'),
      ),
      body: Center(
          child: AnimatedOpacity(
        opacity: this._opacity,
        duration: Duration(seconds: 1),
        child: Container(
          width: 200,
          height: 200,
          color: Colors.red,
        ),
      )),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.play_arrow),
        onPressed: () {
          setState(() {
            this._opacity = (this._opacity >= 1.0) ? 0.0 : 1.0;
          });
        },
      ),
    );
  }
}

また、透明度のアニメーションは処理が重いため必要最小限に留めておきましょう。
(下記公式ドキュメントの引用)

Animating an opacity is relatively expensive because it requires painting the child into an intermediate buffer.

AnimatedPadding

AnimatedPaddingは Padding クラスのImplicit Animationsです。WidgetのPaddingをアニメーションさせることができます。
下記のサンプルではFloatingActionButtonを押下すると、ContainerのPaddingにランダム値を設定しアニメーションさせています。

animated_padding.gif

import 'dart:math';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class AnimatedPaddingScreen extends StatefulWidget {
  @override
  _AnimatedPaddingScreenState createState() => _AnimatedPaddingScreenState();
}

class _AnimatedPaddingScreenState extends State<AnimatedPaddingScreen> {
  EdgeInsets _edgeInsets = EdgeInsets.all(20);

  double _randomPaddingValue() {
    return Random().nextInt(200).toDouble();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('AnimatedPadding Sample'),
      ),
      body: Center(
          child: AnimatedPadding(
        padding: this._edgeInsets,
        duration: Duration(seconds: 1),
        child: Container(color: Colors.red),
      )),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.play_arrow),
        onPressed: () {
          setState(() {
            final Random random = Random();
            double top = this._randomPaddingValue();
            double right = this._randomPaddingValue();
            double left = this._randomPaddingValue();
            double bottom = this._randomPaddingValue();
            this._edgeInsets = EdgeInsets.only(
                top: top, right: right, left: left, bottom: bottom);
          });
        },
      ),
    );
  }
}

AnimatedPhysicalModel

AnimatedPhysicalModelは PhysicalModel のImplicit Animationsです。
PhysicalModelクラスはchildプロパティに設定したWidgetに対してZ座標の概念を持たせることができます。
Z座標の概念を持ったWidgetは影が描画され、影の色や形などを設定できます。

下記のサンプルではFloatingActionButtonを押下すると、AnimatedPhysicalModelを使用して、Z座標の値、Containerの色、影の色をアニメーションさせています。
(影の形を表すAnimatedPhysicalModelのshapeプロパティはアニメーションできないので固定値となっています)

animated_physical_model.gif

class AnimatedPhysicalModelScreen extends StatefulWidget {
  @override
  _AnimatedPhysicalModelScreenState createState() =>
      _AnimatedPhysicalModelScreenState();
}

class _AnimatedPhysicalModelScreenState
    extends State<AnimatedPhysicalModelScreen> {
  // Z座標
  static final double fromElevation = 10.0;
  static final double toElevation = 30.0;
  double _elevation = fromElevation;

  // 色
  static final Color fromColor = Colors.orange;
  static final Color toColor = Colors.greenAccent;
  Color _color = fromColor;

  // 影の色
  static final Color fromShadowColor = Colors.red;
  static final Color toShadowColor = Colors.green;
  Color _shadowColor = fromShadowColor;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('AnimatedPhysicalModel Sample'),
      ),
      body: Center(
          child: AnimatedPhysicalModel(
        elevation: this._elevation,
        shape: BoxShape.rectangle,
        color: this._color,
        shadowColor: this._shadowColor,
        duration: Duration(seconds: 1),
        child: Container(
          width: 200,
          height: 200,
        ),
      )),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.play_arrow),
        onPressed: () {
          setState(() {
            if (this._elevation >= toElevation) {
              this._elevation = fromElevation;
              this._color = fromColor;
              this._shadowColor = fromShadowColor;
            } else {
              this._elevation = toElevation;
              this._color = toColor;
              this._shadowColor = toShadowColor;
            }
          });
        },
      ),
    );
  }
}

AnimatedPositioned

AnimatedPositionedは Positioned クラスのImplicit Animationsです。PositionedクラスはStackクラスのchildプロパティに設定することで動作します。
下記のサンプルではFloatingActionButtonを押下すると、AnimatedPositionedのtoprightleftbottomの値を設定し、childプロパティに設定しているContainerの位置をアニメーションさせています。

animated_position.gif

class AnimatedPositionedScreen extends StatefulWidget {
  @override
  _AnimatedPositionedScreenState createState() =>
      _AnimatedPositionedScreenState();
}

class _AnimatedPositionedScreenState extends State<AnimatedPositionedScreen> {
  // top
  static final double _fromTop = 50;
  static final double _toTop = 100;
  double _top = _fromTop;

  // right
  static final double _fromRight = 150;
  static final double _toRight = 60;
  double _right = _fromRight;

  // left
  static final double _fromLeft = 0;
  static final double _toLeft = 60;
  double _left = _fromLeft;

  // bottom
  static final double _fromBottom = 50;
  static final double _toBottom = 100;
  double _bottom = _fromBottom;

  // fromとtoのフラグ
  bool isFrom = true;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('AnimatedPositioned Sample'),
      ),
      body: Center(
        child: Stack(
          children: [
            Positioned(
                child: Container(
              width: 400,
              height: 400,
              color: Colors.red,
            )),
            AnimatedPositioned(
                duration: Duration(seconds: 1),
                top: this._top,
                right: this._right,
                left: this._left,
                bottom: this._bottom,
                child: Container(
                  color: Colors.green,
                ))
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.play_arrow),
        onPressed: () {
          setState(() {
            this.isFrom = !this.isFrom;
            this._top = this.isFrom ? _fromTop : _toTop;
            this._right = this.isFrom ? _fromRight : _toRight;
            this._left = this.isFrom ? _fromLeft : _toLeft;
            this._bottom = this.isFrom ? _fromBottom : _toBottom;
          });
        },
      ),
    );
  }
}

こちらのサンプルでは、toprightleftbottomを変化させることで、Containerのサイズが変更しています。そのようなケースではAnimatedPositionedが適しています。
もし、Containerのサイズを同じままにして、位置のみを変更する場合は、 SlideTransition を検討した方が良いです。

AnimatedPositionedDirectional

AnimatedPositionedDirectionalはPositionedDirectionalのImplicit Animationsです。
PositionedDirectionalはPositionedクラスとほとんど同じです。
違いはWidgetの横方向の始点と終点を変更できる点です。
例えば日本語のテキストだと、文字は左から右へ流れていきますが、言語によっては右から左へ流れる言語もあります。
条件によってどっちの方向が始点でどっちの方向が終点なのかが変わる場合はPositionedではなく、PositionedDirectionalを使用します。

下記のサンプルでは、上記で紹介したAnimatedPositionedのサンプルを右方向を始点にしてアニメーションさせています。
(AnimatedPositionedの部分をAnimatedPositionedDirectionalに変更したコード以外は同じなので省略します。)

animated_position_directional.gif


Directionality(
    textDirection: TextDirection.rtl,   // 右を始点にする
    child: AnimatedPositionedDirectional(
        duration: Duration(seconds: 1),
        top: this._top,
        start: this._left,              // leftからstartに変更
        end: this._right,               // rightからendに変更
        bottom: this._bottom,
        child: Container(
          color: Colors.green,
        )))

AnimatedTheme

AnimatedThemeは Theme クラスのImplicit Animationsです。
Themeクラスは、childプロパティに設定したWidgetに ThemeData データを適用できます。
ThemeDataではColorやタイポグラフィなどを設定でき、これらのデータをAnimatedThemeを使ってアニメーションさせます。

下記のサンプルでは、FloatingActionButtonを押下すると、Cardの背景色をライトモードとダークモードで切り替えるようにアニメーションしています。

animated_theme.gif

まず、MaterialAppでライトモードとダークモードのThemeDataを設定する必要があります。

main.dart
class MyApp extends StatelessWidget {
  static final ThemeData _lightTheme = ThemeData(
      primaryColor: Colors.blue,
      accentColor: Colors.blue,
      textTheme: TextTheme(bodyText2: TextStyle(color: Colors.black)));

  static final ThemeData _darkTheme = ThemeData(
      primaryColor: Colors.blue,
      accentColor: Colors.black,
      textTheme: TextTheme(bodyText2: TextStyle(color: Colors.white)));

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '',
      theme: _lightTheme,
      darkTheme: _darkTheme,
    );
  }
}

AnimatedThemeで設定したThemeDataを参照することで、childプロパティのWidgetに反映されます。

animated_theme_screen.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class AnimatedThemeScreen extends StatefulWidget {
  @override
  _AnimatedThemeScreenState createState() => _AnimatedThemeScreenState();
}

class _AnimatedThemeScreenState extends State<AnimatedThemeScreen> {
  bool _isLightMode = true;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('AnimatedTheme Sample'),
      ),
      body: Center(
          child: AnimatedTheme(
        duration: Duration(seconds: 1),
        data: this._isLightMode ? ThemeData.light() : ThemeData.dark(),
        child: Card(
          child: Padding(
            padding: EdgeInsets.all(50),
            child: Text(
              this._isLightMode ? 'ライトモード' : 'ダークモード',
              style: TextStyle(fontSize: 30),
            ),
          ),
        ),
      )),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.play_arrow),
        onPressed: () {
          setState(() {
            this._isLightMode = !this._isLightMode;
          });
        },
      ),
    );
  }
}

AnimatedCrossFade

AnimatedCrossFadeは異なる二つのWidgetをクロスフェードアニメーションさせるImplicit Animationsです。

下記のサンプルでは色とサイズの違うContainerをクロスフェードさせています。

animated_crossfade.gif

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class AnimatedCrossFadeScreen extends StatefulWidget {
  @override
  _AnimatedCrossFadeScreenState createState() =>
      _AnimatedCrossFadeScreenState();
}

class _AnimatedCrossFadeScreenState extends State<AnimatedCrossFadeScreen> {
  // 最初に表示するWidget
  Widget _firstWidget() {
    return Container(
      width: 400,
      height: 200,
      color: Colors.red,
    );
  }

  // クロスフェードするWidget
  Widget _secondWidget() {
    return Container(
      width: 200,
      height: 300,
      color: Colors.blue,
    );
  }

  // 最初のWidgetを表示するフラグ
  bool showFirst = true;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('AnimatedCrossFade Sample'),
      ),
      body: Center(
          child: AnimatedCrossFade(
        duration: Duration(seconds: 1),
        crossFadeState: this.showFirst
            ? CrossFadeState.showFirst
            : CrossFadeState.showSecond,
        firstChild: this._firstWidget(),
        secondChild: this._secondWidget(),
        // layoutBuilderでサイズの違うWidgetでも違和感なく表示する
        layoutBuilder: (topChild, topChildKey, bottomChild, bottomChildKey) {
          return Stack(
            overflow: Overflow.visible,
            alignment: Alignment.center,
            children: [
              Positioned(
                child: bottomChild,
                key: bottomChildKey,
              ),
              Positioned(
                child: topChild,
                key: topChildKey,
              )
            ],
          );
        },
      )),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.play_arrow),
        onPressed: () {
          setState(() {
            this.showFirst = !this.showFirst;
          });
        },
      ),
    );
  }
}

AnimatedCrossFadeは同じ幅のWidgetをクロスフェードするのを目的としています。
異なる幅をクロスフェードさせようした場合は、クロスフェードさせようとしたWidgetが急に現れる(伝えづらい…)ような違和感ある動きになってしまいます。
その場合、layoutBuilderを使用すると違和感なく表示することができます。
詳しくは公式ドキュメントを参照ください。

AnimatedSize

AnimatedSizeはchildに設定したWidgetのサイズをアニメーションできるImplicit Animationsです。

下記のサンプルではFlutterロゴのサイズを変更するアニメーションをします。

animated_size.gif

import 'dart:math';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class AnimatedSizeScreen extends StatefulWidget {
  @override
  _AnimatedSizeScreenState createState() => _AnimatedSizeScreenState();
}

class _AnimatedSizeScreenState extends State<AnimatedSizeScreen>
    with SingleTickerProviderStateMixin {
  double _size = 200;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('AnimatedSize Sample'),
      ),
      body: Center(
          child: Container(
        color: Colors.amberAccent,
        child: AnimatedSize(
            vsync: this,
            duration: Duration(seconds: 1),
            child: FlutterLogo(
              size: this._size,
            )),
      )),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.play_arrow),
        onPressed: () {
          setState(() {
            this._size = Random().nextInt(300).toDouble();
          });
        },
      ),
    );
  }
}

※ childプロパティにFlutterLogoではなくContainerを指定したところ、アニメーションが滑らかにいきませんでした。こちらは調査中です。

AnimatedSwitcher

AnimatedSwitcherはchildプロパティに新たに設定したWidgetと、前のWidgetをアニメーションで切り替えるImplicit Animationsです。

デフォルトのアニメーションはクロスフェードですが、そのほかにもスケールしながら表示させるScaleTransitionや、回転しながら表示させるRotationTransitionなども設定できます。

注意点として、新たに表示させるWidgetと前のwidgetが同じWidgetの場合は、keyを設定する必要があります。

下記のサンプルでは、ElevatedButton押下でカウントをインクリメントされたテキストをScaleTransitionでアニメーションさせています。

animated_switcher.gif

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class AnimatedSwitcherScreen extends StatefulWidget {
  @override
  _AnimatedSwitcherScreenState createState() => _AnimatedSwitcherScreenState();
}

class _AnimatedSwitcherScreenState extends State<AnimatedSwitcherScreen> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('AnimatedSize Sample'),
      ),
      body: Center(
        child: Container(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              AnimatedSwitcher(
                duration: const Duration(milliseconds: 500),
                transitionBuilder: (Widget child, Animation<double> animation) {
                  return ScaleTransition(child: child, scale: animation);
                },
                child: Text(
                  '$_count',
                  key: ValueKey<int>(_count),
                  style: Theme.of(context).textTheme.headline4,
                ),
              ),
              ElevatedButton(
                child: const Text('Increment'),
                onPressed: () {
                  setState(() {
                    this._count += 1;
                  });
                },
              ),
            ],
          ),
        ),
      ),
    );
  }
}

終わりに

本記事ではImplicit Animationsのサンプルを紹介しました。

各アニメーションの実装で省略しましたが、ほとんどのImplicit AnimationsにはCurveを設定でき、アニメーションの速度の波を変えることもできます。

またこちらも紹介しませんでしたが、TweenAnimationBuilderクラスを使うと、カスタムのアニメーションを実装できるので、これまで紹介したImplicit Animationsでは実現が難しいならこちらを検討するのも良いと思います。

以上です。

参考にしたサイト

リポジトリ

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
9