Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
491ae77
feat: add viewModelInstance to ViewProps, so we don't need to wait fo…
mfazekas Nov 13, 2025
248c82f
feat: getBoundViewModelInstance
mfazekas Nov 13, 2025
6d4dbfb
dataBind 1st gen
mfazekas Nov 17, 2025
b164165
fix: apply data binding changes in-place without reload
mfazekas Nov 17, 2025
1571d02
fix(ios): bind view model instance to both artboard and state machine
mfazekas Nov 17, 2025
23c1d84
fix(ios): play after binding to refresh view
mfazekas Nov 17, 2025
ff931d6
fix(ios): play after none and auto binding modes
mfazekas Nov 17, 2025
8baafac
fix(android): bind to artboard and play after binding changes
mfazekas Nov 17, 2025
ad702d1
fix(android): trigger full reload on binding mode changes
mfazekas Nov 17, 2025
ca726eb
refactor(android): remove manual afterUpdate call from dataBind setter
mfazekas Nov 17, 2025
aae4a7e
fix(android): play state machine after binding mode reload
mfazekas Nov 17, 2025
719ebbe
fix(andorid): use ios like implementation
mfazekas Nov 17, 2025
726d3d4
fix(andorid): use ios like implementation
mfazekas Nov 17, 2025
c46c6aa
removed unused files
mfazekas Nov 17, 2025
9c39621
refator: simplify changes
mfazekas Nov 17, 2025
c49e386
fix: make dataBind optional and default to auto
mfazekas Nov 17, 2025
6ba304e
review fixes
mfazekas Nov 18, 2025
21ae834
renamed miklos_viewmodel to many_viewmodels
mfazekas Nov 18, 2025
233cd12
review fixes
mfazekas Nov 18, 2025
78599a8
fix: add logged to android
mfazekas Nov 18, 2025
b3a94ee
fix: dataBindingChanged
mfazekas Nov 18, 2025
4b467f1
Apply suggestion from @mfazekas
mfazekas Nov 18, 2025
0cf12b1
fix: always apply data binding on initial update
mfazekas Nov 18, 2025
582345d
fix(andorid): always apply data binding on initial update
mfazekas Nov 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 74 additions & 27 deletions android/src/main/java/com/margelo/nitro/rive/HybridRiveView.kt
Original file line number Diff line number Diff line change
@@ -1,18 +1,42 @@
package com.margelo.nitro.rive

import android.util.Log
import androidx.annotation.Keep
import com.facebook.proguard.annotations.DoNotStrip
import com.facebook.react.uimanager.ThemedReactContext
import com.margelo.nitro.core.Promise
import com.rive.BindData
import com.rive.RiveReactNativeView
import com.rive.ViewConfiguration
import app.rive.runtime.kotlin.core.Fit as RiveFit
import app.rive.runtime.kotlin.core.Alignment as RiveAlignment
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

fun Variant_HybridViewModelInstanceSpec_DataBindMode_DataBindByName?.toBindData(): BindData {
if (this == null) return BindData.Auto

return when (this) {
is Variant_HybridViewModelInstanceSpec_DataBindMode_DataBindByName.First -> {
val instance = (this.asFirstOrNull() as? HybridViewModelInstance)?.viewModelInstance
?: throw Error("Invalid ViewModelInstance")
BindData.Instance(instance)
}
is Variant_HybridViewModelInstanceSpec_DataBindMode_DataBindByName.Second -> {
when (this.asSecondOrNull()) {
DataBindMode.AUTO -> BindData.Auto
DataBindMode.NONE -> BindData.None
else -> BindData.None
}
}
is Variant_HybridViewModelInstanceSpec_DataBindMode_DataBindByName.Third -> {
val name = this.asThirdOrNull()?.byName ?: throw Error("Missing byName value")
BindData.ByName(name)
}
}
}

