Flutter 最优秀动画库「完全商业化」,Rive 2 你全面了解过吗?

说到 rive ,非 Flutter 开发者可能会感觉比较陌生,而做过 Flutter 开发的可能对 rive 会有所耳闻,因为 rive 在一开始叫 flare ,是 2dimensions 公司的开源动画产品,在发布之初由于和 Flutter 团队有深入合作,所以在初期一直是 Flutter 官方推荐的动画框架之一。

Flutter 最优秀动画库「完全商业化」,Rive 2 你全面了解过吗? - 图1

前言

rive 作为一个面向设计师的动画框架,他支持在 Web Editor 里进行 UI 编排和动画绘制,当然现在他也支持 PC 客户端开发,整体开发环境需求上相对 Lottie 会轻量化很多。

Flutter 最优秀动画库「完全商业化」,Rive 2 你全面了解过吗? - 图2 Flutter 最优秀动画库「完全商业化」,Rive 2 你全面了解过吗? - 图3

另外, rive 是通过导出矢量的动画数据文件(也可以包含一些静态资源),然后利用平台的 Canvas来实现动画效果,所以它的资源占用体积也不会很大。

当然,rive 其实并不是只针对 Flutter, rive 现在也是全平台支持, Android、 iOS、Web、Desktop、Flutter 、React、Vue、C++ 等等都在支持范围之内。

Flutter 最优秀动画库「完全商业化」,Rive 2 你全面了解过吗? - 图4

关于 rive 的设计端的简单使用,可以看我之前的 《给掘金 Logo 快速添加动画效果》 ,其实对于程序员来说,rive 其实很好上手,打开一个 WebEdit 就可以编辑调整。

Flutter 最优秀动画库「完全商业化」,Rive 2 你全面了解过吗? - 图5

PS,第二代 rive 和第一代 flare 存在断档不兼容,而且基本可以忽略迁移的可能,当然, flare 和 rive 其实可以同时存在一个项目不会冲突,所以也不需要当心旧动画的升级问题

Rive Flutter

开始进入主题,其实 rive 比 flare 使用起来更加简单,如下代码所示,只需要通过 RiveAnimation.asset 就可以实现一个下图里炫酷的动画效果,

  1. dependencies:
  2. rive: 0.9.0
  3. import 'package:rive/rive.dart';
  4. RiveAnimation.asset('static/file/launch.riv'),

Flutter 最优秀动画库「完全商业化」,Rive 2 你全面了解过吗? - 图6

当然,除了上面的 asset ,还可以通过 file 还有 network 等方式这加载,这也算是比较常规的集成方式。

Flutter 最优秀动画库「完全商业化」,Rive 2 你全面了解过吗? - 图7

那么使用 rive ,作为开发者端,需要简单知道的几个概念:

  • Artboards:画布,rive 里至少会有一块画布,当然一个 riv 动画文件可以有多个画布
  • animations:需要播放的动画
  • StateMachine:状态机,可以将动画连接在一起并定义切换条件的支持
  • Inputs:StateMachine 的输入,然后可用于与 StateMachine 交互并修改动画切换的状态

如下代码所示,一般情况下我们不需要关心上述设定,因为只要在设计时考虑好默认情况,那么只需要简单引入就可以播放动画。

  1. RiveAnimation.asset('assets/33333.riv')

但是如果你需要更灵活的控制时,就需要理解上述这些设定的作用,后续才能和动画设计师进行有效的沟通和对接

如下图所示就是对应的设定解读,例如:

  • 知道了画布名称,就可以通过 artboard 切换画布
  • 知道动画名称,就可以通过 animations 指定动画
  • 知道了状态机名称,就可以通过 stateMachines 切换状态机
  • 知道了状态条件,就可以通过 findInput 来切换条件变量

Flutter 最优秀动画库「完全商业化」,Rive 2 你全面了解过吗? - 图8

animations

我们先看 animations ,默认情况下 33333.riv 这个 riv 动画播放的是 Shaking 效果,从上图左下角可以看到 Shaking 是一个有循环♻️标识的动画,所以如下图所示车辆动画处于都懂状态。

  1. RiveAnimation.asset('assets/33333.riv')

