Skip to content

Commit 743e566

Browse files
committed
feat: add rive events listener
1 parent a5bb08c commit 743e566

File tree

11 files changed

+388
-28
lines changed

11 files changed

+388
-28
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ The following runtime features are currently supported:
8888
| Asset management | 🚧 | Out-of-band assets not yet supported |
8989
| State machine inputs | 🚧 | Get/Set (nested) state machine inputs (legacy, see data binding) |
9090
| Text Runs | 🚧 | Update (nested) text runs (legacy, see data binding) |
91-
| Rive Events | 🚧 | Listen to Rive events |
91+
| Rive Events | | Listen to Rive events |
9292
| Rive Audio || Full Rive audio playback supported |
9393
| `useRive()` hook || Convenient hook to access the Rive View ref after load |
9494
| `useRiveFile()` hook | 🚧 | Convenient hook to load a Rive file |

android/src/main/java/com/margelo/nitro/rive/HybridRiveView.kt

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,26 +29,42 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() {
2929
}
3030

3131
override var artboardName: String? = null
32-
set(value) { changed(field, value) { field = it } }
32+
set(value) {
33+
changed(field, value) { field = it }
34+
}
3335
override var stateMachineName: String? = null
34-
set(value) { changed(field, value) { field = it } }
36+
set(value) {
37+
changed(field, value) { field = it }
38+
}
3539
override var autoPlay: Boolean? = null
36-
set(value) { changed(field, value) { field = it } }
40+
set(value) {
41+
changed(field, value) { field = it }
42+
}
3743
override var autoBind: Boolean? = null
38-
set(value) { changed(field, value) { field = it } }
44+
set(value) {
45+
changed(field, value) { field = it }
46+
}
3947
override var file: HybridRiveFileSpec = HybridRiveFile()
40-
set(value) { changed(field, value) { field = it } }
48+
set(value) {
49+
changed(field, value) { field = it }
50+
}
4151
override var alignment: Alignment? = null
4252
override var fit: Fit? = null
4353
//endregion
4454

4555
//region View Methods
46-
override fun bindViewModelInstance(viewModelInstance: HybridViewModelInstanceSpec) = executeOnUiThread {
47-
val hybridVmi = viewModelInstance as? HybridViewModelInstance ?: return@executeOnUiThread;
48-
view.bindViewModelInstance(hybridVmi.viewModelInstance)
49-
}
56+
override fun bindViewModelInstance(viewModelInstance: HybridViewModelInstanceSpec) =
57+
executeOnUiThread {
58+
val hybridVmi = viewModelInstance as? HybridViewModelInstance ?: return@executeOnUiThread;
59+
view.bindViewModelInstance(hybridVmi.viewModelInstance)
60+
}
61+
5062
override fun play() = executeOnUiThread { view.play() }
5163
override fun pause() = executeOnUiThread { view.pause() }
64+
override fun onEventListener(onEvent: (event: RiveEvent) -> Unit) =
65+
executeOnUiThread { view.addEventListener(onEvent) }
66+
67+
override fun removeEventListeners() = executeOnUiThread { view.removeEventListeners() }
5268
//endregion
5369

5470
//region Update

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

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,17 @@ import android.annotation.SuppressLint
44
import android.widget.FrameLayout
55
import com.facebook.react.uimanager.ThemedReactContext
66
import app.rive.runtime.kotlin.RiveAnimationView
7+
import app.rive.runtime.kotlin.controllers.RiveFileController
78
import app.rive.runtime.kotlin.core.Alignment
89
import app.rive.runtime.kotlin.core.File
910
import app.rive.runtime.kotlin.core.Fit
11+
import app.rive.runtime.kotlin.core.RiveEvent
12+
import app.rive.runtime.kotlin.core.RiveGeneralEvent
13+
import app.rive.runtime.kotlin.core.RiveOpenURLEvent
1014
import app.rive.runtime.kotlin.core.ViewModelInstance
15+
import com.margelo.nitro.core.AnyMap
16+
import com.margelo.nitro.rive.RiveEventType
17+
import com.margelo.nitro.rive.RiveEvent as RNEvent
1118