object DefaultConfiguration {
const val AUTOBIND = false
const val AUTOPLAY = true
val FIT = RiveFit.CONTAIN
val ALIGNMENT = RiveAlignment.CENTER
Expand All @@ -25,6 +49,8 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() {
//region State
override val view: RiveReactNativeView = RiveReactNativeView(context)
private var needsReload = false
private var dataBindingChanged = false
private var initialUpdate = true
private var registeredFile: HybridRiveFile? = null
//endregion

Expand All @@ -41,10 +67,6 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() {
set(value) {
changed(field, value) { field = it }
}
override var autoBind: Boolean? = null
set(value) {
changed(field, value) { field = it }
}
override var file: HybridRiveFileSpec = HybridRiveFile()
set(value) {
if (field != value) {
Expand All @@ -56,6 +78,13 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() {
override var alignment: Alignment? = null
override var fit: Fit? = null
override var layoutScaleFactor: Double? = null
override var dataBind: Variant_HybridViewModelInstanceSpec_DataBindMode_DataBindByName? = null
set(value) {
if (field != value) {
field = value
dataBindingChanged = true
}
}
//endregion

//region View Methods
Expand All @@ -73,6 +102,11 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() {
view.bindViewModelInstance(hybridVmi.viewModelInstance)
}

override fun getViewModelInstance(): HybridViewModelInstanceSpec? {
val viewModelInstance = view.getViewModelInstance() ?: return null
return HybridViewModelInstance(viewModelInstance)
}

override fun play() = executeOnUiThread { view.play() }

override fun pause() = executeOnUiThread { view.pause() }
Expand Down Expand Up @@ -109,28 +143,32 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() {
}

override fun afterUpdate() {
val hybridFile = file as? HybridRiveFile
val riveFile = hybridFile?.riveFile ?: return

val config = ViewConfiguration(
artboardName = artboardName,
stateMachineName = stateMachineName,
autoPlay = autoPlay ?: DefaultConfiguration.AUTOPLAY,
autoBind = autoBind ?: DefaultConfiguration.AUTOBIND,
riveFile = riveFile,
alignment = convertAlignment(alignment) ?: DefaultConfiguration.ALIGNMENT,
fit = convertFit(fit) ?: DefaultConfiguration.FIT,
layoutScaleFactor = layoutScaleFactor?.toFloat() ?: DefaultConfiguration.LAYOUTSCALEFACTOR,
)
view.configure(config, needsReload)

if (needsReload && hybridFile != null) {
hybridFile.registerView(this)
registeredFile = hybridFile
}

needsReload = false
super.afterUpdate()
logged(TAG, "afterUpdate") {
val hybridFile = file as? HybridRiveFile
val riveFile = hybridFile?.riveFile ?: return@logged

val config = ViewConfiguration(
artboardName = artboardName,
stateMachineName = stateMachineName,
autoPlay = autoPlay ?: DefaultConfiguration.AUTOPLAY,
riveFile = riveFile,
alignment = convertAlignment(alignment) ?: DefaultConfiguration.ALIGNMENT,
fit = convertFit(fit) ?: DefaultConfiguration.FIT,
layoutScaleFactor = layoutScaleFactor?.toFloat() ?: DefaultConfiguration.LAYOUTSCALEFACTOR,
bindData = dataBind.toBindData()
)
view.configure(config, dataBindingChanged=dataBindingChanged, needsReload, initialUpdate= initialUpdate)

if (needsReload && hybridFile != null) {
hybridFile.registerView(this)
registeredFile = hybridFile
}

needsReload = false
dataBindingChanged = false
initialUpdate = false
super.afterUpdate()
}
}
//endregion

Expand Down Expand Up @@ -184,5 +222,14 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() {
Fit.LAYOUT -> RiveFit.LAYOUT
}
}

fun logged(tag: String, note: String? = null, fn: () -> Unit) {
try {
fn()
} catch (e: Exception) {
// TODO add onError callback
Log.e("[RIVE]", "$tag ${note ?: ""} $e")
}
}
//endregion
}
75 changes: 70 additions & 5 deletions android/src/main/java/com/rive/RiveReactNativeView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,35 @@ import app.rive.runtime.kotlin.core.SMIBoolean
import app.rive.runtime.kotlin.core.SMIInput
import app.rive.runtime.kotlin.core.SMINumber
import app.rive.runtime.kotlin.core.ViewModelInstance
import app.rive.runtime.kotlin.core.errors.ViewModelException
import com.margelo.nitro.core.AnyMap
import com.margelo.nitro.rive.EventPropertiesOutput
import com.margelo.nitro.rive.EventPropertiesOutputExtensions as EPO
import com.margelo.nitro.rive.RiveEventType
import com.margelo.nitro.rive.UnifiedRiveEvent as RNEvent
import kotlinx.coroutines.CompletableDeferred

sealed class BindData {
data object None : BindData()
data object Auto : BindData()
data class Instance(val instance: ViewModelInstance) : BindData()
data class ByName(val name: String) : BindData()
}

data class ViewConfiguration(
val artboardName: String?,
val stateMachineName: String?,
val autoBind: Boolean,
val autoPlay: Boolean,
val riveFile: File,
val alignment: Alignment,
val fit: Fit,
val layoutScaleFactor: Float?
val layoutScaleFactor: Float?,
val bindData: BindData
)

@SuppressLint("ViewConstructor")
class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) {
private var riveAnimationView: RiveAnimationView? = null
internal var riveAnimationView: RiveAnimationView? = null
private var eventListeners: MutableList<RiveFileController.RiveEventListener> = mutableListOf()
private val viewReadyDeferred = CompletableDeferred<Boolean>()
private var _activeStateMachineName: String? = null
Expand All @@ -49,14 +57,14 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) {
return viewReadyDeferred.await()
}

fun configure(config: ViewConfiguration, reload: Boolean = false) {
fun configure(config: ViewConfiguration, dataBindingChanged: Boolean, reload: Boolean = false, initialUpdate: Boolean = false) {
if (reload) {
riveAnimationView?.setRiveFile(
config.riveFile,
artboardName = config.artboardName,
stateMachineName = config.stateMachineName,
autoplay = config.autoPlay,
autoBind = config.autoBind,
autoBind = config.bindData is BindData.Auto,
alignment = config.alignment,
fit = config.fit
)
Expand All @@ -67,6 +75,11 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) {
// TODO: this seems to require a reload for the view to take the new value (bug on Android)
riveAnimationView?.layoutScaleFactor = config.layoutScaleFactor
}

if (dataBindingChanged || initialUpdate) {
applyDataBinding(config.bindData)
}

viewReadyDeferred.complete(true)
}

Expand All @@ -77,6 +90,58 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) {
}
}

fun getViewModelInstance(): ViewModelInstance? {
val stateMachines = riveAnimationView?.controller?.stateMachines
return if (!stateMachines.isNullOrEmpty()) {
stateMachines.first().viewModelInstance
} else {
null
}
}

fun applyDataBinding(bindData: BindData) {
val stateMachines = riveAnimationView?.controller?.stateMachines
if (stateMachines.isNullOrEmpty()) return

val stateMachine = stateMachines.first()

when (bindData) {
is BindData.None -> {
stateMachine.viewModelInstance = null
}
is BindData.Auto -> {
val artboard = riveAnimationView?.controller?.activeArtboard
val file = riveAnimationView?.controller?.file
if (artboard != null && file != null) {
try {
file.defaultViewModelForArtboard(artboard)
} catch (e: ViewModelException) {
null
}?.let {
val instance = it.createDefaultInstance()
stateMachine.viewModelInstance = instance
}
}
}
is BindData.Instance -> {
stateMachine.viewModelInstance = bindData.instance
}
is BindData.ByName -> {
val artboard = riveAnimationView?.controller?.activeArtboard
val file = riveAnimationView?.controller?.file
if (artboard != null && file != null) {
val viewModel = file.defaultViewModelForArtboard(artboard)
val instance = viewModel.createInstanceFromName(bindData.name)
stateMachine.viewModelInstance = instance
}
}
}

stateMachine.name.let { smName ->
riveAnimationView?.play(smName, isStateMachine = true)
}
}

fun play() = riveAnimationView?.play()

fun pause() = riveAnimationView?.pause();
Expand Down
Binary file added example/assets/rive/many_viewmodels.riv
Binary file not shown.
Loading
Loading