Flutter 最优秀动画库「完全商业化」,Rive 2 你全面了解过吗? - 图9

接着我们更新代码,添加了 animations 选择播放 "Jump" ,可以看到,车辆播放到了 Jump 效果的动画,并停留不动,因为 Jump 不是循环动画,所以只会播放一次,然后可以看到 Shaking 也没有了,因为我们只选中了 Jump

  1. RiveAnimation.asset(
  2. 'assets/33333.riv',
  3. animations: [
  4. "Jump",
  5. ],
  6. )

Flutter 最优秀动画库「完全商业化」,Rive 2 你全面了解过吗? - 图10

同样,如果我们多选中一个 Wheel 动画,可以看到车轮开始动起来,因为 Wheel 也是一个循环♻️动画,所以车轮可以一直滚动。

  1. RiveAnimation.asset(
  2. 'assets/33333.riv',
  3. animations: [
  4. "Jump",
  5. "Wheel",
  6. ],
  7. )

Flutter 最优秀动画库「完全商业化」,Rive 2 你全面了解过吗? - 图11

所以通过 animations 我们可以快捷组合需要播放的动画效果。

stateMachines & Inputs

前面我们知道了可以通过 animations 配置动画,那么接下来再看看如何通过 stateMachines 来控制动画效果。

animations 一样,stateMachines 同样是一个List<String>,也就是可以配置多个状态,例如通过前面编辑器我们知道,此时 33333.riv 的状态机只有一个 State Machine 1 ,所以我们只需要配置上对应的 stateMachines ,就可以看到此时车辆动起来,进入状态机动画模式,也即是 Entry

  1. RiveAnimation.asset(
  2. 'assets/33333.riv',
  3. stateMachines: [
  4. "State Machine 1"
  5. ],

Flutter 最优秀动画库「完全商业化」,Rive 2 你全面了解过吗? - 图12

Flutter 最优秀动画库「完全商业化」,Rive 2 你全面了解过吗? - 图13

那配置 stateMachines 只是进入 Entry,如果要控制状态变化该怎么办?这就要说到 Inputs

获取 Inputs 我们需要在 _onRiveInit 回调里去获取,如下代码所示:

  • 首先通过 StateMachineController.fromArtboard 获取到状态机的控制器,这样我们使用的是默认画板,所以直接使用初始化时传入的 artboard 即可
  • fromArtboard 时通过 State Machine 1 指定了状态机,然后通过onStateChange 监听状态机变化
  • 通过 addController 将获取到的状态机控制器添加到画布
  • 通过 findInput 找到对应的控制状态 SMIBool
  • 调用 change 改变 SMIBool 的 value 来切换动画状态
  1. RiveAnimation.asset(
  2. 'assets/33333.riv',
  3. onInit: _onRiveInit,
  4. )
  5. SMIBool? _skin;
  6. void _onRiveInit(Artboard artboard) {
  7. final controller = StateMachineController.fromArtboard(
  8. artboard,
  9. 'State Machine 1',
  10. onStateChange: _onStateChange,
  11. );
  12. artboard.addController(controller!);
  13. _skin = controller.findInput<bool>('Boolean 1') as SMIBool;
  14. }
  15. void _onStateChange(String stateMachineName, String stateName) {
  16. print("stateMachineName $stateMachineName stateName $stateName");
  17. }
  18. void _swapSkin() {
  19. _skin?.change(!_skin!.value);
  20. }

为什么这里是 SMIBool ? 因为在该状态机设定里用的是 Bool 类型条件。

Flutter 最优秀动画库「完全商业化」,Rive 2 你全面了解过吗? - 图14

当然,除了 Bool 还可以用数字作为判断条件,对应的 Type 类型也会变成 SMINumber

Flutter 最优秀动画库「完全商业化」,Rive 2 你全面了解过吗? - 图15

另外还有 SMITrigger 类型, SMITrigger 只需要通过 fireadvance 去控制动画的前后切换,变化也只能单路径模式一个一个切换。

Flutter 最优秀动画库「完全商业化」,Rive 2 你全面了解过吗? - 图16

回到最初的设定里,通过 _skin?.change(!_skin!.value); 切换 Bool 值的变化,可以看到此时车辆开始在 Jump 和 Down 进行变化,这就是最简单的状态机和 Input 的示例效果。

Flutter 最优秀动画库「完全商业化」,Rive 2 你全面了解过吗? - 图17

当然,如下图变高变胖的人就是通过 SMINumber 随意切换状态的效果,而小黑人换皮肤,就是通过 SMITrigger 单路径模式一个一个切换的动画效果。

777777-2 Flutter 最优秀动画库「完全商业化」,Rive 2 你全面了解过吗? - 图19

当然,动画里也可能会包含多个不同类型的 Input ,你可以通过 StateMachineControllerInputs 参数去获取所有你需要的 Input 去控制动画效果。

Flutter 最优秀动画库「完全商业化」,Rive 2 你全面了解过吗? - 图20

其他

布局调整

其实了解上面哪些,大致上你就基本学会完美使用 rive 了,剩下的一些参数支持就都是小事,例如:

  1. RiveAnimation.network(
  2. 'https://cdn.rive.app/animations/vehicles.riv',
  3. fit: BoxFit.fitWidth,
  4. alignment: Alignment.topCenter,
  5. );

这里会用到 fitalignment ,他们都是 Flutter 里常见的配置支持,这里就不多赘述,默认情况下是 BoxFit.ContainAlignment.Center

文本支持

新版的 rive 支持运行过程中替换动画文件里的文本内容,前提是使用新版导出,然后需要编辑器中手动设置名称的文本才能支持该能力

Flutter 最优秀动画库「完全商业化」,Rive 2 你全面了解过吗? - 图21

代码上简单说来说,就是在 onInit 的时候通过自定义的文本名称,然后通过 artboard 获取该节点,从而修改文本内容。

  1. extension _TextExtension on Artboard {
  2. TextValueRun? textRun(String name) => component<TextValueRun>(name);
  3. }
  4. RiveAnimation.asset(
  5. 'assets/hello_world_text.riv',
  6. animations: const ['Timeline 1'],
  7. onInit: (artboard) {
  8. final textRun = artboard.textRun('MyRun')!; // find text run named "MyRun"
  9. print('Run text used to be ${textRun.text}');
  10. textRun.text = 'Hi Flutter Runtime!';
  11. },
  12. )

播放控制

现在的 rive 自带的 RiveAnimationController 对比 flare 弱化了很多,基本上就是用来实现简单的 playpausestop 等,默认官方提供了 SimpleAnimationOneShotAnimation 两种 RiveAnimationController 默认实现。

一般用不上自定义。

SimpleAnimation 主要是提供单个动画的简单播放控制,如 play、 pause (isActive) 和 reset ,以下是官方 Demo 的示例,

  1. class PlayPauseAnimation extends StatefulWidget {
  2. const PlayPauseAnimation({Key? key}) : super(key: key);
  3. @override
  4. State<PlayPauseAnimation> createState() => _PlayPauseAnimationState();
  5. }
  6. class _PlayPauseAnimationState extends State<PlayPauseAnimation> {
  7. /// Controller for playback
  8. late RiveAnimationController _controller;
  9. /// Toggles between play and pause animation states
  10. void _togglePlay() =>
  11. setState(() => _controller.isActive = !_controller.isActive);
  12. /// Tracks if the animation is playing by whether controller is running
  13. bool get isPlaying => _controller.isActive;
  14. @override
  15. void initState() {
  16. super.initState();
  17. _controller = SimpleAnimation('idle');
  18. }
  19. @override
  20. Widget build(BuildContext context) {
  21. return Scaffold(
  22. appBar: AppBar(
  23. title: const Text('Animation Example'),
  24. ),
  25. body: RiveAnimation.asset(
  26. 'assets/off_road_car.riv',
  27. fit: BoxFit.cover,
  28. controllers: [_controller],
  29. // Update the play state when the widget's initialized
  30. onInit: (_) => setState(() {}),
  31. ),
  32. floatingActionButton: FloatingActionButton(
  33. onPressed: _togglePlay,
  34. tooltip: isPlaying ? 'Pause' : 'Play',
  35. child: Icon(
  36. isPlaying ? Icons.pause : Icons.play_arrow,
  37. ),
  38. ),
  39. );
  40. }
  41. }

主要就是通过 isActive 来控制动画的暂停或者播放。

Flutter 最优秀动画库「完全商业化」,Rive 2 你全面了解过吗? - 图22

OneShotAnimation 主要用于在播放完一次动画后自动停止并重置动画,以下是官方 Demo 的示例,其实 OneShotAnimation 就是继承了 SimpleAnimation ,然后在其基础上增加了监听,在播放结束时调用 reset 重制动画而已。

  1. /// Demonstrates playing a one-shot animation on demand
  2. class PlayOneShotAnimation extends StatefulWidget {
  3. const PlayOneShotAnimation({Key? key}) : super(key: key);
  4. @override
  5. State<PlayOneShotAnimation> createState() => _PlayOneShotAnimationState();
  6. }
  7. class _PlayOneShotAnimationState extends State<PlayOneShotAnimation> {
  8. /// Controller for playback
  9. late RiveAnimationController _controller;
  10. /// Is the animation currently playing?
  11. bool _isPlaying = false;
  12. @override
  13. void initState() {
  14. super.initState();
  15. _controller = OneShotAnimation(
  16. 'bounce',
  17. autoplay: false,
  18. onStop: () => setState(() => _isPlaying = false),
  19. onStart: () => setState(() => _isPlaying = true),
  20. );
  21. }
  22. @override
  23. Widget build(BuildContext context) {
  24. return Scaffold(
  25. appBar: AppBar(
  26. title: const Text('One-Shot Example'),
  27. ),
  28. body: Center(
  29. child: RiveAnimation.asset(
  30. 'assets/vehicles.riv',
  31. animations: const ['idle', 'curves'],
  32. fit: BoxFit.cover,
  33. controllers: [_controller],
  34. ),
  35. ),
  36. floatingActionButton: FloatingActionButton(
  37. onPressed: () => _isPlaying ? null : _controller.isActive = true,
  38. tooltip: 'Bounce',
  39. child: const Icon(Icons.arrow_upward),
  40. ),
  41. );
  42. }
  43. }

上述代码就是在行驶过程中,点击是触发 'bounce' 的一次性跳跃效果,OneShotAnimation 主要就是用在类似的一次性动画场景上,

Flutter 最优秀动画库「完全商业化」,Rive 2 你全面了解过吗? - 图23

最后

可以看到 Rive 的使用其实很简单,但是因为状态机的实现,它又可以很灵活地去控制不同动画的效果。

一个 riv 文件内可以包含多个画板,画板里可以包含多个动画,多个状态机和输入条件,从而实现多样化的动画效果,甚至实现 Rive 版本的 Flutter 小游戏场景。

而且 Rive 并不只是支持 Flutter ,它如今几乎支持所有你能想到的平台,那么这样的一个优秀的平台有什么缺点呢?

那就是 Rive 最近开始收费了,完全的商业化产品, 其实不给钱你也可以用,只是 Free 模式下已经不是以前那个眉清目秀的 Rive 了

Flutter 最优秀动画库「完全商业化」,Rive 2 你全面了解过吗? - 图24

Free 模式的 Rive 会有多个如下图所示的 Make with Rive 的水印,同时现在 Free 模式不支持 Share links 了,也就是你自己体验一下,要投入生产使用还是得付费。

Flutter 最优秀动画库「完全商业化」,Rive 2 你全面了解过吗? - 图25

Flutter 最优秀动画库「完全商业化」,Rive 2 你全面了解过吗? - 图26

那么有机智的小伙伴可能就要说了, Rive 不是开源的吗?那我们可以自己弄一套免费的吗?

答案是可以,但是成本无疑巨大,因为 Rive 的门槛不在于它开源的端侧 SDK ,而是在于设计端和产出端,目前的水印是在导出时强制加上的,所以对于使用 Rive 的用户来说,自己搭一套明显不现实。

那么,最后,你会愿意为这样一套产品而付费吗?反正我是已经付费ing了。