Rive (Flare) 运行时:骨骼动画与状态机在 Flutter 渲染循环中的集成

Rive (Flare) 运行时:骨骼动画与状态机在 Flutter 渲染循环中的集成

大家好,今天我们要深入探讨 Rive(前身 Flare)运行时在 Flutter 渲染循环中的集成方式。Rive 是一款强大的实时动画工具,它允许设计师创建复杂的骨骼动画和状态机,而 Rive 的运行时库则负责在各种平台上渲染这些动画。我们的重点将放在 Flutter 平台上,理解 Rive 运行时如何与 Flutter 的渲染机制协同工作,以及如何利用其提供的 API 来控制和驱动动画。

Rive 的核心概念

在深入集成细节之前,我们先回顾一下 Rive 的几个核心概念:

  • Artboard (画板): 包含动画资源的基本容器,类似于一个场景或舞台。
  • Animation (动画): 一系列关键帧,定义了对象属性随时间的变化。可以是线性动画,也可以是复杂的骨骼动画。
  • StateMachine (状态机): 定义了动画之间的切换规则,允许创建交互式和响应式的动画。状态机由状态、输入和转换组成。
  • State (状态): 代表动画的特定阶段或模式。
  • Input (输入): 外部信号,例如用户交互或程序变量,用于触发状态之间的转换。输入可以是 Boolean、Number 或 Trigger 类型。
  • Transition (转换): 定义了从一个状态到另一个状态的条件和行为。
  • Bone (骨骼): 骨骼动画的基础,用于创建角色和物体的层级结构。
  • Skin (蒙皮): 将图像或形状附加到骨骼,使其随骨骼移动。
  • Mesh (网格): 用于复杂形状变形,通常与骨骼结合使用。

理解这些概念是掌握 Rive 在 Flutter 中使用的关键。

Flutter 渲染循环概述

Flutter 的渲染循环是一个持续的过程,它负责将 UI 描述转换为屏幕上的像素。这个循环大致分为以下几个阶段:

  1. Build (构建): Flutter 根据当前的应用程序状态构建 Widget 树。这是一个纯 Dart 代码的过程。
  2. Layout (布局): Flutter 确定 Widget 树中每个 Widget 的大小和位置。
  3. Paint (绘制): Flutter 将 Widget 绘制到 Canvas 上。Canvas 是一个提供绘图操作的 API。
  4. Compose (合成): Flutter 将多个图层合成为最终的图像。
  5. Render (渲染): Flutter 将最终的图像提交给 GPU 进行渲染。

Rive 运行时需要集成到这个循环中,以便在每一帧更新动画并将其绘制到 Canvas 上。

RiveRuntime 组件:RiveAnimation Widget

Rive 官方提供了 rive 包,其中最核心的组件是 RiveAnimation Widget。这个 Widget 负责加载 Rive 文件(通常是 .riv 格式),管理动画状态,并将动画渲染到 Flutter 的 Canvas 上。

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

class RiveExample extends StatefulWidget {
  const RiveExample({Key? key}) : super(key: key);

  @override
  _RiveExampleState createState() => _RiveExampleState();
}

class _RiveExampleState extends State<RiveExample> {
  Artboard? _riveArtboard;
  StateMachineController? _controller;

  @override
  void initState() {
    super.initState();
    // 加载 Rive 文件
    rootBundle.load('assets/rive_animation.riv').then((data) {
      final file = RiveFile.import(data);
      final artboard = file.mainArtboard;
      var controller = StateMachineController.fromArtboard(artboard, 'State Machine 1');
      if (controller != null) {
        artboard.addController(controller);
      }
      setState(() => {
        _riveArtboard = artboard,
        _controller = controller
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Rive Example'),
      ),
      body: Center(
        child: _riveArtboard == null
            ? const CircularProgressIndicator()
            : Rive(artboard: _riveArtboard!),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // 触发状态机输入
          _controller?.findInput<bool>('BooleanInput')?.value = !(_controller?.findInput<bool>('BooleanInput')?.value ?? false);
        },
        child: const Icon(Icons.play_arrow),
      ),
    );
  }
}

这段代码演示了如何加载 Rive 文件,获取 Artboard 和 StateMachineController,并将 Artboard 传递给 Rive Widget。Rive Widget 内部处理了动画的渲染。

深入 Rive Widget 的实现

Rive Widget 实际上是一个 StatefulWidget,它创建了一个 _RiveState 对象来管理 Rive 运行时的生命周期。_RiveState 负责以下任务:

  1. 加载 Rive 文件: 使用 RiveFile.import 方法从字节流中加载 Rive 文件。
  2. 创建 Artboard: 从 Rive 文件中获取 Artboard 对象。Artboard 包含了动画的所有资源。
  3. 创建 Renderer: Rive 运行时使用 Renderer 将动画绘制到 Canvas 上。Flutter 提供了 FlutterRenderer,它是 Rive Renderer 的一个实现,专门用于 Flutter 环境。
  4. 更新动画: 在 Flutter 渲染循环的每一帧,_RiveState 调用 artboard.advance 方法来更新动画。artboard.advance 方法会根据当前时间和状态机的状态,更新动画中所有对象的属性。
  5. 绘制动画: _RiveState 使用 FlutterRenderer 将 Artboard 绘制到 Canvas 上。

状态机控制:与动画交互

Rive 的强大之处在于其状态机功能,它允许我们根据外部输入来控制动画的行为。StateMachineController 类提供了与状态机交互的 API。

  1. 获取 StateMachineController: 使用 StateMachineController.fromArtboard 方法从 Artboard 中获取 StateMachineController。需要指定状态机的名称。
  2. 获取 Input: 使用 controller.findInput 方法获取状态机的输入。输入可以是 Boolean、Number 或 Trigger 类型。
  3. 设置 Input 值: 通过设置 Input 的 value 属性来触发状态之间的转换。

在上面的例子中,我们通过点击 FloatingActionButton 来切换一个 Boolean Input 的值,从而触发状态机的转换。

性能优化

Rive 动画的性能取决于动画的复杂程度和渲染平台的性能。以下是一些优化 Rive 动画性能的建议:

  • 减少骨骼数量: 骨骼越多,计算量越大。尽量减少不必要的骨骼。
  • 优化网格: 复杂的网格需要更多的计算资源。尽量简化网格的结构。
  • 使用纹理图集: 将多个图像合并到一个纹理图集中可以减少纹理切换的次数,从而提高渲染性能。
  • 避免过度绘制: 尽量减少透明物体的数量,因为透明物体需要进行混合计算。
  • 使用缓存: 对于静态的动画,可以使用缓存来避免重复渲染。
  • 限制动画帧率: 如果动画不需要很高的帧率,可以限制动画的帧率来降低 CPU 和 GPU 的负载。
  • 使用 Rive 的优化工具: Rive 编辑器提供了一些优化工具,可以帮助你减少动画的大小和复杂度。

自定义 Renderer:高级用法

虽然 FlutterRenderer 已经足够满足大多数需求,但在某些情况下,你可能需要自定义 Renderer。例如,你可能想要:

  • 实现自定义的着色器: 使用自定义的着色器可以实现更高级的视觉效果。
  • 集成到现有的渲染管线: 将 Rive 动画集成到现有的渲染管线中。
  • 优化特定平台的性能: 针对特定平台进行性能优化。

要自定义 Renderer,你需要创建一个实现 Renderer 接口的类。Renderer 接口定义了渲染动画所需的各种方法,例如 drawMeshdrawImagedrawLine

import 'package:rive/src/core/core.dart';
import 'package:rive/src/generated/component_base.dart';
import 'package:rive/src/generated/drawable_base.dart';
import 'package:rive/src/generated/node_base.dart';
import 'package:rive/src/renderer.dart';
import 'package:rive/src/rive_core/math/mat2d.dart';
import 'package:rive/src/rive_core/math/raw_path.dart';
import 'package:rive/src/rive_core/math/vec2d.dart';
import 'package:rive/src/transform_space.dart';
import 'package:rive/src/trie/trie.dart';

class CustomRenderer extends Renderer {
  @override
  void align(Mat2D transform, AABB contentRect, AABB containerRect, Fit fit, Alignment alignment) {
    // TODO: implement align
  }

  @override
  void beginFrame() {
    // TODO: implement beginFrame
  }

  @override
  void clear(double r, double g, double b, double a) {
    // TODO: implement clear
  }

  @override
  void clip(Path path) {
    // TODO: implement clip
  }

  @override
  void drawImage(Core core, Image image, double x, double y, double width, double height) {
    // TODO: implement drawImage
  }

