跳到主要内容

Data Binding

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

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

  • Compose API:面向 Jetpack Compose,通常使用 rememberViewModelInstanceViewModelSourceViewModelInstanceSource 和 Kotlin Flow
  • Legacy API:面向 RiveAnimationView,通过 view.controller.fileViewModelViewModelInstance 以及属性对象进行操作。

概览

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

  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