Skip to content

Commit 518ce68

Browse files
committed
feat: add data binding lists example with list.riv
1 parent 403971b commit 518ce68

4 files changed

Lines changed: 257 additions & 6 deletions

File tree

example/assets/list.riv

857 KB
Binary file not shown.
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
import {
2+
View,
3+
Text,
4+
StyleSheet,
5+
ActivityIndicator,
6+
TouchableOpacity,
7+
} from 'react-native';
8+
import { useMemo, useState, useCallback, useRef } from 'react';
9+
import {
10+
Fit,
11+
RiveView,
12+
type ViewModelInstance,
13+
type RiveFile,
14+
type RiveViewRef,
15+
useRiveFile,
16+
} from '@rive-app/react-native';
17+
import { type Metadata } from '../helpers/metadata';
18+
19+
export default function DataBindingListExample() {
20+
const { riveFile, isLoading, error } = useRiveFile(
21+
require('../../assets/list.riv')
22+
);
23+
24+
return (
25+
<View style={styles.container}>
26+
<View style={styles.riveContainer}>
27+
{isLoading ? (
28+
<ActivityIndicator size="large" color="#0000ff" />
29+
) : riveFile ? (
30+
<WithViewModelSetup file={riveFile} />
31+
) : (
32+
<Text style={styles.errorText}>{error || 'Unexpected error'}</Text>
33+
)}
34+
</View>
35+
</View>
36+
);
37+
}
38+
39+
function WithViewModelSetup({ file }: { file: RiveFile }) {
40+
const viewModel = useMemo(() => file.viewModelByName('menu VM'), [file]);
41+
const instance = useMemo(
42+
() => viewModel?.createDefaultInstance(),
43+
[viewModel]
44+
);
45+
46+
if (!instance || !viewModel) {
47+
return (
48+
<Text style={styles.errorText}>
49+
{!viewModel
50+
? "No view model 'menu VM' found"
51+
: 'Failed to create view model instance'}
52+
</Text>
53+
);
54+
}
55+
56+
return <ListExample instance={instance} file={file} />;
57+
}
58+
59+
function ListExample({
60+
instance,
61+
file,
62+
}: {
63+
instance: ViewModelInstance;
64+
file: RiveFile;
65+
}) {
66+
const riveRef = useRef<RiveViewRef>(undefined);
67+
const [isPlaying, setIsPlaying] = useState(true);
68+
const listProperty = useMemo(
69+
() => instance.listProperty('ListItemVM'),
70+
[instance]
71+
);
72+
const [listLength, setListLength] = useState(listProperty?.length ?? 0);
73+
74+
const refreshLength = useCallback(() => {
75+
setListLength(listProperty?.length ?? 0);
76+
}, [listProperty]);
77+
78+
const handleAddItem = useCallback(() => {
79+
if (!listProperty) return;
80+
const buttonVM = file.viewModelByName('button VM');
81+
if (!buttonVM) {
82+
console.error('button VM view model not found');
83+
return;
84+
}
85+
const newInstance = buttonVM.createInstance();
86+
if (!newInstance) {
87+
console.error('Failed to create new button VM instance');
88+
return;
89+
}
90+
const stringProp = newInstance.stringProperty('string');
91+
if (stringProp) {
92+
stringProp.value = 'new btn';
93+
}
94+
listProperty.addInstance(newInstance);
95+
refreshLength();
96+
}, [listProperty, file, refreshLength]);
97+
98+
const handleRemoveFirst = useCallback(() => {
99+
if (!listProperty || listProperty.length === 0) return;
100+
listProperty.removeInstanceAt(0);
101+
refreshLength();
102+
}, [listProperty, refreshLength]);
103+
104+
const handleRemoveLast = useCallback(() => {
105+
if (!listProperty || listProperty.length === 0) return;
106+
listProperty.removeInstanceAt(listProperty.length - 1);
107+
refreshLength();
108+
}, [listProperty, refreshLength]);
109+
110+
const handleSwapFirstTwo = useCallback(() => {
111+
if (!listProperty || listProperty.length < 2) return;
112+
listProperty.swap(0, 1);
113+
refreshLength();
114+
}, [listProperty, refreshLength]);
115+
116+
const logListItems = useCallback(() => {
117+
if (!listProperty) return;
118+
console.log(`List has ${listProperty.length} items:`);
119+
for (let i = 0; i < listProperty.length; i++) {
120+
const item = listProperty.instanceAt(i);
121+
console.log(` [${i}]: ${item?.instanceName ?? 'undefined'}`);
122+
}
123+
}, [listProperty]);
124+
125+
const handlePlayPause = useCallback(() => {
126+
if (isPlaying) {
127+
riveRef.current?.pause();
128+
} else {
129+
riveRef.current?.play();
130+
}
131+
setIsPlaying(!isPlaying);
132+
}, [isPlaying]);
133+
134+
if (!listProperty) {
135+
return (
136+
<Text style={styles.errorText}>ListItemVM list property not found</Text>
137+
);
138+
}
139+
140+
return (
141+
<View style={styles.container}>
142+
<RiveView
143+
hybridRef={{
144+
f: (ref) => {
145+
riveRef.current = ref;
146+
},
147+
}}
148+
style={styles.rive}
149+
autoPlay={true}
150+
dataBind={instance}
151+
fit={Fit.Contain}
152+
file={file}
153+
/>
154+
<View style={styles.controls}>
155+
<Text style={styles.infoText}>List length: {listLength}</Text>
156+
<View style={styles.buttonRow}>
157+
<TouchableOpacity style={styles.button} onPress={handleAddItem}>
158+
<Text style={styles.buttonText}>Add Item</Text>
159+
</TouchableOpacity>
160+
<TouchableOpacity style={styles.button} onPress={handleRemoveFirst}>
161+
<Text style={styles.buttonText}>Remove First</Text>
162+
</TouchableOpacity>
163+
</View>
164+
<View style={styles.buttonRow}>
165+
<TouchableOpacity style={styles.button} onPress={handleRemoveLast}>
166+
<Text style={styles.buttonText}>Remove Last</Text>
167+
</TouchableOpacity>
168+
<TouchableOpacity style={styles.button} onPress={handleSwapFirstTwo}>
169+
<Text style={styles.buttonText}>Swap 0↔1</Text>
170+
</TouchableOpacity>
171+
</View>
172+
<View style={styles.buttonRow}>
173+
<TouchableOpacity
174+
style={[styles.button, styles.playButton]}
175+
onPress={handlePlayPause}
176+
>
177+
<Text style={styles.buttonText}>
178+
{isPlaying ? 'Pause' : 'Play'}
179+
</Text>
180+
</TouchableOpacity>
181+
<TouchableOpacity
182+
style={[styles.button, styles.logButton]}
183+
onPress={logListItems}
184+
>
185+
<Text style={styles.buttonText}>Log Items</Text>
186+
</TouchableOpacity>
187+
</View>
188+
</View>
189+
</View>
190+
);
191+
}
192+
193+
DataBindingListExample.metadata = {
194+
name: 'Data Binding Lists',
195+
description: 'Test data binding with list properties (ViewModelListProperty)',
196+
} satisfies Metadata;
197+
198+
const styles = StyleSheet.create({
199+
container: {
200+
flex: 1,
201+
backgroundColor: '#fff',
202+
},
203+
riveContainer: {
204+
flex: 1,
205+
backgroundColor: '#f5f5f5',
206+
},
207+
rive: {
208+
flex: 1,
209+
width: '100%',
210+
},
211+
controls: {
212+
padding: 16,
213+
backgroundColor: '#f0f0f0',
214+
},
215+
infoText: {
216+
fontSize: 16,
217+
fontWeight: 'bold',
218+
textAlign: 'center',
219+
marginBottom: 12,
220+
},
221+
buttonRow: {
222+
flexDirection: 'row',
223+
justifyContent: 'space-around',
224+
marginBottom: 8,
225+
},
226+
button: {
227+
backgroundColor: '#007AFF',
228+
paddingVertical: 10,
229+
paddingHorizontal: 16,
230+
borderRadius: 8,
231+
minWidth: 120,
232+
},
233+
playButton: {
234+
backgroundColor: '#34C759',
235+
},
236+
logButton: {
237+
backgroundColor: '#666',
238+
},
239+
buttonText: {
240+
color: '#fff',
241+
fontSize: 14,
242+
fontWeight: 'bold',
243+
textAlign: 'center',
244+
},
245+
errorText: {
246+
color: 'red',
247+
textAlign: 'center',
248+
padding: 20,
249+
},
250+
});

