Flutter动画新纪元:Rive助力打造炫酷交互
导读:Flutter 生态中,动画一直是提升用户体验的重要利器。而 Rive(https://rive.app/)作为一款实时交互动画设计工具,凭借轻量、高性能和可编程特性,正在为 Flutter 带来“动画新纪元”。本文从“什么是 Rive”讲起,到“如何将 Rive 与 Flutter 结合”,再到“实战案例代码”,“图解”关键流程与原理,帮助你快速上手,打造出炫酷又高效的交互动画。
目录
- 什么是 Rive?为什么用 Rive 而非传统 Lottie 或手写动画?
- 2.1 Artboard(画板)
- 2.2 Shapes 和 Layers(图形与层次)
- 2.3 Keyframes 与 Timelines(关键帧与时间线)
- 2.4 State Machine(状态机)
- 3.1 准备 Rive 文件
.riv
- 3.2 添加依赖与配置
- 3.3 加载并显示静态动画
- 3.1 准备 Rive 文件
- 4.1 在 Rive 编辑器中创建一个 “Loading” 动画
- 4.2 Flutter 中显示动画的最简代码
- 4.3 图解:渲染流程与帧率优化
- 5.1 在 Rive 中制作带有状态机的按钮动画
- 5.2 Flutter 中读取
State Machine
并触发状态转换 - 5.3 代码详解与图解流程
- 6.1 通过 Flutter 代码实时修改 Rive 的属性(颜色、形状、数值)
- 6.2 双向绑定:监听 Rive 内部回调,触发 Flutter 逻辑
- 6.3 图解:属性流与事件流
- 7.1 限制渲染区域与使用
RiveAnimationController
- 7.2 减少不必要的
Artboard
reload - 7.3 AOT 下动态加载与预缓存
- 7.1 限制渲染区域与使用
- 总结与扩展思考
一、什么是 Rive?为什么用 Rive 而非传统 Lottie 或手写动画?
- Rive 是一款集“设计—交互—实时渲染”于一体的矢量动画创作工具。设计师在 Rive 编辑器中绘制图形、配置动画、编写状态机;开发者通过 Rive Flutter Runtime 将其无缝嵌入应用,高性能地渲染效果。
与 Lottie 对比
- Lottie(Bodymovin)主要依赖 After Effects 导出 JSON,逐帧播放,适用于简单动画,但对交互、双向绑定支持有限。
- Rive 支持 “实时” 动态控制:不仅可以播放预制动画,还能通过状态机在运行时根据业务逻辑触发不同动画,还可在 Flutter 侧实时修改 Rive 实例中的属性(如渐变颜色、大小、形状参数等)。
手写 Flutter 动画 vs Rive
- Flutter 自带
AnimationController
、Tween
、CustomPainter
等强大工具,但若动画效果需求复杂(例如交互性、可编辑的动态形变、循环节奏精准把控),需要耗费大量时间去调试。 - Rive 编辑器提供可视化、实时预览的创作环境,设计师与开发者协同无缝:设计师调好状态机、帧率、Bezier 曲线、缓动函数等后导出
.riv
,开发者只需加载并调用状态机接口。
- Flutter 自带
二、Rive 基础概念与术语解析
在踏入 Flutter 代码前,我们先梳理核心概念,帮助理解接下来所用到的文件结构与运行时逻辑。
2.1 Artboard(画板)
- Artboard 类似于 After Effects 的“合成”,是一个容纳所有图形与动画的容器。一个
.riv
文件中可以包含多个 Artboard,每个自由定义不同动画或 UI 组件。 在 Rive 编辑器中,Artboard 主界面的左上角即显示当前所选 Artboard 的名称。导出时,Flutter 端可通过名字获取对应的 Artboard 实例。
+─────────────────────────────────+ │ Artboard: LoadingScreen │ ← 画板名称 │ ┌─────────────────────────────┐ │ │ │ [Vector shapes, layers…] │ │ │ └─────────────────────────────┘ │ +─────────────────────────────────+
2.2 Shapes 和 Layers(图形与层次)
- 在 Artboard 内,你可以通过
Shapes
(矢量图形,如矩形、圆形、路径)与Layers
(图层分组)来组织素材。 每一个 Shape 都支持填充、渐变、Mask(蒙版)、裁剪路径等属性。Layers 支持 Blend Mode(混合模式)、Opacity(不透明度)等。
Artboard: LoadingAnim ├─ Layer 0: Background (Rectangle shape 填充渐变) ├─ Layer 1: Logo (Group: Path + Mask) └─ Layer 2: Spinner (Ellipse shape + Stroke)
2.3 Keyframes 与 Timelines(关键帧与时间线)
- Rive 使用 关键帧(Keyframe)在不同时间点记录属性值(位置、旋转、缩放、颜色等),并通过 Timeline 进行插值,自动填充中间帧(In-between)。
- 在 Rive 编辑器的下方就是时间线面板,可拖动查看任意帧效果,设置缓动曲线(Linear、Ease In/Out、Custom Bézier)等。
- Flutter 端无需关心具体时间线细节,只要调用对应 Animation Name 即可开始播放。
2.4 State Machine(状态机)
- State Machine 是 Rive 的核心交互机制。它允许设计师在 Rive 中定义“状态”(States)与“转换”(Transitions),并基于布尔、触发器(Triggers)、数值输入(Number inputs)等逻辑驱动不同动画片段。
举例:一个按钮可以有
idle
、hover
、pressed
三个状态,设计师在 Rive 中分别制作这三段动画,并在状态机中添加条件(如isHover == true
时从idle
跳到hover
;onPressed
触发后跳到pressed
),这样发布成.riv
后,Flutter 端只需控制状态机变量即可完成复杂交互动画。StateMachine: ButtonSM ┌─────────┐ onHover=true ┌──────────┐ │ idle │ ──────────────────▶ │ hover │ └─────────┘ └──────────┘ ▲ │ │ │ onHover=false onPressed ▼ │ ┌──────────┐ ┌─────────┐ │ pressed │ │ pressed │ ◀──────────────────────── └──────────┘ └─────────┘ onPressFinish(自动回到 idle)
三、快速开始:在 Flutter 中集成 Rive
3.1 准备 Rive 文件 .riv
下载安装 Rive 编辑器
- 官网 https://rive.app/ 提供 macOS/Windows/Linux 版本。
- 注册帐号后,新建或打开示例项目。
创建一个简单 Artboard
- 在 Rive 编辑器中新建一个 Artboard,绘制一个图形(如旋转圆环或 Logo ),并设置简单的 “Animate” 时间线,导出成
loading.riv
。
- 在 Rive 编辑器中新建一个 Artboard,绘制一个图形(如旋转圆环或 Logo ),并设置简单的 “Animate” 时间线,导出成
导出
.riv
文件- 点击右上角 “Export File” 按钮,选择 “Rive File (.riv)” 并保存到项目目录下(例如
assets/animations/loading.riv
)。
- 点击右上角 “Export File” 按钮,选择 “Rive File (.riv)” 并保存到项目目录下(例如
3.2 添加依赖与配置
在 pubspec.yaml
中添加 Rive Flutter Runtime 依赖,并声明 assets:
dependencies:
flutter:
sdk: flutter
rive: ^0.10.0 # 请按最新版本号替换
flutter:
assets:
- assets/animations/loading.riv
然后运行:
flutter pub get
3.3 加载并显示静态动画
在 Flutter 代码中,只需如下步骤:
引入 Rive 包:
import 'package:rive/rive.dart';
使用
RiveAnimation.asset(...)
Widget:class SimpleRivePage extends StatelessWidget { const SimpleRivePage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Rive 简单示例')), body: const Center( child: RiveAnimation.asset( 'assets/animations/loading.riv', // artboard: 'LoadingScreen', // 可指定 Artboard 名称 fit: BoxFit.contain, ), ), ); } }
- 运行,屏幕中即可看到
loading.riv
中默认播放的动画(通常是第一个在 File Inspector 中勾选为 Default Animation 的 Timeline)。
四、实战示例一:播放简单动画
下面我们以一个“Loading 动画”为例,从 Rive 编辑器制作到 Flutter 代码显示,完整梳理流程。
4.1 在 Rive 编辑器中创建一个 “Loading” 动画
- 新建 Artboard,命名为
LoadingScreen
。 - 绘制图形:使用 Ellipse(椭圆)绘制一个圆环,设置 Stroke(描边)粗细为 8px,颜色为渐变色(可选)。
添加关键帧动画:
在 Timeline 面板中为 “Ellipse” 属性添加 Rotation 关键帧。例如:
- 时间 0s:Rotation = 0
- 时间 1s:Rotation = 360
- 设定该 Timeline 为 “Loop” 循环模式。
- 在右侧 File Inspector 中,勾选 “Loop Animation” 并将 Timeline 命名为
LoadingRotate
。
- 测试预览:点击 Play,可看到圆环持续旋转。
- 导出:File → Export → Rive File (
loading.riv
),保存到 Flutter 项目的assets/animations
目录中。
4.2 Flutter 中显示动画的最简代码
import 'package:flutter/material.dart';
import 'package:rive/rive.dart';
class LoadingPage extends StatelessWidget {
const LoadingPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Rive Loading 示例')),
body: const Center(
child: SizedBox(
width: 150,
height: 150,
child: RiveAnimation.asset(
'assets/animations/loading.riv',
artboard: 'LoadingScreen', // 指定 Artboard
animations: ['LoadingRotate'], // 播放指定动画
fit: BoxFit.contain,
),
),
),
);
}
}
解释:
RiveAnimation.asset(...)
会在内部完成rootBundle.load('...loading.riv')
,并解析.riv
文件。- 指定
artboard
为LoadingScreen
,animations
数组中可列出多个 Timeline,但这里只有一个LoadingRotate
。
4.3 图解:渲染流程与帧率优化
┌─────────────────────────────────────────┐
│ Flutter Frame Render (60 FPS) │
│ │
│ 每帧 build → RiveAnimation Widget │
│ │ │
│ ▼ (Rive Runtime) │
│ RiveAnimation.asset ──► 解码 .riv 文件 │
│ │ │
│ ▼ │
│ Artboard.load() │
│ │ │
│ ▼ 每帧 update │
│ Controller.advance(deltaTime) │
│ │ │
│ ▼ Evaluates Timeline │
│ Graph render → Raster → GPU │
│ │
└─────────────────────────────────────────┘
- 总体思路:Rive 的 Runtime 会在 Flutter 的渲染周期内根据
deltaTime
(两帧之间的时间差)计算下一帧动画状态,更新矢量图形的 Transform、颜色和路径等属性,然后通过CustomPainter
快速绘制到 Canvas 上,最后提交给 GPU。因此只要保持场景较为简单,Flutter 端能轻松达成 60+ FPS。
五、实战示例二:State Machine 驱动交互
相比于上面的“被动播放”动画,Rive 最强大的功能在于 State Machine,可根据外部事件在不同动画状态之间切换。下面演示如何打造一个“炫酷按钮”交互。
5.1 在 Rive 中制作带有状态机的按钮动画
- 新建 Artboard,命名为
ButtonAnim
。 - 绘制按钮形状:使用
RoundedRectangle
(圆角矩形)和Text
组合成一个矩形按钮。 创建两段 Animation:
Idle
:默认状态,按钮颜色为淡蓝色,大小为 200×60;Pressed
:按下时动画,例如按钮缩小为 180×54,并颜色变为深蓝,同时文字颜色变为白色,持续 0.2 秒后返回 Idle。
添加 State Machine:
- 打开 “State Machines” 面板,创建一个新状态机
ButtonSM
。 - 在
Idle
状态下点右键 → “Add Animation” 选择Idle
Timeline; - 添加一个新状态
Pressed
,绑定Pressed
Timeline。 - 添加一个 Trigger 输入
pressTrigger
。 - 建立 Transition (Idle → Pressed):条件为
pressTrigger == true
。 - 在 Transition 的 “Exit Time” 中勾选
Use Exit Time
,确保动画执行完才退出。 - 建立 Transition (Pressed → Idle):无需条件,设定为“自动”状态(Auto Transition)在动画结束后跳到 Idle。
- 打开 “State Machines” 面板,创建一个新状态机
- 测试状态机:在右侧 “State Machine” 面板点击 “Play” 按钮,在 “Inputs” 区点击
pressTrigger
,看按钮按下动画是否正常。
最终 .riv
文件中包含:
- Artboard:
ButtonAnim
- Animations:
Idle
(循环)、Pressed
(单次) - StateMachine:
ButtonSM
with inputpressTrigger
5.2 Flutter 中读取 State Machine
并触发状态转换
在 Flutter 端,我们需要:
- 加载
.riv
文件 - 查找 Artboard 并 State Machine Controller
- 通过按钮点击触发 State Machine 的
pressTrigger
import 'package:flutter/material.dart';
import 'package:rive/rive.dart';
class InteractiveButtonPage extends StatefulWidget {
const InteractiveButtonPage({Key? key}) : super(key: key);
@override
_InteractiveButtonPageState createState() => _InteractiveButtonPageState();
}
class _InteractiveButtonPageState extends State<InteractiveButtonPage> {
late RiveAnimationController _btnController;
late SMIInput<bool> _pressInput;
@override
void initState() {
super.initState();
// 1. 加载 Rive 文件并在回调中获取 State Machine Controller
_btnController = SimpleAnimation('Idle'); // 默认先运行 Idle
rootBundle.load('assets/animations/button.riv').then((data) async {
final file = RiveFile.import(data);
final artboard = file.artboardByName('ButtonAnim');
if (artboard != null) {
// 2. 创建 State Machine Controller
StateMachineController? controller = StateMachineController.fromArtboard(
artboard,
'ButtonSM',
onStateChange: _onStateChange,
);
if (controller != null) {
artboard.addController(controller);
// 3. 获取 Trigger Input (pressTrigger)
_pressInput = controller.findInput<bool>('pressTrigger')!;
setState(() {
// 用新的 controller 替换旧的
_btnController = controller;
});
}
}
});
}
// 可选:监听 Rive 状态机状态变化
void _onStateChange(String stateMachineName, String stateName) {
debugPrint('StateMachine $stateMachineName 切换到状态 $stateName');
}
@override
void dispose() {
_btnController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Rive 交互按钮示例')),
body: Center(
child: GestureDetector(
onTap: () {
// 4. 触发状态机输入,切换到 Pressed 状态
_pressInput.value = true;
},
child: SizedBox(
width: 200,
height: 60,
child: Rive(
// 5. 直接传入 controller(已在 initState 中注入到 Artboard)
controllers: [_btnController],
fit: BoxFit.contain,
// artboard 可省略,Rive 会用默认 Artboard
),
),
),
),
);
}
}
关键步骤:
- 使用
rootBundle.load(...)
异步加载.riv
; - 用
RiveFile.import(data)
解析,获取Artboard
; StateMachineController.fromArtboard(artboard, 'ButtonSM')
获取状态机控制器;- 通过
controller.findInput<bool>('pressTrigger')
拿到 Trigger 类型输入,点击时赋值为true
,即触发状态转换; - 将 controller 添加到 artboard 中:
artboard.addController(controller)
,再将该 controller 传给 Rive Widget。
- 使用
5.3 代码详解与图解流程
┌───────────────────────────────────────────────────────┐
│ Flutter Widget 树 │
│ │
│ Center │
│ └── GestureDetector (onTap: _pressInput.value=true) │
│ └── SizedBox (200×60) │
│ └── Rive Widget (controllers: [_btnController]) │
│ │
│ ▲ │
│ │ │
│ RiveAnimationController (_btnController) │
│ │ │
│ │ 从 Artboard 中 update() │
│ ▼ │
│ Artboard: ButtonAnim │
│ ┌──────────────────────────────────┐ │
│ │ StateMachineController ButtonSM │ │
│ │ inputs: pressTrigger │ │
│ │ states: Idle, Pressed │ │
│ └──────────────────────────────────┘ │
│ │ │
│ (状态机根据 pressTrigger 触发转换 Idle→Pressed)│
│ │ │
│ ▼ │
│ 播放 Pressed Timeline (0.2s) │
│ │ │
│ (动画结束后自动跳回 Idle) │
└───────────────────────────────────────────────────────┘
交互流程:
- 用户在 Flutter 侧点击按钮,
onTap
设置_pressInput.value = true
; - Rive Runtime 在下一个
update()
周期检测到pressTrigger == true
,触发从Idle
到Pressed
的 Transition; - 播放
Pressed
Animation(缩小、变色),并自动在动画结束后通过 “Exit Time” 返回Idle
; - 开发者可在
_onStateChange
回调中监听状态切换,执行额外逻辑(如播放音效、触发网络请求等)。
- 用户在 Flutter 侧点击按钮,
六、高级技巧:动态修改属性 & 双向绑定
Rive 强大的地方在于不仅能播放预设动画,还能在 Flutter 侧实时修改 Rive 实例的属性,或监听 Rive 内部事件再触发 Flutter 逻辑。
6.1 通过 Flutter 代码实时修改 Rive 的属性(颜色、形状、数值)
在 Rive 中为某个 Shape 添加 “Number” 输入:
- 例如,为一个矩形的
fillColor
添加 rgba 四个数值属性(rValue
、gValue
、bValue
、aValue
),并设定在 Timeline 中用 Keyframe 读取它们变化。 - Rive 中 “inspector” 面板 → “Inputs” → “+” → 选择 Number → 命名为
rValue
,同理添加gValue
、bValue
、aValue
。
- 例如,为一个矩形的
在 Flutter 中获取这些 Input:
// initState 中 StateMachineController? controller = StateMachineController.fromArtboard(artboard, 'ColorSM'); artboard.addController(controller!); SMINumber? rInput = controller.findInput<double>('rValue'); SMINumber? gInput = controller.findInput<double>('gValue'); SMINumber? bInput = controller.findInput<double>('bValue'); SMINumber? aInput = controller.findInput<double>('aValue');
实时修改属性:
// 在滑块(Slider)回调中 onChanged: (value) { setState(() { rInput?.value = value; // value 范围 0.0 - 1.0 }); }
Rive 中使用这些属性:
- 在 Timeline 或 Animation 中,为“Shape.Fill.Color.R”绑定
rValue
变量。这样当rValue
改变时,下一帧 Rive 渲染时会读取最新数值。
- 在 Timeline 或 Animation 中,为“Shape.Fill.Color.R”绑定
6.2 双向绑定:监听 Rive 内部回调,触发 Flutter 逻辑
有时我们希望当 Rive 状态机切换到某个状态时,Flutter 端能收到通知。例如:动画播放到“完成”状态后弹出提示。
- 在 Rive 中为 State Machine 的 Transition 添加“On Entry”或“On Exit”事件,并勾选“Async Event” →
onFinished
类型(可自定义名称)。 在 Flutter 端注册回调:
// initState 中 controller = StateMachineController.fromArtboard( artboard, 'MySM', onStateChange: _onStateChange, ); ... void _onStateChange(String smName, String stateName) { if (stateName == 'Completed') { // Rive 中已到“Completed”状态,触发业务逻辑 showDialog(...); // 弹出提示 } }
整体图解:双向事件流
Flutter Button 点击 ——► _pressInput.value = true ——► Rive StateMachine │ │ │(状态机切到 Completed) │ └──────── onStateChange 回调 ◄─────────┘ │ ▼ Flutter 逻辑(弹出对话框)
七、性能优化与最佳实践
尽管 Rive 在 Flutter 中性能表现优异,但若项目中大量使用动画或多个 Rive 实例,也需要注意优化。
7.1 限制渲染区域与使用 RiveAnimationController
限制渲染区域:
- 若某个 Rive Widget 在屏幕外不可见,仍会持续执行动画更新,浪费 CPU。
- 可结合
VisibilityDetector
或ListView
中的Visibility
/Offstage
控制:当 Widget 不可见时调用controller.isActive = false
,停止更新。
使用
RiveAnimationController
控制播放:SimpleAnimation
、StateMachineController
等都继承自RiveAnimationController
,可调用isActive = false
暂停动画。- 在
dispose()
中务必调用controller.dispose()
,避免泄露。
7.2 减少不必要的 Artboard
reload
不要在
build()
中频繁RiveFile.import(...)
- 若把解析
.riv
文件的逻辑直接写在build()
方法里,会导致每次setState()
重新解析,对性能伤害很大。 - 建议在
initState()
中或使用FutureBuilder
加载一次,保留RiveFile
/Artboard
实例,重复使用。
- 若把解析
复用同一个
Artboard
实例- 若多个页面需要展示相同动画,尽量传递同一个 Artboard 或 Controller,并在切换页面时仅切换
isActive
,而非重新导入.riv
。
- 若多个页面需要展示相同动画,尽量传递同一个 Artboard 或 Controller,并在切换页面时仅切换
7.3 AOT 下动态加载与预缓存
AOT 模式打包体积
- Rive Runtime 自带一部分 C++ 解析库,在 AOT 模式下会被打包进
libapp.so
。若频繁动态加载,会略微增加 APK 大小,但对于较小的.riv
文件影响不大。
- Rive Runtime 自带一部分 C++ 解析库,在 AOT 模式下会被打包进
预缓存常用动画
- 对于启动页动画或常用交互,可在 App 启动时调用一次
RiveFile.import(...)
,并缓存结果到全局单例,这样后续再创建 Rive Widget 时,只需从缓存拿到已解析的Artboard
或RiveFile
。 - 配合
FutureBuilder
或Provider
管理,避免动画首次加载出现延迟。
- 对于启动页动画或常用交互,可在 App 启动时调用一次
八、总结与扩展思考
Rive 优势回顾
- 可视化动画创作:设计师可在编辑器中实时调参、预览;
- 实时交互状态机:支持复杂条件触发、双向交互;
- 高性能:基于 Flutter Canvas 矢量渲染,支持 60 FPS 或更高帧率;
Flutter 与 Rive 的典型使用场景
- 启动页 / Loading:优雅的加载指示;
- 交互动效按钮 / 图标微动:按下、切换状态、Tab 栏选中等;
- 游戏 UI / 角色动画:在小型游戏或卡通风格 App 中,角色跑跳、场景动画;
- 数据可视化:动态图表中嵌入交互动效,例如 Data Dashboard 中的折线图动画。
未来扩展方向
- 与 Flutter 的自定义 Shader 结合:通过自定义
CustomPainter
在 Rive 渲染后再加上 Shader 效果,实现更炫酷的光影或粒子特效。 - 混合平台:Rive 支持 Web、iOS、Android,此时可将相同
.riv
文件用于 Flutter Web 与移动端,实现动画统一。 - 代码生成:考虑将 Rive Animation Controller 代码自动生成,减少手写拼接逻辑,提高可维护性。
- 与 Flutter 的自定义 Shader 结合:通过自定义
至此,你已掌握从零开始在 Flutter 中使用 Rive 的“炫酷交互”全流程,包括:
- Rive 核心概念与术语;
- 在 Flutter 中加载并播放简单动画;
- 使用 State Machine 实现复杂交互;
- 动态修改属性与双向绑定;
- 一系列性能优化与最佳实践。
评论已关闭