Skip to content

Commit 3e900e6

Browse files
committed
feat: add data binding observability
1 parent 9056d35 commit 3e900e6

9 files changed

Lines changed: 440 additions & 13 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.rivereactnative
2+
3+
enum class RNPropertyType(private val mValue: kotlin.String) {
4+
Number("number"),
5+
String("string"),
6+
Boolean("boolean"),
7+
Color("color"),
8+
Trigger("trigger"),
9+
Enum("enum");
10+
11+
override fun toString(): kotlin.String {
12+
return mValue
13+
}
14+
15+
companion object {
16+
17+
fun mapToRNPropertyType(propertyType: kotlin.String): RNPropertyType {
18+
return valueOf(entries.first() { it.toString() == propertyType }.name)
19+
}
20+
}
21+
}

android/src/main/java/com/rivereactnative/RiveReactNativeModule.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,14 @@ class RiveReactNativeModule(reactContext: ReactApplicationContext) : ReactContex
3737
fun getNumberStateAtPath(node: Int, inputName: String, path: String, promise: Promise) {
3838
handleState(node, promise) { view -> view.getNumberStateAtPath(inputName, path) }
3939
}
40+
41+
@ReactMethod
42+
fun addListener(type: String?) {
43+
// Keep: Required for RN built in Event Emitter Calls.
44+
}
45+
46+
@ReactMethod
47+
fun removeListeners(type: Int?) {
48+
// Keep: Required for RN built in Event Emitter Calls.
49+
}
4050
}

android/src/main/java/com/rivereactnative/RiveReactNativeView.kt

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@ import com.facebook.react.bridge.ReadableArray
2727
import com.facebook.react.bridge.ReadableMap
2828
import com.facebook.react.bridge.ReadableType
2929
import com.facebook.react.bridge.WritableMap
30+
import com.facebook.react.modules.core.DeviceEventManagerModule
3031
import com.facebook.react.modules.core.ExceptionsManagerModule
3132
import com.facebook.react.uimanager.ThemedReactContext
3233
import com.facebook.react.uimanager.events.RCTEventEmitter
3334
import kotlinx.coroutines.CoroutineScope
3435
import kotlinx.coroutines.Dispatchers
36+
import kotlinx.coroutines.Job
3537
import kotlinx.coroutines.SupervisorJob
3638
import kotlinx.coroutines.cancel
3739
import kotlinx.coroutines.launch
@@ -98,6 +100,8 @@ class RiveReactNativeView(private val context: ThemedReactContext) : FrameLayout
98100
private var listener: RiveFileController.Listener
99101
private var eventListener: RiveFileController.RiveEventListener
100102
private var assetStore: RiveReactNativeAssetStore? = null
103+
private val scope = CoroutineScope(Dispatchers.Default)
104+
private val propertyListeners = mutableMapOf<String, Job>()
101105

