Flutter状态机动画插件state_machine_animation的使用

Flutter状态机动画插件state_machine_animation的使用

Goals

State-machine动画库旨在解决使用多个单独的动画控制器时代码复杂度迅速增加的问题。当一个元素需要使用数十个动画控制器,并且尝试根据它们之间的关系保持同步时,问题尤为突出。

虽然当前的最佳实践是让专门的动画运行时(如rive或flare)接管,但这样会导致你失去许多特定于Flutter的功能,以及对应用状态变化时动画行为的精细控制。

因此,该库的目标是提供:

  • 最简单的表面API,使你可以通过声明式和命令式编程方法的完美结合实现几乎所有的行为,同时保持可读性、清晰性和可维护性。
  • 最简单的实现方式,使用户能够轻松理解其代码库,克隆仓库并根据其独特需求进行调整。
  • 推迟对动画运行时的需求,直到需要像动画骨架和网格这样的功能,这些功能需要专用的用户界面来实现,并限制仅在这些功能上使用它们。

特点

  • 关键帧评估与插值,
  • 可以提供默认值和函数评估的持续时间和曲线评估,适用于动画定义的不同层次,
  • 动画模型容器,用于处理特定实体的多个动画属性,
  • 反应式方法确保连续性,可以处理层叠过渡,并具有不同的并发选项,以应对应用状态的变化。

开始使用

目前,表面级别的API设计得非常适合基于流的状态管理技术,如BLOC。

该库使用BehaviorSubject实例(具有当前值的流)来处理所有级别的状态。因此,熟悉流概念及其操作会有所帮助。

此外,鼓励大家克隆仓库并调整一些类,例如使用更高效和同步的ValueNotifier实例,或者为状态机实例采用更声明性的方法而不是对象继承。

基本状态机表示

一个示例状态机配置

使用

它考虑了三种不同级别的流:

  • 实体状态流,作为状态机的输入。其值需要包含动画应该响应的所有信息。
  • 状态机输出流,代表动画控制器状态。
  • 动画属性或动画模型流,评估控制器状态,您的应用可以使用它来渲染动画对象。

以下是一个完整的示例:

void main() {
  // 创建一个简单的TickerProvider扩展,负责管理Ticker的创建和销毁。
  final AppTickerManager tickerManager = AppTickerManager();

  // 实体状态流,用于动画的对象。在这种情况下,AppState可以有三个Position值之一。
  final BehaviorSubject<AppState> stateSubject = BehaviorSubject<AppState>.seeded(AppState(Position.center));

  // 状态机控制器实例,告诉状态机流如何响应实体状态流的变化。
  final ExampleSM stateMachine = ExampleSM(stateSubject, tickerManager);

  // 最终的动画流,评估状态机控制器流。在这种情况下,它是一个双精度属性,我们为每个关键帧提供它的值。
  final animation = DoubleAnimationProperty<AppState>(
    keyEvaluator: (key, sourceState) {
      if (key == "LEFT") {
        return -100;
      } else if (key == "CENTER") {
        return 0;
      } else if (key == "RIGHT") {
        return 100;
      }
    }
  ).getAnimation(stateMachine.output);

  // 使用流订阅暴露动画的值。
  animation.listen((animationProperty) { 
    print("${animationProperty.time}: ${animationProperty.value}");
  });

  // 改变输入流的值,从center到right,以便状态机可以反应并过渡到其他状态。
  stateSubject.add(AppState(Position.left));
}

// 源状态实现
enum Position {
  left,
  center,
  right;
}

class AppState extends Equatable {
  final Position position;

  const AppState(this.position);

  @override
  List<Object?> get props => [position];
}

// 状态机定义。实现此抽象类意味着实现以下三个钩子方法,这些方法会在输入状态发生变化时被调用。
class ExampleSM extends AnimationStateMachine<AppState> {
  ExampleSM(super.input, super.tickerManager);

  @override
  bool isReady(state) => true;

  @override
  AnimationStateMachineConfig<AppState> getConfig(state) => const AnimationStateMachineConfig(
    nodes: ["LEFT", "CENTER", "RIGHT"],
    initialState: Idle("CENTER"),
    defaultDuration: 1000
  );

  @override
  void reactToStateChanges(state, previous) {
    transitionTo(Idle(state.position.name.toUpperCase()));
  }
}

// 一个基本的Ticker管理器实现。如果你有一个游戏循环,它应该是实现此接口的。
class AppTickerManager implements TickerManager {
  final List<Ticker> _tickers = [];

  @override
  Ticker createTicker(TickerCallback onTick) {
    final ticker = Ticker(onTick);
    _tickers.add(ticker);
    return ticker;
  }

  @override
  void disposeTicker(Ticker ticker) {
    ticker.dispose();
    _tickers.remove(ticker);
  }
}

