CurveAnimationControllerTickerProviderStateMixin を使って、アニメーションを実装します。

今回

今回は、以下のようなちょっと複雑なアニメーションを書いてみようと思います。

  • 左右に行ったり来たりする
  • 真ん中で画像を切り替える

Curveの実装

今回は、バウンスとかイージングを実現するための Curve も自前で実装してみます。まず、いつものように、一番上の方に、

import 'dart:math' as math;

を記述します。これにより、三角関数 math.sin を使用することができます。

class _ShakeCurve extends Curve {
  @override
  double transform(double t) {
    return math.sin(t * math.pi * 2) * 64.0;
  }
}

のようにすることで、t0.0 から 1.0 動く間に、 一回だけ行って帰って元の位置に戻ってくるようなカーブが実現できます。

sin 関数は、 0.0 から始まり、 1.00.0-1.0 そして 0.0 に戻ってくる波状の滑らかな関数です。 今回、それに、 64.0 を掛けることで振れ幅 (振幅) を実現しています。

Flutter には、自前で Curve を実装しなくても、 いくつものアニメーションカーブが定義されています。

Curves class - animation library - Dart API をご覧になるとよいと思います。

TickerProviderStateMixinの実装

FingerWidget という名前のウィジット (StatefulWidget) を新しく実装します。

class FingerWidget extends StatefulWidget {
  @override
  _FingerWidgetState createState() =>
      _FingerWidgetState();
}

class _FingerWidgetState extends State<FingerWidget>
    with TickerProviderStateMixin {
  AnimationController _animationController;
  CurvedAnimation _curvedAnimation;

などとします。 State には、 with TickerProviderStateMixin を記述することで、後で述べる AnimationController と連携するウィジットになります。

そして、 2 つプロパティを追加します。 _animationController と、 _curvedAnimation ですね。 _animationController は、アニメーションの実装に必ず必要になります。そして、 _curvedAnimation は、今回のように、 Curve と組み合わせる際に必要になります。

AnimationControllerの初期化

初期化は、 AnimationController を作成します。

  @override
  void initState() {
    _animationController =
    AnimationController(
      duration: const Duration(seconds: 3),
      vsync: this,)
      .addListener(() {
        setState(() {});
      });
    _animationController.repeat();
    _curvedAnimation =
        CurvedAnimation(parent: _animationController, curve: _ShakeCurve());
    super.initState();
  }

のようにします。 引数 duration に、アニメーション開始から終了までの時間、 vsyncthis をいれます。

最後に、 .addListener() の中で、 setState(() {}) する必要があります。 これによって、時と共に値が大きくなっていきます。

AnimationController 他にも、初期値 value 、 下端 lowerBound 、 上端 upperBound を指定できます。 ですが、基本的に、初期値のまま使うのが間違いを減らすためによいと思います。

今回は、繰り返しのアニメーションなので、 _animationController.repeat() します。

最後に、 _curvedAnimation に先程定義した、 _ShakeCurve()curve として CurvedAnimation を代入します。 これによって、線形な値の変化 ( 0.0 から 1.0 ) が、なめらかな値に変換されます。

AnimationControllerのdispose

Statedispose される際に、必ず、 _animationController.dispose() しないといけません。忘れずにやりましょう。

  @override
  void dispose() {
    if (_animationController != null) {
      _animationController.dispose();
      _animationController = null;
    }
    super.dispose();
  }

のようにします。

描画部分

そして、描画部分で、 _curvedAnimation.value を使用して、 位置と、画像の切り替えを実現します。

  @override
  Widget build(BuildContext context) {
    double left = (320.0 - 128.0) / 2.0;
    left += _curvedAnimation.value;
    return ConstrainedBox(constraints: BoxConstraints.expand(width: 320.0, height: 128.0),
      child: Stack(
        fit: StackFit.expand,
        children: <Widget>[
          Positioned(
            left: left,
            top: 0.0,
            child: _image(_curvedAnimation.value),
          ),
        ],
      ),);
  }

のようにします。 Stack 直下に、 Positioned を入れて、位置を可変にします。 Positionedchild に後で説明する _image() を入れて完成です。

StackPositioned の間には、なにも入れてはいけません。 Positioned の親要素は、必ず Stack になるようにしましょう。

画像の切り替え

_curvedAnimation.value を適応することによって、渡ってきた、 value を見て適切な画像を出力します。

  Widget _image(double value) {
    if (value >= 0) {
      return Icon(Icons.arrow_forward, size: 128.0,);
    }
    return Icon(Icons.arrow_back, size: 128.0,);
  }

このようなコードになります。value0 以上であれば、右向きの矢印。 それ以外であれば、左向きの矢印を表示するようにしています。

コードの例

例えば、コードは以下のようになります。

import 'dart:math' as math;

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: 'わかりやすい'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(child: FingerWidget()),
    );
  }
}

class FingerWidget extends StatefulWidget {
  @override
  _FingerWidgetState createState() =>
      _FingerWidgetState();
}

class _FingerWidgetState extends State<FingerWidget>
    with TickerProviderStateMixin {
  AnimationController _animationController;
  CurvedAnimation _curvedAnimation;

  @override
  Widget build(BuildContext context) {
    double left = (320.0 - 128.0) / 2.0;
    left += _curvedAnimation.value;
    return ConstrainedBox(constraints: BoxConstraints.expand(width: 320.0, height: 128.0),
      child: Stack(
        fit: StackFit.expand,
        children: <Widget>[
          Positioned(
            left: left,
            top: 0.0,
            child: _image(_curvedAnimation.value),
          ),
        ],
      ),);
  }

  @override
  void dispose() {
    if (_animationController != null) {
      _animationController.dispose();
      _animationController = null;
    }
    super.dispose();
  }

  @override
  void initState() {
    _animationController =
    AnimationController(
      duration: const Duration(seconds: 3),
      vsync: this,)
      .addListener(() {
        setState(() {});
      });
    _animationController.repeat();
    _curvedAnimation =
        CurvedAnimation(parent: _animationController, curve: _ShakeCurve());
    super.initState();
  }

  Widget _image(double value) {
    if (value >= 0) {
      return Icon(Icons.arrow_forward, size: 128.0,);
    }
    return Icon(Icons.arrow_back, size: 128.0,);
  }
}

class _ShakeCurve extends Curve {
  @override
  double transform(double t) {
    return math.sin(t * math.pi * 2) * 64.0;
  }
}

スクリーンショット

矢印の向きが切り替わりながら左右に動きます。

参考リンク