跳到主要内容

Data Binding

数据绑定(Data Binding)用于把运行时代码连接到 Rive 编辑器中已经绑定的元素。通过 View Model 和 View Model Instance,你可以在 Android 运行时中读取、写入和观察属性值,并把这些值绑定到状态机、图片、列表、Artboard、枚举等内容。

Rive Android 目前包含两套常见 API:

  • Compose API:面向 Jetpack Compose,通常使用 rememberViewModelInstance、ViewModelSource、ViewModelInstanceSource 和 Kotlin Flow。
  • Legacy API:面向 RiveAnimationView,通过 view.controller.file、ViewModel、ViewModelInstance 以及属性对象进行操作。

概览​

数据绑定的核心概念包括:

  1. View Model:在 Rive 文件中定义的一组可绑定属性结构。
  2. View Model Instance:View Model 的具体实例,持有实际运行时数据。
  3. Binding:将 View Model Instance 绑定到 Artboard 或 State Machine,让动画可以响应实例中的数据。
  4. Properties:View Model Instance 中的具体字段,例如数字、字符串、布尔值、颜色、图片、列表、Artboard、嵌套实例等。
  5. Observability:在运行时监听属性变化,并在属性更新时同步 UI 或业务逻辑。

View Models​

View Model 是在 Rive 编辑器中定义的数据结构。运行时可以根据名称、索引,或某个 Artboard 的默认 View Model 来获取它。

Compose​

与其他运行时不同,在 Compose API 中,View Model 不作为单独对象存在。它由 ViewModelSource sealed class 表示,并作为创建 View Model Instance 的构建器模式的一部分。另一部分是创建实例,详见 View Model Instances。

// Named source
val vmSource = ViewModelSource.Named("My View Model")
// Default for artboard source
val vmSource = ViewModelSource.DefaultForArtboard(artboard)

Legacy​

// `view` of type RiveAnimationView
view.setRiveResource(R.raw.my_rive_file)
val file = view.controller.file!!

// Get reference by name
val vm = file.getViewModelByName("My View Model")

// Get reference by index
for (i in 0 until file.viewModelCount) {
val indexedVM = file.getViewModelByIndex(i)
}

// Get reference to the default view model
val defaultVM = file.defaultViewModelForArtboard(view.controller.activeArtboard!!)

View Model Instances​

View Model Instance 是 View Model 的具体数据实例。你可以创建空白实例、默认实例、按名称创建的实例,或引用嵌套实例。

Compose​

请先参考 View Models 获取可用于下面示例的 ViewModelSource。有了它之后,可以使用构建器模式创建 ViewModelInstanceSource。随后将该 source 传给 rememberViewModelInstance,即可在 composition 生命周期内创建并记住该实例。

// From previous section
val vmSource = ViewModelSource.Named("My View Model")

// Blank instance source
val vmiSourceBlank = ViewModelInstanceSource.Blank(vmSource)
// or
val vmiSourceBlank = vmSource.blankInstance()

// Default instance source
val vmiSourceDefault = ViewModelInstanceSource.Default(vmSource)
// or
val vmiSourceDefault = vmSource.defaultInstance()

// Named instance source
val vmiSourceNamed = ViewModelInstanceSource.Named(vmSource, "My Instance")
// or
val vmiSourceNamed = vmSource.namedInstance("My Instance")

// The completed source can now be used along with the Rive file to create and remember the instance
val viewModelInstance = rememberViewModelInstance(riveFile, vmiSourceNamed)

此外,你也可以通过 Reference 变体,从父实例中引用嵌套 View Model Instance。

val myVMI = rememberViewModelInstance(riveFile, mySource)
val referenceSource = ViewModelInstanceSource.Reference(myVMI, "Path/To/Nested VMI")
val nestedVMI = rememberViewModelInstance(riveFile, referenceSource)

Legacy​

val vm = view.controller.file?.getViewModelByName("My View Model")!!

// Create blank
val vmiBlank = vm.createBlankInstance()

// Create default
val vmiDefault = vm.createDefaultInstance()

// Create by index
for (i in 0 until vm.instanceCount) {
val vmiIndexed = vm.createInstanceFromIndex(i)
}

// Create by name
val vmiNamed = vm.createInstanceFromName("My Instance")

Binding​

创建 View Model Instance 后,需要将它绑定到 State Machine 或 Artboard,动画才能读取其中的数据并对变化作出响应。

Compose​

请参考 Compose data binding example。

当把 ViewModelInstance 传给 Rive composable 时,它会自动绑定到状态机。