1219
data class ViewConfiguration(
1320
val artboardName: String?,
@@ -22,6 +29,8 @@ data class ViewConfiguration(
2229
@SuppressLint("ViewConstructor")
2330
class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) {
2431
private var riveAnimationView: RiveAnimationView? = null
32+
private val eventListeners: MutableList<RiveFileController.RiveEventListener> = mutableListOf()
33+
private val eventLock = Any()
2534

2635
init {
2736
riveAnimationView = RiveAnimationView(context)
@@ -36,12 +45,38 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) {
3645
}
3746
}
3847

39-
fun play() {
40-
riveAnimationView?.play();
48+
fun play() = riveAnimationView?.play()
49+
50+
fun pause() = riveAnimationView?.pause();
51+
52+
fun addEventListener(onEvent: (event: RNEvent) -> Unit) {
53+
val eventListener = object : RiveFileController.RiveEventListener {
54+
override fun notifyEvent(event: RiveEvent) {
55+
val rnEvent = RNEvent(
56+
name = event.name,
57+
type = if (event is RiveOpenURLEvent) RiveEventType.OPENURL else RiveEventType.GENERAL,
58+
delay = event.delay.toDouble(),
59+
properties = convertEventProperties(event.properties),
60+
url = (event as? RiveOpenURLEvent)?.url,
61+
target = (event as? RiveOpenURLEvent)?.target
62+
)
63+
64+
onEvent(rnEvent)
65+
}
66+
}
67+
synchronized(eventLock) {
68+
riveAnimationView?.addEventListener(eventListener)
69+
eventListeners.add(eventListener)
70+
}
4171
}
4272

43-
fun pause() {
44-
riveAnimationView?.pause();
73+
fun removeEventListeners() {
74+
synchronized(eventLock) {
75+
for (eventListener in eventListeners) {
76+
riveAnimationView?.removeEventListener(eventListener)
77+
}
78+
eventListeners.clear()
79+
}
4580
}
4681

4782
fun configure(config: ViewConfiguration, reload: Boolean = false) {
@@ -63,6 +98,20 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) {
6398
//endregion
6499

65100
//region Internal
101+
private fun convertEventProperties(properties: Map<String, Any>?): AnyMap? {
102+
if (properties == null) return null
103+
104+
val newMap = AnyMap()
66105

106+
properties.forEach { (key, value) ->
107+
when (value) {
108+
is String -> newMap.setString(key, value)
109+
is Number -> newMap.setDouble(key, value.toDouble())
110+
is Boolean -> newMap.setBoolean(key, value)
111+
}
112+
}
113+
114+
return newMap;
115+
}
67116
//endregion
68117
}

example/assets/rive/rating.riv

-366 Bytes
Binary file not shown.

example/src/App.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ import { createStackNavigator } from '@react-navigation/stack';
1010
import RiveFileLoadingExample from './pages/RiveFileLoadingExample';
1111
import DataBindingExample from './pages/RiveDataBindingExample';
1212
import TemplatePage from './pages/TemplatePage';
13+
import EventsExample from './pages/RiveEventsExample';
1314

1415
type RootStackParamList = {
1516
Home: undefined;
1617
RiveFileLoading: undefined;
1718
RiveDataBinding: undefined;
19+
RiveEvents: undefined;
1820
Template: undefined;
1921
};
2022

@@ -37,6 +39,12 @@ function HomeScreen({ navigation }: { navigation: any }) {
3739
>
3840
<Text style={styles.buttonText}>Rive Data Binding Example</Text>
3941
</TouchableOpacity>
42+
<TouchableOpacity
43+
style={styles.button}
44+
onPress={() => navigation.navigate('RiveEvents')}
45+
>
46+
<Text style={styles.buttonText}>Rive Events Example</Text>
47+
</TouchableOpacity>
4048
<TouchableOpacity
4149
style={styles.button}
4250
onPress={() => navigation.navigate('Template')}
@@ -77,6 +85,11 @@ export default function App() {
7785
component={DataBindingExample}
7886
options={{ title: 'Rive Data Binding' }}
7987
/>
88+
<Stack.Screen
89+
name="RiveEvents"
90+
component={EventsExample}
91+
options={{ title: 'Rive Events' }}
92+
/>
8093
<Stack.Screen
8194
name="Template"
8295
component={TemplatePage}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { View, Text, StyleSheet, ActivityIndicator } from 'react-native';
2+
import { useState, useEffect } from 'react';
3+
import {
4+
Fit,
5+
RiveView,
6+
RiveFileFactory,
7+
useRive,
8+
type RiveFile,
9+
RiveEventType,
10+
type RiveEvent,
11+
} from 'react-native-rive';
12+
13+
export default function EventsExample() {
14+
const { riveViewRef, setHybridRef } = useRive();
15+
const [riveFile, setRiveFile] = useState<RiveFile | null>(null);
16+
const [isLoading, setIsLoading] = useState(true);
17+
const [error, setError] = useState<string | null>(null);
18+
const [lastEvent, setLastEvent] = useState<RiveEvent | null>(null);
19+
20+
useEffect(() => {
21+
let currentFile: RiveFile | null = null;
22+
23+
const loadRiveFile = async () => {
24+
try {
25+
const file = await RiveFileFactory.fromSource(
26+
require('../../assets/rive/rating.riv')
27+
);
28+
currentFile = file;
29+
setRiveFile(file);
30+
setIsLoading(false);
31+
setError(null);
32+
} catch (err) {
33+
setError(
34+
err instanceof Error ? err.message : 'Failed to load Rive file'
35+
);
36+
setIsLoading(false);
37+
}
38+
};
39+
40+
loadRiveFile();
41+
42+
// Cleanup function to release the Rive file when component unmounts
43+
return () => {
44+
if (currentFile) {
45+
currentFile.release();
46+
}
47+
};
48+
}, []);
49+
50+
const handleRiveEvent = (event: any) => {
51+
console.log('Rive Event:', event);
52+
if (event.type === RiveEventType.General) {
53+
setLastEvent(event);
54+
}
55+
};
56+
57+
// Add event listener when the ref is available
58+
useEffect(() => {
59+
if (riveViewRef) {
60+
riveViewRef.onEventListener(handleRiveEvent);
61+
}
62+
return () => {
63+
if (riveViewRef) {
64+
riveViewRef.removeEventListeners();
65+
}
66+
};
67+
}, [riveViewRef]);
68+
69+
return (
70+
<View style={styles.container}>
71+
<View style={styles.riveContainer}>
72+
{isLoading ? (
73+
<ActivityIndicator size="large" color="#0000ff" />
74+
) : error ? (
75+
<Text style={styles.errorText}>{error}</Text>
76+
) : riveFile ? (
77+
<RiveView
78+
style={styles.rive}
79+
autoBind={false}
80+
autoPlay={true}
81+
fit={Fit.Contain}
82+
file={riveFile}
83+
hybridRef={setHybridRef}
84+
/>
85+
) : null}
86+
</View>
87+
{lastEvent && (
88+
<View style={styles.eventInfo}>
89+
<Text style={styles.eventTitle}>Last Event:</Text>
90+
<Text>Name: {lastEvent.name}</Text>
91+
<Text>
92+
Type:{' '}
93+
{lastEvent.type === RiveEventType.General
94+
? 'General Event'
95+
: 'Open URL Event'}
96+
</Text>
97+
{lastEvent.url && <Text>URL: {lastEvent.url}</Text>}
98+
{lastEvent.target && <Text>Target: {lastEvent.target}</Text>}
99+
{lastEvent.properties &&
100+
Object.keys(lastEvent.properties).length > 0 && (
101+
<>
102+
<Text style={styles.propertiesTitle}>Properties:</Text>
103+
{Object.entries(lastEvent.properties).map(([key, value]) => (
104+
<Text key={key}>
105+
{key}: {String(value)}
106+
</Text>
107+
))}
108+
</>
109+
)}
110+
</View>
111+
)}
112+
</View>
113+
);
114+
}
115+
116+
const styles = StyleSheet.create({
117+
container: {
118+
flex: 1,
119+
backgroundColor: '#fff',
120+
padding: 20,
121+
},
122+
title: {
123+
fontSize: 24,
124+
fontWeight: 'bold',
125+
textAlign: 'center',
126+
marginTop: 20,
127+
marginBottom: 10,
128+
},
129+
subtitle: {
130+
fontSize: 16,
131+
color: '#666',
132+
textAlign: 'center',
133+
marginBottom: 20,
134+
},
135+
riveContainer: {
136+
flex: 1,
137+
justifyContent: 'center',
138+
alignItems: 'center',
139+
backgroundColor: '#f5f5f5',
140+
borderRadius: 10,
141+
overflow: 'hidden',
142+
},
143+
rive: {
144+
width: '100%',
145+
height: '100%',
146+
},
147+
errorText: {
148+
color: 'red',
149+
textAlign: 'center',
150+
padding: 20,
151+
},
152+
eventInfo: {
153+
marginTop: 20,
154+
padding: 15,
155+
backgroundColor: '#f0f0f0',
156+
borderRadius: 8,
157+
},
158+
eventTitle: {
159+
fontSize: 18,
160+
fontWeight: 'bold',
161+
marginBottom: 10,
162+
},
163+
propertiesTitle: {
164+
fontSize: 16,
165+
fontWeight: 'bold',
166+
marginTop: 10,
167+
marginBottom: 5,
168+
},
169+
});

ios/HybridRiveView.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ class HybridRiveView : HybridRiveViewSpec {
2828
}
2929
func play() throws { riveView?.play() }
3030
func pause() throws { riveView?.pause() }
31+
func onEventListener(onEvent: @escaping (RiveEvent) -> Void) throws {
32+
riveView?.addEventListener(onEvent)
33+
}
34+
35+
func removeEventListeners() throws { riveView?.removeEventListeners() }
3136

3237
// MARK: Views
3338
var view: UIView = RiveReactNativeView()

0 commit comments

Comments
 (0)