文档

AnimationStateMachine的使用

AnimationStateMachine是一个抽象类,通过扩展它来使用。

它负责根据源状态处理状态机的行为:

  • 源状态的就绪检查,
  • 动画节点,
  • 节点之间的过渡持续时间,
  • 状态机对节点变化的反应,
  • 可选地,在过渡中覆盖默认的关键帧。

这些都应该通过此实例的相关钩子进行配置。

isReady钩子
getConfig钩子
reactToStateChanges钩子

使用案例包括:

  • 跳转到Idle状态
  • 默认过渡到Idle状态
  • 跳转到默认InTransition状态
  • 执行命名过渡(带有自定义关键帧)到Idle状态
  • 自命名SelfTransition(带有自定义关键帧)
  • 跳转到命名InTransition状态
并发行为

当在AnimationStateMachine实例的reactToStateChanges钩子中调用transitionTo方法时,您可以提供一个TransitionConcurrencyBehavior值。这将改变状态机在已有过渡正在进行时对过渡尝试的反应方式。

AnimationProperty的使用

当状态机用于管理单个属性时,您应该使用AnimationProperty<T, S>类或其扩展作为快捷方式。

动画属性实例负责通过关键帧和插值评估状态机并得出结果值,以及确定过渡应如何解释的曲线。

DoubleAnimationProperty的使用
final animation = DoubleAnimationProperty<AppState>(
  keyEvaluator: (key, sourceState) {
    if (key == "NODE_1") {
      return -100;
    } else if (key == "NODE_2") {
      return 0;
    } else if (key == "NODE_3") {
      return 100;
    }
  }
).getAnimation(stateMachine.output);
定制AnimationProperty的使用
final animation = AnimationProperty<double, AppState>(
  keyEvaluator: (key, sourceState) {
    if (key == "NODE_1") {
      return -100;
    } else if (key == "NODE_2") {
      return 0;
    }
  }
).getAnimation(stateMachine.output);

要接收动画流,应使用动画属性定义的getAnimation方法,并传入状态机流。

返回的类型为AnimationPropertyState<T>的流将包含以下信息:

  • 方向
  • 速度
  • 时间

现有AnimationProperty类的扩展包括:

  • IntegerAnimationProperty
  • DoubleAnimationProperty
  • ModdedDoubleAnimationProperty
  • SizeAnimationProperty
  • ColorAnimationProperty
  • BoolAnimationProperty
  • StringAnimationProperty
AnimationContainer和AnimationModel的使用

当状态机用于管理由多个属性表示的元素时,您应该使用AnimationContainerAnimationModel类。

动画容器是方便的类,它们持有多个动画属性和它们之间的共同行为。

它们负责序列化动画属性和源状态到它们相关的AnimationModel类中。

它们提供了一个动画模型的输出流。

动画模型是简单的数据类,实现了copyWith方法,这使得容器知道如何将动画属性映射到其字段。

class AwesomeObjectAnimation extends AnimationContainer<AwesomeSourceState, AwesomeObject> {
  AwesomeObjectAnimation(AwesomeObjectStateMachine stateMachine) : super(
    stateMachine: stateMachine,
    initial: AwesomeObject.empty(),
    defaultCurve: Curves.easeInOutQuad,
    staticPropertySerializer: (state) => {
      "name": state.name // 示例:动画模型类中的非动画静态属性
    },
    properties: [
      DoubleAnimationProperty(
        name: "x",
        keyEvaluator: (key, sourceState) {
          if (key == "NODE_1") {
            return 0;
          } else if (key == "NODE_2") {
            return 100;
          }
        }
      ),
      DoubleAnimationProperty(
        name: "y",
        evaluateCurve: (transition) => transition.from == const Idle("NODE_2") && transition.to == const Idle("NODE_1")
          ? Curves.bounceOut 
          : Curves.easeInOutQuad,
        keyEvaluator: (key, sourceState) {
          if (key == "NODE_1") {
            return 0;
          } else if (key == "NODE_2") {
            return 100;
          }
        }
      ),
      DoubleAnimationProperty(
        name: "scale",
        keyEvaluator: (key, sourceState) {
          if (key == "NODE_1") {
            return 1;
          } else if (key == "NODE_2") {
            return 2;
          }
        }
      ),
      DoubleAnimationProperty<AppState>(
        name: "opacity",
        evaluateKeyframes: (transition, sourceState) => const [
          AnimationKeyframe(Idle("NODE_1"), 0), 
          AnimationKeyframe(Idle("KEYFRAME_1"), 0.2),
          AnimationKeyframe(Idle("KEYFRAME_2"), 0.4), 
          AnimationKeyframe(Idle("NODE_2"), 1)
        ],
        keyEvaluator: (key, sourceState){
          if (key == "NODE_1") {
            return 0.5;
          } else if (key == "KEYFRAME_1") {
            return 0.6;
          } else if (key == "KEYFRAME_2") {
            return 0.7;
          } else if (key == "NODE_2") {
            return 1;
          }
        }
      )
    ]
  );
}

