🟥

Signals Animation

2024/09/16に公開

アニメーションも作れるのか!

最近話題のSignalsというパッケージで遊んでます。公式のサンプルがあったのですが動かしてみるとこんなアニメーション作れるようです。

https://youtube.com/shorts/WcKD_0HSVn8

これがサンプルコード

https://github.com/rodydavis/signals.dart/blob/main/examples/animations_example/lib/main.dart

このコードは、多数の小さな赤い四角形がランダムに動き回るアニメーションを作成しています。主な仕組みは以下の通りです:

  1. アニメーションコントローラー:
    controllerはアニメーションの全体的なタイミングを制御します。3秒間の周期で前後に繰り返すように設定されています。

  2. カーブアニメーション:
    curveCurves.easeInOutを使用して、アニメーションの加速と減速を滑らかにします。

  3. 円(実際は四角形)の生成:
    circlesリストには100個の四角形のアニメーション情報が格納されます。各四角形に対して:

    • ランダムな開始位置と終了位置が生成されます。
    • サイズは1〜10ピクセルのランダムな正方形です。
    • RectTweenを使用して、開始位置から終了位置へのアニメーションが定義されます。
  4. アニメーションの表示:
    Stackウィジェット内で、各四角形に対してAnimatedPositioned.fromRectウィジェットが使用されます。これにより、四角形の位置が滑らかにアニメーションされます。

  5. リアクティブな更新:
    WatchウィジェットとvalueListenableToSignalを使用して、アニメーションの状態変化を監視し、UIを更新します。

  6. レイアウトサイズの適応:
    MediaQuery.sizeOf(context)を使用して、画面サイズに基づいてアニメーション範囲を調整しています。

このアニメーションの特徴は、多数の独立した要素が同時に動くことと、それぞれの動きがランダムでありながら滑らかな曲線に従っていることです。全体として、画面上で複雑で動的な視覚効果を生み出しています。

全体のコード

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:signals/signals_flutter.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final ticker = tickerSignal();

  late final controller = ticker.toAnimationController(
    duration: const Duration(seconds: 3),
  )..repeat(
      reverse: true,
      period: const Duration(seconds: 3),
    );

  late final curve = CurvedAnimation(
    parent: controller,
    curve: Curves.easeInOut,
  );

  final size = signal(const Size(0, 0));

  late final circles = List.generate(
    100,
    (index) {
      final rdm = Random(index);
      final start = Offset(
        rdm.nextInt(this.size.value.width.toInt()).toDouble(),
        rdm.nextInt(this.size.value.height.toInt()).toDouble(),
      );
      final end = Offset(
        rdm.nextInt(this.size.value.width.toInt()).toDouble(),
        rdm.nextInt(this.size.value.height.toInt()).toDouble(),
      );
      final size = Size.square(min(10, rdm.nextInt(100).toDouble()));
      final startRect = start & size;
      final endRect = end & size;
      final animation = RectTween(
        begin: startRect,
        end: endRect,
      ).animate(curve);
      return valueListenableToSignal(animation);
    },
  );

  
  Widget build(BuildContext context) {
    size.value = MediaQuery.sizeOf(context);
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Stack(
        children: [
          for (final rect in circles)
            Watch((context) {
              return AnimatedPositioned.fromRect(
                rect: rect.value!,
                duration: kThemeAnimationDuration,
                child: Container(
                  color: Colors.red,
                  child: const SizedBox.expand(),
                ),
              );
            }),
        ],
      ),
    );
  }
}

Ticker signal used to drive animations and can create animation controllers

アニメーションの駆動に使用され、アニメーション・コントローラを作成できるティッカー信号

シグナルって信号って意味でしたかね。。。

tickerSignalの内部実装を見てみた。コードジャンプするとこんなコードがあった!