102106
enum class Events(private val mName: String) {
103107
PLAY("onPlay"), PAUSE("onPause"), STOP("onStop"), LOOP_END("onLoopEnd"), STATE_CHANGED("onStateChanged"), RIVE_EVENT(
@@ -175,6 +179,7 @@ class RiveReactNativeView(private val context: ThemedReactContext) : FrameLayout
175179

176180
override fun onDetachedFromWindow() {
177181
if (willDispose) {
182+
scope.cancel()
178183
assetStore?.dispose()
179184
riveAnimationView?.dispose()
180185
removeListeners()
@@ -190,6 +195,7 @@ class RiveReactNativeView(private val context: ThemedReactContext) : FrameLayout
190195
}
191196

192197
private fun removeListeners() {
198+
clearPropertyListeners()
193199
riveAnimationView?.unregisterListener(listener)
194200
riveAnimationView?.removeEventListener(eventListener)
195201
}
@@ -425,6 +431,43 @@ class RiveReactNativeView(private val context: ThemedReactContext) : FrameLayout
425431
}
426432
}
427433

434+
fun registerPropertyListener(path: String, propertyType: String) {
435+
val key = "$propertyType:$path"
436+
437+
val propertyTypeEnum = RNPropertyType.mapToRNPropertyType(propertyType);
438+
439+
val property = when (propertyTypeEnum) {
440+
RNPropertyType.String -> getViewModelInstance()?.getStringProperty(path)
441+
RNPropertyType.Boolean -> getViewModelInstance()?.getBooleanProperty(path)
442+
RNPropertyType.Number -> getViewModelInstance()?.getNumberProperty(path)
443+
RNPropertyType.Color -> getViewModelInstance()?.getColorProperty(path)
444+
RNPropertyType.Enum -> getViewModelInstance()?.getEnumProperty(path)
445+
RNPropertyType.Trigger -> getViewModelInstance()?.getTriggerProperty(path)
446+
} ?: return
447+
448+
// This should not be required, as JavaScript does a check to ensure
449+
// event emitters are unique. Adding this as a safety.
450+
propertyListeners[key]?.cancel()
451+
452+
val job = scope.launch {
453+
property.valueFlow.collect { value ->
454+
sendEvent(key, value)
455+
}
456+
}
457+
propertyListeners[key] = job
458+
}
459+
460+
private fun sendEvent(eventName: String, value: Any) {
461+
context
462+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
463+
.emit(eventName, value)
464+
}
465+
466+
private fun clearPropertyListeners() {
467+
propertyListeners.values.forEach { it.cancel() }
468+
propertyListeners.clear()
469+
}
470+
428471
fun update() {
429472
reloadIfNeeded()
430473
}

android/src/main/java/com/rivereactnative/RiveReactNativeViewManager.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,16 @@ class RiveReactNativeViewManager : SimpleViewManager<RiveReactNativeView>() {
171171
}
172172
}
173173

174+
"registerPropertyListener" -> {
175+
args?.let {
176+
val path = it.getString(0)
177+
val propertyType = it.getString(1)
178+
view.run {
179+
registerPropertyListener(path, propertyType)
180+
}
181+
}
182+
}
183+
174184
// Touch Events
175185

176186
"touchBegan" -> {

example/app/(examples)/DataBinding.tsx

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,48 @@
1-
import React from 'react';
1+
import React, { useEffect } from 'react';
22
import {
33
SafeAreaView,
44
ScrollView,
55
StyleSheet,
66
Button,
77
View,
88
} from 'react-native';
9-
import Rive, { Fit, RiveRef } from 'rive-react-native';
9+
import Rive, {
10+
Fit,
11+
RiveRef,
12+
useRiveColor,
13+
useRiveNumber,
14+
useRiveReady,
15+
useRiveString,
16+
} from 'rive-react-native';
1017

1118
export default function DataBinding() {
1219
const riveRef = React.useRef<RiveRef>(null);
20+
const riveIsReady = useRiveReady(riveRef);
1321

22+
let [buttonText, setButtonText] = useRiveString(riveRef, 'Button/State_1');
23+
let [lives, setLives] = useRiveNumber(riveRef, 'Energy_Bar/Lives');
24+
let [barColor, setBarColor] = useRiveColor(riveRef, 'Energy_Bar/Bar_Color');
25+
26+
useEffect(() => {
27+
if (riveIsReady) {
28+
// Set initial values through hooks
29+
setButtonText("Let's go!");
30+
setLives(7);
31+
setBarColor({ r: 0, g: 255, b: 0, a: 255 });
32+
}
33+
}, [riveIsReady, setButtonText, setLives, setBarColor]);
34+
35+
console.log('Button Text:', buttonText);
36+
console.log('Lives:', lives);
37+
console.log('Bar Color:', barColor);
38+
39+
// Set values directly
1440
const updateDataBindingValues = () => {
15-
// Update the view model instance properties
16-
// NUMBER VALUE
17-
riveRef.current?.setNumberPropertyValue('Energy_Bar/Lives', 7);
18-
// STRING VALUE
19-
riveRef.current?.setStringPropertyValue('Button/State_1', "Let's go!");
20-
// COLOR VALUE
41+
// SET NUMBER VALUE
42+
riveRef.current?.setNumberPropertyValue('Energy_Bar/Lives', 6);
43+
// SET STRING VALUE
44+
riveRef.current?.setStringPropertyValue('Button/State_1', 'Direct!');
45+
// SET COLOR VALUE
2146
riveRef.current?.setColorPropertyValue('Energy_Bar/Bar_Color', {
2247
r: 0,
2348
g: 255,

0 commit comments

Comments
 (0)