val vmiSource = ViewModelSource.Named("My View Model").namedInstance("My Instance")
val vmi = rememberViewModelInstance(riveFile, vmiSource)

Rive(
riveFile,
viewModelInstance = vmi
)

Legacy​

请参考 Legacy data binding example。

view.setRiveResource(
R.raw.my_rive_file,
artboardName = "My Artboard",
)

val vm = view.controller.file?.getViewModelByName("My View Model")!!
val vmi = vm.createInstanceFromName("My Instance")

// Apply the instance to the state machine (preferred)
view.controller.stateMachines.first().viewModelInstance = vmi

// Alternatively, apply the instance to the artboard
view.controller.activeArtboard?.viewModelInstance = vmi

Auto Binding​

Auto-binding 会自动创建并绑定默认 Artboard、默认 View Model 和默认实例。它适合使用 Rive 文件中的默认绑定配置快速启动。

Compose​

由于 composable 是函数,在 Compose API 中不存在 Legacy API 那样的 auto-binding。和类相比,从 composable 中取值较困难;如果使用回调,还需要在回调触发前记住一个 null 占位值,这会比直接提供实例产生更多额外开销。

等价做法是创建一个没有 source 的 View Model Instance。它会在内部创建默认 Artboard、该 Artboard 的默认 View Model,以及该 View Model 的默认实例。然后你可以把它传给 Rive composable。

val vmi = rememberViewModelInstance(riveFile)
Rive(
riveFile,
viewModelInstance = vmi,
)

Legacy​

view.setRiveResource(
R.raw.my_rive_file,
autoBind = true,
)

Properties​

属性是 View Model 中定义的具体数据字段。常见类型包括数字、字符串、布尔值、颜色、枚举、图片、列表、Artboard,以及嵌套 View Model Instance。

运行时可以列出属性、读取属性、写入属性,也可以观察属性变化。

Listing properties​

你可以列出某个 View Model 中定义的所有属性,用于调试、校验或动态构建 UI。

Compose​

获取 View Model 属性是挂起操作,因此需要在协程作用域中调用,例如 LaunchedEffect。

LaunchedEffect(riveFile) {
riveFile.getViewModelProperties("My View Model").forEach { property ->
Log.d("My Tag", "Property Name: ${property.name}, Type: ${property.type}")
}
}

Legacy​

val vm = view.controller.file?.getViewModelByName("My View Model")!!

// A list of properties
val properties = vm.properties
assertContains(
properties,
ViewModel.Property(ViewModel.PropertyDataType.NUMBER, "My Number Property")
)

Reading and writing properties​

读取和写入属性是数据绑定中最常见的操作。Compose API 通常直接通过路径设置属性,并通过 Flow 读取和观察属性;Legacy API 则通常先获取属性对象,然后读取或修改它的 value。

Compose​

写入值​

Compose API 没有显式的属性对象。属性值会直接通过 ViewModelInstance 上的方法设置,这些方法接收属性路径。

val vmi = rememberViewModelInstance(...)
vmi.setNumberProperty("Path/To/Property", 10f)

读取值​

值会通过 Kotlin Flow 读取。每当属性值发生变化时,该 Flow 都会发出最新值。你可以在 LaunchedEffect 中 collect 这个 Flow,也可以使用 collectAsState() 将其转换为 State(或使用 collectAsStateWithLifecycle() 只在特定生命周期状态下 collect)。

如果只想获取一次最新值而不持续观察,可以使用终端操作符 first()。

val vmi = rememberViewModelInstance(...)
// Collect as State
val numberValue by vmi.numberPropertyFlow("Path/To/Property").collectAsState(initial = 0f)

Text(text = "Number value: $numberValue")

// Or collect
LaunchedEffect(vmi) {
vmi.numberPropertyFlow("Path/To/Property").collect { value ->
Log.d("Rive", "Number value changed: $value")
}

// Or get once
val numberValue = vmi.numberPropertyFlow("Path/To/Property").first()
Log.d("Rive", "Current number value: $numberValue")
}

Legacy​

val vm = view.controller.file?.getViewModelByName("My View Model")!!
val vmi = vm.createInstanceFromName("My Instance")

val numberProperty = vmi.getNumberProperty("My Number Property")
// Get
val numberValue = numberProperty.value
// Set
numberProperty.value = 10f

Nested property paths​

属性可以位于嵌套 View Model Instance 中。你可以逐级获取嵌套实例,也可以使用 / 分隔的路径直接访问嵌套属性。

Compose​

val parent = rememberViewModelInstance(riveFile, ViewModelSource.Named("Parent VM").namedInstance("Parent"))