example/src/pages/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ export { default as OutOfBandAssetsWithSuspense } from './OutOfBandAssetsWithSus
99
export { default as ManyViewModels } from './ManyViewModels';
1010
export { default as ResponsiveLayouts } from './ResponsiveLayouts';
1111
export { default as SharedValueListenerExample } from './SharedValueListenerExample';
12+
export { default as DataBindingListExample } from './DataBindingListExample';

ios/HybridViewModelListProperty.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class HybridViewModelListProperty: HybridViewModelListPropertySpec, ValuedProper
1818
}
1919

2020
func instanceAt(index: Double) throws -> (any HybridViewModelInstanceSpec)? {
21-
guard let instance = property.instance(at: Int(index)) else { return nil }
21+
guard let instance = property.instance(at: Int32(index)) else { return nil }
2222
return HybridViewModelInstance(viewModelInstance: instance)
2323
}
2424

@@ -33,25 +33,25 @@ class HybridViewModelListProperty: HybridViewModelListPropertySpec, ValuedProper
3333

3434
func addInstance(instance: any HybridViewModelInstanceSpec) throws {
3535
let viewModelInstance = try requireViewModelInstance(instance)
36-
property.addInstance(viewModelInstance)
36+
property.append(viewModelInstance)
3737
}
3838

3939
func insertInstance(instance: any HybridViewModelInstanceSpec, index: Double) throws {
4040
let viewModelInstance = try requireViewModelInstance(instance)
41-
_ = property.insertInstance(viewModelInstance, at: Int(index))
41+
_ = property.insert(viewModelInstance, at: Int32(index))
4242
}
4343

4444
func removeInstance(instance: any HybridViewModelInstanceSpec) throws {
4545
let viewModelInstance = try requireViewModelInstance(instance)
46-
property.removeInstance(viewModelInstance)
46+
property.remove(viewModelInstance)
4747
}
4848

4949
func removeInstanceAt(index: Double) throws {
50-
property.removeInstance(at: Int(index))
50+
property.remove(at: Int32(index))
5151
}
5252

5353
func swap(index1: Double, index2: Double) throws {
54-
property.swapInstance(at: Int(index1), withInstanceAt: Int(index2))
54+
property.swap(at: UInt32(index1), with: UInt32(index2))
5555
}
5656

5757
func addListener(onChanged: @escaping () -> Void) throws {

0 commit comments

Comments
 (0)