【Flutter】輝く光点のアニメーション周期を可変にする

# はじめに

前回の記事で光点のアニメーションを作りました。

https://qiita.com/GoHiromi/items/a3d24ebde8026a4b7817

その際に課題だった「アニメーション周期を可変にする方法」について、
一応解決していたのでまとめておきます。


# 完成品
スライダーで、アニメーション周期を1000～50msecの範囲で動的に変更できるようにした。
![光点周期.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/515993/66f2df1f-f427-8b74-7965-b1563870cff7.gif)

# ソース
ほとんど前回の記事のまんまですが、
とりあえず全部貼ります。

```dart
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

class LightSpotTest extends StatefulWidget {
  LightSpotTest({Key? key}) : super(key: key);

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

class _LightSpotTest extends State<LightSpotTest>
    with TickerProviderStateMixin {
  final _radius = 10.0; //基本光点のサイズ
  final _backRadius = 20.0; //後光のサイズ
  late AnimationController _animationController;
  late Animation<double> _animationRadius;
  var _duration = 500.0;

  @override
  void initState() {
    super.initState();
    //アニメーションコントローラの設定
    _animationController = AnimationController(
        duration: Duration(milliseconds: _duration.toInt()), vsync: this);
    //アニメーションの設定
    _animationRadius =
        Tween(begin: 0.0, end: _backRadius).animate(_animationController)
          ..addListener(() {
            setState(() {});
          });
    //初回アニメーション実行
    _animationController.forward();

    _animationController.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        //アニメーションを一旦リセット
        _animationController.reset();
        //アニメーション終了時、周期を再設定
        _animationController.duration =
            Duration(milliseconds: _duration.toInt());
        //アニメーション再実行
        _animationController.forward();
      }
    });
  }

  void dispose() {
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Container(
      margin: EdgeInsets.all(200),
      child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
        Text('周期：' + _duration.toInt().toString() + " msec"),
        Container(
          margin: EdgeInsets.only(bottom: 30),
          child: Slider(
            value: _duration,
            min: 50.0,
            max: 1000.0,
            onChanged: (value) {
              setState(() {
                _duration = value;
              });
            },
          ),
        ),
        CustomPaint(
          painter: _CirclePainter(
            _radius,
            _backRadius,
            _animationRadius.value,
          ),
        ),
      ]),
    ));
  }
}

//美しい光点クラス
class _CirclePainter extends CustomPainter {
  final lightColor = Colors.yellow; //光点の色
  //後光の設定
  final Map<int, Map<String, double>> lightLayers = {
    0: {
      'maxOpacity': 0.5, //後光の濃さ
    },
    1: {
      'maxOpacity': 0.3,
    },
  };

  double _basicRadius; //光点の基本サイズ
  double _maxBackRadius; //後光の最大サイズ
  double _animationRadius; //後光アニメーションサイズ

  _CirclePainter(this._basicRadius, this._maxBackRadius, this._animationRadius);

  @override
  void paint(Canvas canvas, Size size) {
    var c = Offset(0, 0); //光点の表示位置x,y
    //光点の実体を配置
    canvas.drawCircle(
      c,
      _basicRadius,
      Paint()
        ..color = lightColor
        ..style = PaintingStyle.fill, //塗り潰しの円
    );

    var size = _basicRadius;

    //後光配列ループ
    for (var i = 0; i < lightLayers.length; i++) {
      var row = lightLayers[i]!;
      //後光の透明度の算出
      var opacity =
          _animationRadius * (row['maxOpacity']! / _maxBackRadius * -1) +
              row['maxOpacity']!;
      //誤差でmax-min外れないように
      opacity = opacity < 0.0 ? 0.0 : opacity;
      opacity = opacity > 1.0 ? 1.0 : opacity;
      //後光サイズ
      size += _animationRadius;
      //後光描画
      canvas.drawCircle(
        c,
        size,
        Paint()
          ..style = PaintingStyle.fill //塗り潰しの円
          ..color = lightColor.withOpacity(opacity),
      );
    }
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
}
```

# 備考

一応、ポイントを２点説明しておきます。

## アニメーションの自動繰り返しをしない
repeatではなくforwardでアニメ実行します。
```dart
    //repeatではなくforwardでアニメ実行
    //_animationController.repeat(reverse: false);
    _animationController.forward();
```

## アニメーション終了時に周期を更新してアニメ再実行

AnimationController.addStatusListenerでアニメ状態変化時のイベントを定義して、
一連のアニメ終了時に周期を変更して再実行させます。
これによってアニメが中断されることは無く、きれいに周期変更できます。

:::note warn
周期の変更前にAnimationControllerのreset処理が必須です。
:::
```dart
    _animationController.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        //アニメーションを一旦リセット
        _animationController.reset();
        //アニメーション終了時、周期を再設定
        _animationController.duration =
            Duration(milliseconds: _duration.toInt());
        //アニメーション再実行
        _animationController.forward();
      }
    });
```

# さいごに
光るスライダーバー、光るテキストボックスについても解決しているので、
また暇な時にまとめてみます。