Flutter 最优秀动画库「完全商业化」,Rive 2 你全面了解过吗?
说到 rive ,非 Flutter 开发者可能会感觉比较陌生,而做过 Flutter 开发的可能对 rive 会有所耳闻,因为 rive 在一开始叫 flare ,是 2dimensions 公司的开源动画产品,在发布之初由于和 Flutter 团队有深入合作,所以在初期一直是 Flutter 官方推荐的动画框架之一。
前言
rive 作为一个面向设计师的动画框架,他支持在 Web Editor 里进行 UI 编排和动画绘制,当然现在他也支持 PC 客户端开发,整体开发环境需求上相对 Lottie 会轻量化很多。
另外, rive 是通过导出矢量的动画数据文件(也可以包含一些静态资源),然后利用平台的 Canvas
来实现动画效果,所以它的资源占用体积也不会很大。
当然,rive 其实并不是只针对 Flutter, rive 现在也是全平台支持, Android、 iOS、Web、Desktop、Flutter 、React、Vue、C++ 等等都在支持范围之内。
关于 rive 的设计端的简单使用,可以看我之前的 《给掘金 Logo 快速添加动画效果》 ,其实对于程序员来说,rive 其实很好上手,打开一个 WebEdit 就可以编辑调整。
PS,第二代 rive 和第一代 flare 存在断档不兼容,而且基本可以忽略迁移的可能,当然, flare 和 rive 其实可以同时存在一个项目不会冲突,所以也不需要当心旧动画的升级问题。
Rive Flutter
开始进入主题,其实 rive 比 flare 使用起来更加简单,如下代码所示,只需要通过 RiveAnimation.asset
就可以实现一个下图里炫酷的动画效果,
dependencies:
rive: 0.9.0
import 'package:rive/rive.dart';
RiveAnimation.asset('static/file/launch.riv'),
当然,除了上面的 asset
,还可以通过 file
还有 network
等方式这加载,这也算是比较常规的集成方式。
那么使用 rive ,作为开发者端,需要简单知道的几个概念:
- Artboards:画布,rive 里至少会有一块画布,当然一个 riv 动画文件可以有多个画布
- animations:需要播放的动画
- StateMachine:状态机,可以将动画连接在一起并定义切换条件的支持
- Inputs:StateMachine 的输入,然后可用于与 StateMachine 交互并修改动画切换的状态
如下代码所示,一般情况下我们不需要关心上述设定,因为只要在设计时考虑好默认情况,那么只需要简单引入就可以播放动画。
RiveAnimation.asset('assets/33333.riv')
但是如果你需要更灵活的控制时,就需要理解上述这些设定的作用,后续才能和动画设计师进行有效的沟通和对接。
如下图所示就是对应的设定解读,例如:
- 知道了画布名称,就可以通过
artboard
切换画布 - 知道动画名称,就可以通过
animations
指定动画 - 知道了状态机名称,就可以通过
stateMachines
切换状态机 - 知道了状态条件,就可以通过
findInput
来切换条件变量
animations
我们先看 animations ,默认情况下 33333.riv
这个 riv 动画播放的是 Shaking
效果,从上图左下角可以看到 Shaking
是一个有循环♻️标识的动画,所以如下图所示车辆动画处于都懂状态。
RiveAnimation.asset('assets/33333.riv')
接着我们更新代码,添加了 animations
选择播放 "Jump"
,可以看到,车辆播放到了 Jump 效果的动画,并停留不动,因为 Jump 不是循环动画,所以只会播放一次,然后可以看到 Shaking
也没有了,因为我们只选中了 Jump
。
RiveAnimation.asset(
'assets/33333.riv',
animations: [
"Jump",
],
)
同样,如果我们多选中一个 Wheel
动画,可以看到车轮开始动起来,因为 Wheel
也是一个循环♻️动画,所以车轮可以一直滚动。
RiveAnimation.asset(
'assets/33333.riv',
animations: [
"Jump",
"Wheel",
],
)
所以通过 animations 我们可以快捷组合需要播放的动画效果。
stateMachines & Inputs
前面我们知道了可以通过 animations
配置动画,那么接下来再看看如何通过 stateMachines
来控制动画效果。
和 animations
一样,stateMachines
同样是一个List<String>
,也就是可以配置多个状态,例如通过前面编辑器我们知道,此时 33333.riv
的状态机只有一个 State Machine 1
,所以我们只需要配置上对应的 stateMachines
,就可以看到此时车辆动起来,进入状态机动画模式,也即是 Entry
。
RiveAnimation.asset(
'assets/33333.riv',
stateMachines: [
"State Machine 1"
],
那配置 stateMachines
只是进入 Entry
,如果要控制状态变化该怎么办?这就要说到 Inputs
。
获取 Inputs
我们需要在 _onRiveInit
回调里去获取,如下代码所示:
- 首先通过
StateMachineController.fromArtboard
获取到状态机的控制器,这样我们使用的是默认画板,所以直接使用初始化时传入的artboard
即可 fromArtboard
时通过State Machine 1
指定了状态机,然后通过onStateChange
监听状态机变化- 通过
addController
将获取到的状态机控制器添加到画布 - 通过
findInput
找到对应的控制状态SMIBool
- 调用
change
改变SMIBool
的 value 来切换动画状态
RiveAnimation.asset(
'assets/33333.riv',
onInit: _onRiveInit,
)
SMIBool? _skin;
void _onRiveInit(Artboard artboard) {
final controller = StateMachineController.fromArtboard(
artboard,
'State Machine 1',
onStateChange: _onStateChange,
);
artboard.addController(controller!);
_skin = controller.findInput<bool>('Boolean 1') as SMIBool;
}
void _onStateChange(String stateMachineName, String stateName) {
print("stateMachineName $stateMachineName stateName $stateName");
}
void _swapSkin() {
_skin?.change(!_skin!.value);
}
为什么这里是 SMIBool
? 因为在该状态机设定里用的是 Bool 类型条件。
当然,除了 Bool 还可以用数字作为判断条件,对应的 Type 类型也会变成 SMINumber
。
另外还有 SMITrigger
类型, SMITrigger
只需要通过 fire
和 advance
去控制动画的前后切换,变化也只能单路径模式一个一个切换。
回到最初的设定里,通过 _skin?.change(!_skin!.value);
切换 Bool 值的变化,可以看到此时车辆开始在 Jump 和 Down 进行变化,这就是最简单的状态机和 Input 的示例效果。
当然,如下图变高变胖的人就是通过 SMINumber
随意切换状态的效果,而小黑人换皮肤,就是通过 SMITrigger
单路径模式一个一个切换的动画效果。
当然,动画里也可能会包含多个不同类型的 Input ,你可以通过 StateMachineController
的 Inputs
参数去获取所有你需要的 Input 去控制动画效果。
其他
布局调整
其实了解上面哪些,大致上你就基本学会完美使用 rive 了,剩下的一些参数支持就都是小事,例如:
RiveAnimation.network(
'https://cdn.rive.app/animations/vehicles.riv',
fit: BoxFit.fitWidth,
alignment: Alignment.topCenter,
);
这里会用到 fit
和 alignment
,他们都是 Flutter 里常见的配置支持,这里就不多赘述,默认情况下是 BoxFit.Contain
和 Alignment.Center
。
文本支持
新版的 rive 支持运行过程中替换动画文件里的文本内容,前提是使用新版导出,然后需要编辑器中手动设置名称的文本才能支持该能力。
代码上简单说来说,就是在 onInit
的时候通过自定义的文本名称,然后通过 artboard
获取该节点,从而修改文本内容。
extension _TextExtension on Artboard {
TextValueRun? textRun(String name) => component<TextValueRun>(name);
}
RiveAnimation.asset(
'assets/hello_world_text.riv',
animations: const ['Timeline 1'],
onInit: (artboard) {
final textRun = artboard.textRun('MyRun')!; // find text run named "MyRun"
print('Run text used to be ${textRun.text}');
textRun.text = 'Hi Flutter Runtime!';
},
)
播放控制
现在的 rive 自带的 RiveAnimationController
对比 flare 弱化了很多,基本上就是用来实现简单的 play
、pause
和 stop
等,默认官方提供了 SimpleAnimation
和 OneShotAnimation
两种 RiveAnimationController
默认实现。
一般用不上自定义。
SimpleAnimation
主要是提供单个动画的简单播放控制,如 play、 pause (isActive
) 和 reset ,以下是官方 Demo 的示例,
class PlayPauseAnimation extends StatefulWidget {
const PlayPauseAnimation({Key? key}) : super(key: key);
@override
State<PlayPauseAnimation> createState() => _PlayPauseAnimationState();
}
class _PlayPauseAnimationState extends State<PlayPauseAnimation> {
/// Controller for playback
late RiveAnimationController _controller;
/// Toggles between play and pause animation states
void _togglePlay() =>
setState(() => _controller.isActive = !_controller.isActive);
/// Tracks if the animation is playing by whether controller is running
bool get isPlaying => _controller.isActive;
@override
void initState() {
super.initState();
_controller = SimpleAnimation('idle');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Animation Example'),
),
body: RiveAnimation.asset(
'assets/off_road_car.riv',
fit: BoxFit.cover,
controllers: [_controller],
// Update the play state when the widget's initialized
onInit: (_) => setState(() {}),
),
floatingActionButton: FloatingActionButton(
onPressed: _togglePlay,
tooltip: isPlaying ? 'Pause' : 'Play',
child: Icon(
isPlaying ? Icons.pause : Icons.play_arrow,
),
),
);
}
}
主要就是通过
isActive
来控制动画的暂停或者播放。
OneShotAnimation
主要用于在播放完一次动画后自动停止并重置动画,以下是官方 Demo 的示例,其实 OneShotAnimation
就是继承了 SimpleAnimation
,然后在其基础上增加了监听,在播放结束时调用 reset
重制动画而已。
/// Demonstrates playing a one-shot animation on demand
class PlayOneShotAnimation extends StatefulWidget {
const PlayOneShotAnimation({Key? key}) : super(key: key);
@override
State<PlayOneShotAnimation> createState() => _PlayOneShotAnimationState();
}
class _PlayOneShotAnimationState extends State<PlayOneShotAnimation> {
/// Controller for playback
late RiveAnimationController _controller;
/// Is the animation currently playing?
bool _isPlaying = false;
@override
void initState() {
super.initState();
_controller = OneShotAnimation(
'bounce',
autoplay: false,
onStop: () => setState(() => _isPlaying = false),
onStart: () => setState(() => _isPlaying = true),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('One-Shot Example'),
),
body: Center(
child: RiveAnimation.asset(
'assets/vehicles.riv',
animations: const ['idle', 'curves'],
fit: BoxFit.cover,
controllers: [_controller],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _isPlaying ? null : _controller.isActive = true,
tooltip: 'Bounce',
child: const Icon(Icons.arrow_upward),
),
);
}
}
上述代码就是在行驶过程中,点击是触发 'bounce'
的一次性跳跃效果,OneShotAnimation
主要就是用在类似的一次性动画场景上,
最后
可以看到 Rive 的使用其实很简单,但是因为状态机的实现,它又可以很灵活地去控制不同动画的效果。
一个 riv 文件内可以包含多个画板,画板里可以包含多个动画,多个状态机和输入条件,从而实现多样化的动画效果,甚至实现 Rive 版本的 Flutter 小游戏场景。
而且 Rive 并不只是支持 Flutter ,它如今几乎支持所有你能想到的平台,那么这样的一个优秀的平台有什么缺点呢?
那就是 Rive 最近开始收费了,完全的商业化产品, 其实不给钱你也可以用,只是 Free 模式下已经不是以前那个眉清目秀的 Rive 了。
Free 模式的 Rive 会有多个如下图所示的 Make with Rive
的水印,同时现在 Free 模式不支持 Share links 了,也就是你自己体验一下,要投入生产使用还是得付费。
那么有机智的小伙伴可能就要说了, Rive 不是开源的吗?那我们可以自己弄一套免费的吗?
答案是可以,但是成本无疑巨大,因为 Rive 的门槛不在于它开源的端侧 SDK ,而是在于设计端和产出端,目前的水印是在导出时强制加上的,所以对于使用 Rive 的用户来说,自己搭一套明显不现实。
那么,最后,你会愿意为这样一套产品而付费吗?反正我是已经付费ing了。