class AwesomeObject extends AnimationModel {
  final double name;
  final double x;
  final double y;
  final double scale;
  final double opacity;

  AwesomeObject(
    this.name,
    this.x,
    this.y,
    this.scale,
    this.opacity,
  );

  AwesomeObject.empty() :
    name = "",
    x = 0,
    y = 0,
    scale = 1,
    opacity = 1;

  @override List<Object?> get props => [name, x, y, scale, opacity];

  @override
  AwesomeObject copyWith(Map<String, dynamic> valueMap) => AwesomeObject(
    valueMap["name"] ?? name,
    valueMap["x"] ?? x,
    valueMap["y"] ?? y,
    valueMap["scale"] ?? scale,
    valueMap["opacity"] ?? opacity
  );
}
使用BehaviorSubjectBuilder渲染动画

BehaviorSubjectBuilder是StreamBuilder的一个简单扩展,用于方便。

class ExampleWidget extends StatelessWidget {
  const ExampleWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BehaviorSubjectBuilder(
      subject: context.read<AwesomeObjectAnimation>(),
      subjectBuilder: (context, awesomeObject) => Container(
       /*.... */ 
      )
    );
  }
}
订阅动画事件的回调
// ...
final ExampleSM stateMachine = ExampleSM(stateSubject, tickerManager); 
//...
stateMachine.output.firstWhere((state) => state?.state.fromKey == "NODE_2").then((value){
  print("ON NODE_2");
});

更多关于Flutter状态机动画插件state_machine_animation的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter状态机动画插件state_machine_animation的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,下面是一个关于如何使用 state_machine_animation 插件的简单示例代码。这个插件可以帮助你在 Flutter 中实现基于状态机的动画。

首先,确保你已经在 pubspec.yaml 文件中添加了 state_machine_animation 依赖:

dependencies:
  flutter:
    sdk: flutter
  state_machine_animation: ^x.y.z  # 请替换为最新版本号

然后运行 flutter pub get 来获取依赖。

以下是一个简单的示例,展示如何使用 state_machine_animation 来创建一个基于状态机的动画:

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('State Machine Animation Example'),
        ),
        body: StateMachineAnimationExample(),
      ),
    );
  }
}

class StateMachineAnimationExample extends StatefulWidget {
  @override
  _StateMachineAnimationExampleState createState() => _StateMachineAnimationExampleState();
}

class _StateMachineAnimationExampleState extends State<StateMachineAnimationExample> with SingleTickerProviderStateMixin {
  late StateMachine<String, AnimationStatus> _stateMachine;
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    )..repeat(reverse: true);

    final stateTransitions = {
      'idle': StateTransition<String, AnimationStatus>(
        'loading',
        (context, animation) => animation.status == AnimationStatus.forward,
      ),
      'loading': StateTransition<String, AnimationStatus>(
        'idle',
        (context, animation) => animation.status == AnimationStatus.dismissed,
      ),
    };

    _stateMachine = StateMachine<String, AnimationStatus>(
      initialState: 'idle',
      stateTransitions: stateTransitions,
      build: (context, state) {
        return AnimatedBuilder(
          animation: _controller,
          child: Center(
            child: Text(
              state,
              style: TextStyle(fontSize: 24),
            ),
          ),
          builder: (context, child) {
            return Transform.scale(
              scale: _controller.value,
              child: child,
            );
          },
        );
      },
    );
  }

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

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Expanded(child: _stateMachine.stateWidget),
        ElevatedButton(
          onPressed: () {
            // Trigger state transition manually if needed,
            // but in this case it's handled by the animation status.
            // _stateMachine.transitionTo('loading');
          },
          child: Text('Trigger State Transition'),
        ),
      ],
    );
  }
}

在这个示例中:

  1. 我们创建了一个 StateMachine 对象,该对象有两个状态:idleloading。状态之间的转换基于动画的状态 (AnimationStatus)。
  2. 使用 AnimationController 来控制动画,动画在 idleloading 状态之间循环切换。
  3. build 方法中,我们根据当前状态显示相应的文本,并使用 AnimatedBuilderTransform.scale 来根据动画值缩放文本。
  4. 我们在 dispose 方法中释放 AnimationController,以避免内存泄漏。

这个示例展示了如何使用 state_machine_animation 插件来基于动画状态管理 UI 的状态转换。你可以根据需要扩展这个示例,添加更多的状态和转换条件。

回到顶部