CurvedAnimationの内部実装も見てみる
/// Ticker signal used to drive animations and can create animation controllers
///
/// ```dart
/// void main() {
///   final ticker = tickerSignal(); // could be a global
///   final controller = ticker.toAnimationController(); // can be local or global
///   final curve = CurvedAnimation(parent: controller, curve: Curves.easeOut); // can be used outside of widget tree
///   final alpha = IntTween(begin: 0, end: 255).animate(curve);
///   ...
///   final alphaSignal = alpha.toSignal(); // can be converted to a signal
/// }
/// ```
TickerSignal tickerSignal({
  Duration? initialDuration,
  String? debugLabel,
}) {
  return TickerSignal(
    initialDuration: initialDuration,
    debugLabel: debugLabel,
  );
}
// coverage:ignore-end

>/// An animation that applies a curve to another animation.
///
/// [CurvedAnimation] is useful when you want to apply a non-linear [Curve] to
/// an animation object, especially if you want different curves when the
/// animation is going forward vs when it is going backward.
///
/// Depending on the given curve, the output of the [CurvedAnimation] could have
/// a wider range than its input. For example, elastic curves such as
/// [Curves.elasticIn] will significantly overshoot or undershoot the default
/// range of 0.0 to 1.0.
///
/// If you want to apply a [Curve] to a [Tween], consider using [CurveTween].
///
/// {@tool snippet}
///
/// The following code snippet shows how you can apply a curve to a linear
/// animation produced by an [AnimationController] `controller`.
///
/// ```dart
/// final Animation<double> animation = CurvedAnimation(
///   parent: controller,
///   curve: Curves.ease,
/// );
/// ```
/// {@end-tool}
/// {@tool snippet}
///
/// This second code snippet shows how to apply a different curve in the forward
/// direction than in the reverse direction. This can't be done using a
/// [CurveTween] (since [Tween]s are not aware of the animation direction when
/// they are applied).
///
/// ```dart
/// final Animation<double> animation = CurvedAnimation(
///   parent: controller,
///   curve: Curves.easeIn,
///   reverseCurve: Curves.easeOut,
/// );
/// ```
/// {@end-tool}
///
/// By default, the [reverseCurve] matches the forward [curve].
///
/// See also:
///
///  * [CurveTween], for an alternative way of expressing the first sample
///    above.
///  * [AnimationController], for examples of creating and disposing of an
///    [AnimationController].
///  * [Curve.flipped] and [FlippedCurve], which provide the reverse of a
///    [Curve].
class CurvedAnimation extends Animation<double> with AnimationWithParentMixin<double> {
  /// Creates a curved animation.
  CurvedAnimation({
    required this.parent,
    required this.curve,
    this.reverseCurve,
  }) {
    // TODO(polina-c): stop duplicating code across disposables
    // https://github.com/flutter/flutter/issues/137435
    if (kFlutterMemoryAllocationsEnabled) {
      FlutterMemoryAllocations.instance.dispatchObjectCreated(
        library: 'package:flutter/animation.dart',
        className: '$CurvedAnimation',
        object: this,
      );
    }
    _updateCurveDirection(parent.status);
    parent.addStatusListener(_updateCurveDirection);
  }

  /// The animation to which this animation applies a curve.
  
  final Animation<double> parent;

  /// The curve to use in the forward direction.
  Curve curve;

  /// The curve to use in the reverse direction.
  ///
  /// If the parent animation changes direction without first reaching the
  /// [AnimationStatus.completed] or [AnimationStatus.dismissed] status, the
  /// [CurvedAnimation] stays on the same curve (albeit in the opposite
  /// direction) to avoid visual discontinuities.
  ///
  /// If you use a non-null [reverseCurve], you might want to hold this object
  /// in a [State] object rather than recreating it each time your widget builds
  /// in order to take advantage of the state in this object that avoids visual
  /// discontinuities.
  ///
  /// If this field is null, uses [curve] in both directions.
  Curve? reverseCurve;

  /// The direction used to select the current curve.
  ///
  /// The curve direction is only reset when we hit the beginning or the end of
  /// the timeline to avoid discontinuities in the value of any variables this
  /// animation is used to animate.
  AnimationStatus? _curveDirection;

  /// True if this CurvedAnimation has been disposed.
  bool isDisposed = false;

  void _updateCurveDirection(AnimationStatus status) {
    _curveDirection = switch (status) {
      AnimationStatus.dismissed || AnimationStatus.completed => null,
      AnimationStatus.forward || AnimationStatus.reverse => _curveDirection ?? status,
    };
  }