// Using references
val child = rememberViewModelInstance(riveFile, ViewModelInstanceSource.Reference(parent, "Child"))
val nestedNumber = child.numberPropertyFlow("My Nested Number").collectAsState(0f)

// Or using paths
val nestedNumber = parent.numberPropertyFlow("Child/My Nested Number").collectAsState(0f)

Legacy​

val vm = view.controller.file?.getViewModelByName("My View Model")!!
val vmi = vm.createInstanceFromName("My Instance")

val nestedNumberByChain = vmi
.getInstanceProperty("My Nested View Model")
.getInstanceProperty("My Second Nested VM")
.getNumberProperty("My Nested Number")

val nestedNumberByPath = vmi
.getNumberProperty("My Nested View Model/My Second Nested VM/My Nested Number")

Observability​

Observability 指的是在属性值变化时收到更新通知。它可以用于同步 Compose UI、更新应用状态,或触发业务逻辑。

Compose​

使用 Compose API 与 Kotlin Flow 时,默认行为就是可观察的。当你 collect 某个属性的 Flow 时,只要该属性值变化,就会收到更新。

val vmi = rememberViewModelInstance(...)
val numberPropertyFlow = vmi.numberPropertyFlow("My Number Property").collectAsState(0f)

Legacy​

val vm = view.controller.file?.getViewModelByName("My View Model")!!
val vmi = vm.createInstanceFromName("My Instance")

val numberProperty = vmi.getNumberProperty("My Number Property")
// Observe
lifecycleScope.launch {
numberProperty.valueFlow.collect { value ->
Log.i("MyActivity", "Value: $value")
}
}
// Or collect in Compose
val numberValue by numberProperty.valueFlow.collectAsState(0f) // 0 as the initial value while waiting for the first value

Images​

图片属性允许你在运行时替换绑定到 View Model 的图像。通常需要先加载图片字节,再将其转换成 Rive 可使用的图片资源。

Compose​

请参考 Compose data binding images example。

要设置图片属性,你需要一个 ImageAsset。它可以通过 rememberImage 从字节数组创建。下面的示例为了方便,将 raw resources 加载到 Result 中;实际应用中应使用最适合你项目的加载方式。

val imageBytes by produceState<Result<ByteArray>>(Result.Loading) {
value = withContext(Dispatchers.IO) {
context.resources.openRawResource(R.raw.my_image)
.use { Result.Success(it.readBytes()) }
}
}

// `andThen` maps over the Result to only call the lambda if it's a Success, propagating Failure and Loading otherwise.
val image = imageBytes.andThen { bytes ->
rememberImage(riveWorker, bytes)
}

// Or combine into one statement
val image = produceState<Result<ByteArray>>(Result.Loading) {
value = withContext(Dispatchers.IO) {
context.resources.openRawResource(R.raw.my_image)
.use { Result.Success(it.readBytes()) }
}
}.value.andThen { bytes ->
rememberImage(riveWorker, bytes)
}

val vmi = rememberViewModelInstance(riveFile, ViewModelSource.Named("My View Model").defaultInstance())
LaunchedEffect(vmi, image) {
when(image) {
is Result.Failure -> { /* Handle failure to load image */ }
is Result.Loading -> { /* Handle loading state if needed */ }
is Result.Success -> {
// Set the image property value
vmi.setImage("Image property", image.value)
}
}
}

如果你想在图片加载完成后再展示 Rive 内容,并且只在 Rive 文件与图片都成功加载时展示,可以使用 zip 便捷函数将多个 Result 对象组合起来。

val fileAndImage = riveFile.zip(image)

when (fileAndImage) {
is Result.Failure -> { /* Handle failure to load file or image */ }
is Result.Loading -> { /* Handle loading state if needed */ }
is Result.Success -> {
val (riveFile, image) = fileAndImage.value
// Both riveFile and image are loaded successfully here
// You can now present your Rive content and set the image property
}
}

关于图片资源的更多信息,请参阅 Loading Assets。

Legacy​

// Load image from the assets folder.
val imageBytes = context.resources.openRawResource(R.raw.my_image).use { stream ->
stream.readBytes()
}

val vmi = it.stateMachines.first().viewModelInstance!!

// Replace image property in view model instance with new image.
val riveImage = RiveRenderImage.fromEncoded(imageBytes)
vmi.getImageProperty("Image property").set(riveImage)

Lists​

列表属性用于在 View Model Instance 中保存多个 View Model Instance。你可以追加、插入、交换和移除列表项。

Compose​

请参考 Compose data binding lists example。

