Skip to content

Commit 5513771

Browse files
committed
feat: add Reanimated shared value to Rive property binding example
Demonstrates driving Rive ViewModelNumberProperty from Reanimated shared values using NitroModules.box() to share HybridObjects with worklets.
1 parent 12aa483 commit 5513771

6 files changed

Lines changed: 226 additions & 6 deletions

File tree

example/assets/rive/movecircle.riv

307 Bytes
Binary file not shown.

example/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
"react-native": "0.79.2",
1818
"react-native-gesture-handler": "^2.25.0",
1919
"react-native-nitro-modules": "^0.31.3",
20-
"react-native-safe-area-context": "^5.4.0"
20+
"react-native-reanimated": "^4.1.5",
21+
"react-native-safe-area-context": "^5.4.0",
22+
"react-native-worklets-core": "^1.6.2"
2123
},
2224
"devDependencies": {
2325
"@babel/core": "^7.25.2",
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import {
2+
View,
3+
Text,
4+
StyleSheet,
5+
Button,
6+
ActivityIndicator,
7+
} from 'react-native';
8+
import { type Metadata } from '../helpers/metadata';
9+
import Animated, {
10+
useSharedValue,
11+
useAnimatedReaction,
12+
useAnimatedStyle,
13+
withSpring,
14+
} from 'react-native-reanimated';
15+
import { useEffect, useMemo } from 'react';
16+
import { NitroModules } from 'react-native-nitro-modules';
17+
import {
18+
Fit,
19+
RiveView,
20+
useRiveFile,
21+
type RiveFile,
22+
type ViewModelInstance,
23+
type ViewModelNumberProperty,
24+
} from '@rive-app/react-native';
25+
26+
export default function SharedValueListenerExample() {
27+
const { riveFile, isLoading, error } = useRiveFile(
28+
require('../../assets/rive/movecircle.riv')
29+
);
30+
31+
return (
32+
<View style={styles.container}>
33+
{isLoading ? (
34+
<ActivityIndicator size="large" color="#0000ff" />
35+
) : riveFile ? (
36+
<WithViewModelSetup file={riveFile} />
37+
) : (
38+
<Text style={styles.errorText}>{error || 'Unexpected error'}</Text>
39+
)}
40+
</View>
41+
);
42+
}
43+
44+
function WithViewModelSetup({ file }: { file: RiveFile }) {
45+
const viewModel = useMemo(() => file.defaultArtboardViewModel(), [file]);
46+
const instance = useMemo(
47+
() => viewModel?.createDefaultInstance(),
48+
[viewModel]
49+
);
50+
51+
if (!instance || !viewModel) {
52+
return (
53+
<Text style={styles.errorText}>
54+
{!viewModel
55+
? 'No view model found'
56+
: 'Failed to create view model instance'}
57+
</Text>
58+
);
59+
}
60+
61+
return <AnimatedRiveExample instance={instance} file={file} />;
62+
}
63+
64+
function AnimatedRiveExample({
65+
instance,
66+
file,
67+
}: {
68+
instance: ViewModelInstance;
69+
file: RiveFile;
70+
}) {
71+
const progress = useSharedValue(0);
72+
73+
const boxedProperty = useMemo(() => {
74+
const posYProperty = instance.numberProperty('posY');
75+
if (!posYProperty) {
76+
return null;
77+
}
78+
return NitroModules.box(posYProperty);
79+
}, [instance]);
80+
81+
useAnimatedReaction(
82+
() => progress.value,
83+
(value: number) => {
84+
'worklet';
85+
if (!boxedProperty) return;
86+
const property = boxedProperty.unbox() as ViewModelNumberProperty;
87+
property.value = value;
88+
},
89+
[boxedProperty]
90+
);
91+
92+
const circleStyle = useAnimatedStyle(() => ({
93+
transform: [{ translateY: progress.value / 3 }],
94+
}));
95+
96+
const animateTo800 = () => {
97+
progress.value = 0;
98+
progress.value = withSpring(800, {
99+
damping: 8,
100+
stiffness: 80,
101+
});
102+
};
103+
104+
const animateTo0 = () => {
105+
progress.value = withSpring(0, {
106+
damping: 8,
107+
stiffness: 80,
108+
});
109+
};
110+
111+
useEffect(() => {
112+
animateTo800();
113+
// eslint-disable-next-line react-hooks/exhaustive-deps
114+
}, []);
115+
116+
return (
117+
<View style={styles.container}>
118+
<Text style={styles.subtitle}>
119+
Circle posY is driven by Reanimated shared value (Red circle is rive,
120+
Blue circle is React Native View)
121+
</Text>
122+
123+
<View style={styles.riveContainer}>
124+
<RiveView
125+
style={styles.rive}
126+
autoPlay={true}
127+
dataBind={instance}
128+
fit={Fit.Layout}
129+
layoutScaleFactor={1}
130+
file={file}
131+
/>
132+
<Animated.View style={[styles.blueCircle, circleStyle]} />
133+
</View>
134+
135+
<View style={styles.buttonContainer}>
136+
<Button title="Bounce to 800" onPress={animateTo800} />
137+
<Button title="Bounce to 0" onPress={animateTo0} />
138+
</View>
139+
</View>
140+
);
141+
}
142+
143+
SharedValueListenerExample.metadata = {
144+
name: 'Reanimated Shared Value',
145+
description: 'Drive Rive properties from Reanimated shared values',
146+
} satisfies Metadata;
147+
148+
const styles = StyleSheet.create({
149+
container: {
150+
flex: 1,
151+
backgroundColor: '#fff',
152+
},
153+
title: {
154+
fontSize: 24,
155+
fontWeight: 'bold',
156+
textAlign: 'center',
157+
marginTop: 20,
158+
marginBottom: 10,
159+
},
160+
subtitle: {
161+
fontSize: 16,
162+
color: '#666',
163+
textAlign: 'center',
164+
marginBottom: 20,
165+
paddingHorizontal: 20,
166+
},
167+
riveContainer: {
168+
flex: 1,
169+
backgroundColor: '#f5f5f5',
170+
},
171+
rive: {
172+
flex: 1,
173+
width: '100%',
174+
height: '100%',
175+
},
176+
errorText: {
177+
color: 'red',
178+
textAlign: 'center',
179+
padding: 20,
180+
},
181+
buttonContainer: {
182+
flexDirection: 'row',
183+
justifyContent: 'center',
184+
gap: 20,
185+
padding: 20,
186+
},
187+
blueCircle: {
188+
position: 'absolute',
189+
left: 50,
190+
top: -20,
191+
width: 40,
192+
height: 40,
193+
borderRadius: 20,
194+
backgroundColor: 'blue',
195+
},
196+
});

example/src/pages/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export { default as TextRunExample } from './RiveTextRunExample';
77
export { default as OutOfBandAssets } from './OutOfBandAssets';
88
export { default as ManyViewModels } from './ManyViewModels';
99
export { default as ResponsiveLayouts } from './ResponsiveLayouts';
10+
export { default as SharedValueListenerExample } from './SharedValueListenerExample';

ios/HybridViewModelNumberProperty.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,20 @@ import RiveRuntime
33
class HybridViewModelNumberProperty: HybridViewModelNumberPropertySpec, ValuedPropertyProtocol {
44
var property: NumberPropertyType!
55
lazy var helper = PropertyListenerHelper(property: property!)
6-
6+
77
init(property: NumberPropertyType) {
88
self.property = property
99
super.init()
1010
}
11-
11+
1212
/// ⚠️ DO NOT REMOVE
1313
/// Nitro requires a parameterless initializer for JS bridging.
1414
/// This is invoked automatically during hybrid module construction.
1515
/// Internally we always use `init(property:)`
1616
override init() {
1717
super.init()
1818
}
19-
19+
2020
var value: Double {
2121
get {
2222
return Double(property.value)
@@ -25,7 +25,7 @@ class HybridViewModelNumberProperty: HybridViewModelNumberPropertySpec, ValuedPr
2525
property.value = Float(newValue)
2626
}
2727
}
28-
28+
2929
// Custom addListener needed because ListenerValueType (Float) != ValueType (Double)
3030
func addListener(onChanged: @escaping (Double) -> Void) throws {
3131
helper.addListener { (value: Float) in

yarn.lock

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13909,7 +13909,7 @@ __metadata:
1390913909
languageName: node
1391013910
linkType: hard
1391113911

13912-
"react-native-reanimated@npm:~4.1.1":
13912+
"react-native-reanimated@npm:^4.1.5, react-native-reanimated@npm:~4.1.1":
1391313913
version: 4.1.5
1391413914
resolution: "react-native-reanimated@npm:4.1.5"
1391513915
dependencies:
@@ -13947,7 +13947,9 @@ __metadata:
1394713947
react-native-builder-bob: ^0.40.10
1394813948
react-native-gesture-handler: ^2.25.0
1394913949
react-native-nitro-modules: ^0.31.3
13950+
react-native-reanimated: ^4.1.5
1395013951
react-native-safe-area-context: ^5.4.0
13952+
react-native-worklets-core: ^1.6.2
1395113953
languageName: unknown
1395213954
linkType: soft
1395313955

@@ -13994,6 +13996,18 @@ __metadata:
1399413996
languageName: node
1399513997
linkType: hard
1399613998

13999+
"react-native-worklets-core@npm:^1.6.2":
14000+
version: 1.6.2
14001+
resolution: "react-native-worklets-core@npm:1.6.2"
14002+
dependencies:
14003+
string-hash-64: ^1.0.3
14004+
peerDependencies:
14005+
react: "*"
14006+
react-native: "*"
14007+
checksum: 367981bfc8adf2f989ae73300849a5b8ef89bcea0174480e6ff1614df075f1eab28e30899daefa081a5c06588ca2df4d629c3de74e69f5830cbe54e004cf7235
14008+
languageName: node
14009+
linkType: hard
14010+
1399714011
"react-native-worklets@npm:0.5.1":
1399814012
version: 0.5.1
1399914013
resolution: "react-native-worklets@npm:0.5.1"
@@ -15309,6 +15323,13 @@ __metadata:
1530915323
languageName: node
1531015324
linkType: hard
1531115325

15326+
"string-hash-64@npm:^1.0.3":
15327+
version: 1.0.3
15328+
resolution: "string-hash-64@npm:1.0.3"
15329+
checksum: 79de8431b4fa3e85a2429cd52a34f7948221ff167b7a094e05d6bcfd0173474b232e0c9845c96f74b0d7b6b0c8bbe2c3532a4cacb21635293ef0cf3cc8e77f06
15330+
languageName: node
15331+
linkType: hard
15332+
1531215333
"string-length@npm:^4.0.1":
1531315334
version: 4.0.2
1531415335
resolution: "string-length@npm:4.0.2"

0 commit comments

Comments
 (0)