Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 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
51 changes: 45 additions & 6 deletions android/src/main/java/com/margelo/nitro/rive/HybridRiveView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,38 @@ 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 +48,7 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() {
//region State
override val view: RiveReactNativeView = RiveReactNativeView(context)
private var needsReload = false
private var firstUpdate = true
private var registeredFile: HybridRiveFile? = null
//endregion

Expand All @@ -41,10 +65,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 +76,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
applyDataBinding()
}
}
//endregion

//region View Methods
Expand All @@ -73,6 +100,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 @@ -103,24 +135,31 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() {
view.getTextRunValue(name, path)
//endregion

//region Data Binding
private fun applyDataBinding() {
view.applyDataBinding(dataBind.toBindData(), shouldRefresh = !firstUpdate)
}
//endregion

//region Update
fun refreshAfterAssetChange() {
afterUpdate()
}

override fun afterUpdate() {
firstUpdate = false
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,
bindData = dataBind.toBindData()
)
view.configure(config, needsReload)

Expand Down
67 changes: 63 additions & 4 deletions android/src/main/java/com/rive/RiveReactNativeView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,27 @@ 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 @@ -56,7 +63,7 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) {
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 +74,9 @@ 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
}

applyDataBinding(config.bindData, shouldRefresh = false)

viewReadyDeferred.complete(true)
}

Expand All @@ -77,6 +87,55 @@ 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, shouldRefresh: Boolean = false) {
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) {
val viewModel = file.defaultViewModelForArtboard(artboard)
val instance = viewModel.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
}
}
}

if (shouldRefresh) {
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/miklos_viewmodels.riv
Binary file not shown.
148 changes: 148 additions & 0 deletions example/src/pages/MIklosViewModels.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { useState, useMemo } from 'react';
import type { Metadata } from '../helpers/metadata';
import {
DataBindMode,
RiveView,
useRiveFile,
type ViewModelInstance,
} from 'react-native-rive';

type BindModeOption =
| 'none'
| 'auto'
| 'red'
| 'green'
| 'blue'
| 'green-instance';

function getDataBindValue(
mode: BindModeOption,
greenInstance: ViewModelInstance | null
) {
if (mode === 'none') return DataBindMode.None;
if (mode === 'auto') return DataBindMode.Auto;
if (mode === 'green-instance' && greenInstance) return greenInstance;
return { byName: mode };
}

export default function DataBindingMode() {
const { riveFile } = useRiveFile(
require('../../assets/rive/miklos_viewmodels.riv')
);
const [bindMode, setBindMode] = useState<BindModeOption>('none');

// Create a ViewModelInstance for "green" to demonstrate instance binding
const greenInstance = useMemo(() => {
if (!riveFile) return null;
try {
const viewModel = riveFile.defaultArtboardViewModel();
if (!viewModel) return null;
return viewModel.createInstanceByName('green');
} catch (e) {
console.error('Failed to create green instance:', e);
return null;
}
}, [riveFile]);

const dataBindValue = getDataBindValue(bindMode, greenInstance);

return (
<View style={styles.container}>
<View style={styles.controlsContainer}>
<Text style={styles.label}>Binding Mode:</Text>
<View style={styles.buttonRow}>
{(
[
'none',
'auto',
'red',
'green',
'blue',
'green-instance',
] as BindModeOption[]
).map((mode) => (
<TouchableOpacity
key={mode}
style={[styles.button, bindMode === mode && styles.buttonActive]}
onPress={() => setBindMode(mode)}
>
<Text
style={[
styles.buttonText,
bindMode === mode && styles.buttonTextActive,
]}
>
{mode === 'green-instance' ? 'green (instance)' : mode}
</Text>
</TouchableOpacity>
))}
</View>
</View>
{riveFile && (
<RiveView
style={styles.rive}
file={riveFile}
dataBind={dataBindValue}
autoPlay={true}
/>
)}
</View>
);
}

DataBindingMode.metadata = {
name: 'Miklos View Models',
description:
'Interactive data binding mode selector (none, auto, byName, and instance)',
} satisfies Metadata;

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
controlsContainer: {
padding: 16,
backgroundColor: '#f8f8f8',
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
},
label: {
fontSize: 14,
fontWeight: '600',
marginBottom: 8,
color: '#333',
},
buttonRow: {
flexDirection: 'row',
gap: 8,
flexWrap: 'wrap',
},
button: {
paddingHorizontal: 16,
paddingVertical: 8,
backgroundColor: '#fff',
borderRadius: 8,
borderWidth: 1,
borderColor: '#d0d0d0',
},
buttonActive: {
backgroundColor: '#007AFF',
borderColor: '#007AFF',
},
buttonText: {
fontSize: 14,
fontWeight: '500',
color: '#333',
textTransform: 'capitalize',
},
buttonTextActive: {
color: '#fff',
},
rive: {
flex: 1,
width: '100%',
height: '100%',
},
});
2 changes: 2 additions & 0 deletions example/src/pages/OutOfBandAssets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
/*Rive, */ Fit,
/*RNRiveError,*/ useRiveFile,
RiveView,
DataBindMode,
} from 'react-native-rive';
import { Picker } from '@react-native-picker/picker';
import { type Metadata } from '../helpers/metadata';
Expand Down Expand Up @@ -65,6 +66,7 @@ export default function StateMachine() {
fit={Fit.Contain}
style={styles.animation}
stateMachineName="State Machine 1"
dataBind={DataBindMode.None}
// The `referencedAssets` prop allows you to load external assets from various sources:
// - A URI
// - A bundled asset on the native platform (iOS and Android)
Expand Down
Loading
Loading