val mainVMI = rememberViewModelInstance(riveFile)
val newListItem = rememberViewModelInstance(riveFile, ViewModelSource.Named("My Item VM").namedInstance("My List Item"))
LaunchedEffect(mainVMI, newListItem) {
val listProperty = "My List"

// Add new item to the end of the list
mainVMI.appendToList(listProperty, newListItem)
// Insert new item at index 0
mainVMI.insertToListAtIndex(listProperty, 0, newListItem)

// Swap items at index 0 and 1
mainVMI.swapListItems(listProperty, 0, 1)

// Remove specific instance
mainVMI.removeFromList(listProperty, newListItem)
// Remove item at index 0
mainVMI.removeFromListAtIndex(listProperty, 0)
}

由于列表是动态的,你可能需要在协程中创建条目,而不是提前使用 rememberViewModelInstance 创建。请注意,如果把同一个实例多次添加到列表中,它们会共享状态,这可能并不是你想要的行为。可以使用下面的模式按需创建新实例。

val mainVMI = rememberViewModelInstance(riveFile)
LaunchedEffect(mainVMI) {
val listProperty = "My List"
// ⚠️ This must be `close`d, which is done here through `AutoCloseable.use`.
ViewModelInstance.fromFile(
riveFile,
ViewModelSource.Named("My Item VM").defaultInstance()
).use { item ->
mainVMI.insertToListAtIndex(listProperty, 0, item)
}
}

Legacy​

// Acquire the default view model instance and the list property.
val vmi = animationView.file!!.firstArtboard.viewModelInstance!!
val listProperty = vmi.getListProperty("list")

// Create a view model instance for "First" and "Second" and add them to the list.
val firstInstance = animationView.file!!.getViewModelByName("My Item VM").createInstanceFromName("First")
listProperty.add(firstInstance)

val secondInstance = animationView.file!!.getViewModelByName("My Item VM").createInstanceFromName("Second")
listProperty.add(secondInstance)

// Swap the two items in the list.
listProperty.swap(0, 1)

// Remove both items from the list.
listProperty.remove(firstInstance)
listProperty.removeAt(0)

Artboards​

Artboard 属性允许你在运行时把某个 Artboard 设置为 View Model Instance 的属性值。它既可以来自同一个 Rive 文件,也可以来自外部 Rive 文件。还可以创建带有 View Model Instance 的 bindable Artboard,用于控制子 Artboard 的状态。

Compose​

请参考 Compose data binding artboards example。

val vmi = rememberViewModelInstance(mainFile)
val artboard = rememberArtboard(mainFile, "My Artboard")

LaunchedEffect(vmi, artboard) {
vmi.setArtboard("My Artboard Property", artboard)
}

Legacy​

// Acquire the default view model instance and the artboard property.
val vmi = animationView.file!!.firstArtboard.viewModelInstance!!
val artboardProperty = vmi.getArtboardProperty("My Artboard Property")

// Set artboard from same file.
val localArtboard = animationView.file!!.getArtboard("My Artboard")
artboardProperty.set(localArtboard)

// Load external file if needed
val externalFile = File.load(context.assets, "external_file.riv")

// Set artboard from external file.
val externalArtboard = externalFile.getArtboard("My External Artboard")
artboardProperty.set(externalArtboard)

// Clean up external file when done
externalFile.dispose()

你也可以创建一个带有 View Model Instance 的 bindable Artboard,以控制其状态。

// Create the view model instance for the child artboard.
val childVmi = childFile.getViewModelByName("ChildVM").createBlankInstance()
val bindableArtboard = childFile.createBindableArtboardByName("Child", childVmi)

// Get the artboard property from the main artboard.
val vmi = rive.file!!.firstArtboard.viewModelInstance!!
val artboardProperty = vmi.getArtboardProperty("Artboard property")

// Set the bound artboard VMIs state.
childVmi.getNumberProperty("rotation").value = 90f

// Set the bound artboard on the main artboard.
artboardProperty.set(bindableArtboard)

// Release the reference we hold from creation.
bindableArtboard.release()

Enums​

枚举定义可以从 Rive 文件中读取。你可以获取枚举名称以及枚举的可用值。

Compose​

LaunchedEffect(riveFile) {
val enums = riveFile.getEnums()
Log.i("RiveEnums", "First enum name: ${enums[0].name}")
}

Legacy​

val enums = view.controller.file?.enums!!

val firstEnumName = enums[0].name
val firstEnumFirstValue = enums[0].values[0]

Examples​

Compose​

请参考以下示例:

Legacy​

请参考 data binding overview example。

看完还有疑问?进群交流下!
与众多 Rive 创作者、开发者一起交流探讨与答疑解惑。
加入交流群