  @override
  void drawPath(Path path, PathFillRule fillRule, Mat2D transform) {
    // TODO: implement drawPath
  }

  @override
  void drawRawPath(RawPath path, PathFillRule fillRule, Mat2D transform) {
    // TODO: implement drawRawPath
  }

  @override
  void endFrame() {
    // TODO: implement endFrame
  }

  @override
  void fill(PathFillRule fillRule) {
    // TODO: implement fill
  }

  @override
  Mat2D getTransform() {
    // TODO: implement getTransform
    return Mat2D();
  }

  @override
  void popTransform() {
    // TODO: implement popTransform
  }

  @override
  void pushTransform(Mat2D transform) {
    // TODO: implement pushTransform
  }

  @override
  void restore() {
    // TODO: implement restore
  }

  @override
  void save() {
    // TODO: implement save
  }

  @override
  void stroke() {
    // TODO: implement stroke
  }

  @override
  set blendMode(BlendMode blendMode) {}

  @override
  set clipOp(ClipOp clipOp) {}

  @override
  set color(int value) {}

  @override
  set composition(BlendMode value) {}

  @override
  set dashOffset(double value) {}

  @override
  set drawRule(PathFillRule value) {}

  @override
  set fillRule(PathFillRule value) {}

  @override
  set height(double height) {}

  @override
  set lineCap(StrokeCap value) {}

  @override
  set lineJoin(StrokeJoin value) {}

  @override
  set opacity(double value) {}

  @override
  set strokeCap(StrokeCap value) {}

  @override
  set strokeColor(int value) {}

  @override
  set strokeJoin(StrokeJoin value) {}

  @override
  set strokeMiterLimit(double value) {}

  @override
  set strokeWidth(double value) {}

  @override
  set width(double width) {}
}

创建自定义 Renderer 后,你需要将其传递给 Rive Widget。这可以通过自定义 Rive Widget 或修改 Rive 运行时库来实现。

Rive 与 Flutter 的交互:事件监听

Rive 不仅可以接收 Flutter 的输入,还可以向 Flutter 发送事件。这可以通过 Rive 的事件系统来实现。你可以在 Rive 编辑器中定义事件,并在 Flutter 代码中监听这些事件。

Rive 提供了 RiveAnimationController 类,它可以监听 Rive 动画中的事件。

//监听trigger事件
_controller?.registerController(SimpleAnimation('Animation1', autoplay: true));

  @override
  void initState() {
    super.initState();
    // 加载 Rive 文件
    rootBundle.load('assets/new_community.riv').then((data) {
      final file = RiveFile.import(data);
      final artboard = file.mainArtboard;
      artboard.addController(_controller = SimpleAnimation('Animation1', autoplay: true));
      setState(() => _riveArtboard = artboard);
    });
  }

Rive 文件结构:深入了解 .riv 格式

.riv 文件是 Rive 动画的二进制格式。了解 .riv 文件的结构可以帮助你更好地理解 Rive 运行时的工作原理。

.riv 文件包含以下几个部分:

  • Header: 包含文件的元数据,例如版本号和文件大小。
  • Assets: 包含动画所需的资源,例如图像、字体和声音。
  • Artboards: 包含动画场景。
  • Animations: 包含动画数据。
  • State Machines: 包含状态机数据。

虽然直接解析 .riv 文件比较复杂,但了解其结构可以帮助你更好地理解 Rive 运行时的工作原理。

Rive 的局限性

尽管 Rive 非常强大,但也存在一些局限性:

  • 学习曲线: Rive 编辑器和运行时 API 都有一定的学习曲线。
  • 文件大小: 复杂的 Rive 动画可能会导致文件大小增加。
  • 性能: 复杂的动画可能会影响性能,尤其是在低端设备上。
  • 生态系统: 与其他动画工具相比,Rive 的生态系统相对较小。

总结:Rive 与 Flutter 结合的关键

Rive 通过 RiveAnimation Widget 集成到 Flutter 的渲染循环中。RiveAnimation Widget 负责加载 Rive 文件,管理动画状态,并将动画渲染到 Flutter 的 Canvas 上。通过 StateMachineController,我们可以控制动画的状态和行为,实现交互式动画。 了解 Rive 的核心概念,Flutter 渲染循环以及性能优化技巧,可以帮助我们更好地利用 Rive 创建出色的动画效果。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注