  bool get _useForwardCurve {
    return reverseCurve == null || (_curveDirection ?? parent.status) != AnimationStatus.reverse;
  }

  /// Cleans up any listeners added by this CurvedAnimation.
  void dispose() {
    // TODO(polina-c): stop duplicating code across disposables
    // https://github.com/flutter/flutter/issues/137435
    if (kFlutterMemoryAllocationsEnabled) {
      FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
    }
    isDisposed = true;
    parent.removeStatusListener(_updateCurveDirection);
  }

  
  double get value {
    final Curve? activeCurve = _useForwardCurve ? curve : reverseCurve;

    final double t = parent.value;
    if (activeCurve == null) {
      return t;
    }
    if (t == 0.0 || t == 1.0) {
      assert(() {
        final double transformedValue = activeCurve.transform(t);
        final double roundedTransformedValue = transformedValue.round().toDouble();
        if (roundedTransformedValue != t) {
          throw FlutterError(
            'Invalid curve endpoint at $t.\n'
            'Curves must map 0.0 to near zero and 1.0 to near one but '
            '${activeCurve.runtimeType} mapped $t to $transformedValue, which '
            'is near $roundedTransformedValue.',
          );
        }
        return true;
      }());
      return t;
    }
    return activeCurve.transform(t);
  }

  
  String toString() {
    if (reverseCurve == null) {
      return '$parent\u27A9$curve';
    }
    if (_useForwardCurve) {
      return '$parent\u27A9$curve\u2092\u2099/$reverseCurve';
    }
    return '$parent\u27A9$curve/$reverseCurve\u2092\u2099';
  }
}

翻訳すると

/// カーブを別のアニメーションに適用するアニメーション。
///
/// [CurvedAnimation] は、非線形な [Curve] を適用したい場合に便利です。
特に、アニメーションが前進しているときと後進しているときで、 /// 別のカーブを適用したい場合に便利です。
/// 特に、アニメーションが前進しているときと、後進しているときで、異なるカーブが必要な場合に便利です。
///
与えられたカーブによって、[CurvedAnimation] の出力は /// 入力よりも広い範囲を持つことができます。
/// 入力よりも広い範囲を持つことができます。例えば
/// [Curves.elasticIn]のようなエラスティックカーブは、デフォルトの
/// 0.0から1.0の範囲。
///
/// Tween]に[Curve]を適用したい場合は、[CurveTween]の使用を検討してください。
///
/// {@tool snippet} を使用します。
///
/// 次のコードスニペットは、[Animation] によって生成された直線的なアニメーションにカーブを適用する方法を示しています。
/// AnimationController] controller によって生成されたアニメーションにカーブを適用する方法を示します。
///
/// dart /// final Animation<double> animation = CurvedAnimation(). /// parent: controller、 /// curve: Curves.ease、 /// ); /// ``` /// {@end-tool} /// {@tool snippet}. /// /// この2番目のコード・スニペットは、順方向と逆方向で異なるカーブを適用する方法を示している。 /// 方向と逆方向で異なるカーブを適用する方法を示している。これは /// CurveTween]を使用して行うことはできません([Tween]は適用されるときにアニメーションの方向を認識しないため)。 /// Tween]はアニメーションの方向を意識しないため)。 /// /// dart
/// final Animation<double> animation = CurvedAnimation().
/// parent: controller、
/// curve: Curves.easeIn、
/// reverseCurve: Curves.easeOut、
/// );
/// ```
/// {@end-tool}.
///
/// デフォルトでは、[reverseCurve]は順方向の[curve]と一致する。
///
/// 参照:
///
/// * [CurveTween] を参照してください。
/// を参照してください。
/// * [AnimationController], /// [AnimationController]の作成と破棄の例。
/// [AnimationController].
/// * [Curve.flipped]と[FlippedCurve]。
/// [Curve].

感想

新しい状態管理のパッケージでアニメーションができるのを知って今後はやるのではと思い試して見たくなりましたが、普通のアニメーションまだわかってないのでもう少し勉強してからにしようかな。

Discussion