Skip to content

Commit 2a88413

Browse files
committed
feat: add MenuList example adapted from rive-react codesandbox
1 parent e0380f8 commit 2a88413

4 files changed

Lines changed: 327 additions & 0 deletions

File tree

example/assets/lists_demo.rev

339 KB
Binary file not shown.

example/assets/lists_demo.riv

324 KB
Binary file not shown.
Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
import {
2+
View,
3+
Text,
4+
StyleSheet,
5+
ActivityIndicator,
6+
TouchableOpacity,
7+
TextInput,
8+
ScrollView,
9+
} from 'react-native';
10+
import { useMemo, useRef } from 'react';
11+
import {
12+
Fit,
13+
RiveView,
14+
type ViewModelInstance,
15+
type RiveFile,
16+
useRiveFile,
17+
useRiveList,
18+
} from '@rive-app/react-native';
19+
import { type Metadata } from '../helpers/metadata';
20+
21+
export default function MenuListExample() {
22+
const { riveFile, isLoading, error } = useRiveFile(
23+
require('../../assets/lists_demo.riv')
24+
);
25+
26+
return (
27+
<View style={styles.container}>
28+
{isLoading ? (
29+
<ActivityIndicator size="large" color="#007AFF" />
30+
) : riveFile ? (
31+
<WithViewModelSetup file={riveFile} />
32+
) : (
33+
<Text style={styles.errorText}>{error || 'Unexpected error'}</Text>
34+
)}
35+
</View>
36+
);
37+
}
38+
39+
function WithViewModelSetup({ file }: { file: RiveFile }) {
40+
const viewModel = useMemo(() => file.viewModelByName('main'), [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 'main' found"
51+
: 'Failed to create view model instance'}
52+
</Text>
53+
);
54+
}
55+
56+
return <MenuList instance={instance} file={file} />;
57+
}
58+
59+
function MenuList({
60+
instance,
61+
file,
62+
}: {
63+
instance: ViewModelInstance;
64+
file: RiveFile;
65+
}) {
66+
const addLabelRef = useRef<TextInput>(null);
67+
const lastAdded = useRef<ViewModelInstance | null>(null);
68+
const indexToDeleteRef = useRef<TextInput>(null);
69+
const index1Ref = useRef<TextInput>(null);
70+
const index2Ref = useRef<TextInput>(null);
71+
const updateIndexRef = useRef<TextInput>(null);
72+
const updateLabelRef = useRef<TextInput>(null);
73+
74+
const addLabelValue = useRef('New Item');
75+
const indexToDeleteValue = useRef('0');
76+
const index1Value = useRef('0');
77+
const index2Value = useRef('1');
78+
const updateIndexValue = useRef('0');
79+
const updateLabelValue = useRef('Updated Item');
80+
81+
const {
82+
length,
83+
addInstance,
84+
removeInstance,
85+
removeInstanceAt,
86+
getInstanceAt,
87+
swap,
88+
error,
89+
} = useRiveList('menu', instance);
90+
91+
const listItemViewModel = useMemo(
92+
() => file.viewModelByName('listItem'),
93+
[file]
94+
);
95+
96+
const addNewMenuItem = (label: string) => {
97+
if (!listItemViewModel) return;
98+
99+
const newMenuItemVmi = listItemViewModel.createInstance();
100+
if (!newMenuItemVmi) return;
101+
102+
const labelProperty = newMenuItemVmi.stringProperty('label');
103+
const hoverColorProperty = newMenuItemVmi.colorProperty('hoverColor');
104+
const fontIconProperty = newMenuItemVmi.stringProperty('fontIcon');
105+
106+
if (!labelProperty || !hoverColorProperty || !fontIconProperty) return;
107+
108+
labelProperty.value = label;
109+
hoverColorProperty.value = 0xff323232;
110+
fontIconProperty.value = '';
111+
112+
lastAdded.current = newMenuItemVmi;
113+
addInstance(newMenuItemVmi);
114+
};
115+
116+
const removeLastAdded = () => {
117+
if (lastAdded.current) {
118+
removeInstance(lastAdded.current);
119+
lastAdded.current = null;
120+
}
121+
};
122+
123+
const removeByIndex = (index: number) => {
124+
removeInstanceAt(index);
125+
};
126+
127+
const swapIndexes = (index1: number, index2: number) => {
128+
swap(index1, index2);
129+
};
130+
131+
const updateLabelAtIndex = (index: number, label: string) => {
132+
const menuItem = getInstanceAt(index);
133+
if (!menuItem) return;
134+
135+
const menuItemLabel = menuItem.stringProperty('label');
136+
if (!menuItemLabel) return;
137+
138+
menuItemLabel.value = label;
139+
};
140+
141+
if (error) {
142+
return <Text style={styles.errorText}>{error.message}</Text>;
143+
}
144+
145+
return (
146+
<View style={styles.container}>
147+
<RiveView
148+
style={styles.rive}
149+
autoPlay={true}
150+
dataBind={instance}
151+
fit={Fit.FitWidth}
152+
file={file}
153+
/>
154+
155+
<ScrollView style={styles.controls}>
156+
<Text style={styles.listLength}>Menu Items: {length}</Text>
157+
158+
<View style={styles.controlGroup}>
159+
<TextInput
160+
ref={addLabelRef}
161+
style={styles.input}
162+
placeholder="label"
163+
defaultValue="New Item"
164+
onChangeText={(text) => (addLabelValue.current = text)}
165+
/>
166+
<TouchableOpacity
167+
style={styles.button}
168+
onPress={() => addNewMenuItem(addLabelValue.current)}
169+
>
170+
<Text style={styles.buttonText}>Add Menu Item</Text>
171+
</TouchableOpacity>
172+
<TouchableOpacity style={styles.button} onPress={removeLastAdded}>
173+
<Text style={styles.buttonText}>Delete Last Added</Text>
174+
</TouchableOpacity>
175+
</View>
176+
177+
<View style={styles.controlGroup}>
178+
<TextInput
179+
ref={indexToDeleteRef}
180+
style={styles.inputSmall}
181+
keyboardType="numeric"
182+
defaultValue="0"
183+
onChangeText={(text) => (indexToDeleteValue.current = text)}
184+
/>
185+
<TouchableOpacity
186+
style={styles.button}
187+
onPress={() =>
188+
removeByIndex(parseInt(indexToDeleteValue.current, 10))
189+
}
190+
>
191+
<Text style={styles.buttonText}>Remove by Index</Text>
192+
</TouchableOpacity>
193+
</View>
194+
195+
<View style={styles.controlGroup}>
196+
<TextInput
197+
ref={index1Ref}
198+
style={styles.inputSmall}
199+
keyboardType="numeric"
200+
defaultValue="0"
201+
onChangeText={(text) => (index1Value.current = text)}
202+
/>
203+
<TextInput
204+
ref={index2Ref}
205+
style={styles.inputSmall}
206+
keyboardType="numeric"
207+
defaultValue="1"
208+
onChangeText={(text) => (index2Value.current = text)}
209+
/>
210+
<TouchableOpacity
211+
style={styles.button}
212+
onPress={() =>
213+
swapIndexes(
214+
parseInt(index1Value.current, 10),
215+
parseInt(index2Value.current, 10)
216+
)
217+
}
218+
>
219+
<Text style={styles.buttonText}>Swap Indexes</Text>
220+
</TouchableOpacity>
221+
</View>
222+
223+
<View style={styles.controlGroup}>
224+
<TextInput
225+
ref={updateIndexRef}
226+
style={styles.inputSmall}
227+
keyboardType="numeric"
228+
defaultValue="0"
229+
onChangeText={(text) => (updateIndexValue.current = text)}
230+
/>
231+
<TextInput
232+
ref={updateLabelRef}
233+
style={styles.input}
234+
placeholder="label"
235+
defaultValue="Updated Item"
236+
onChangeText={(text) => (updateLabelValue.current = text)}
237+
/>
238+
<TouchableOpacity
239+
style={styles.button}
240+
onPress={() =>
241+
updateLabelAtIndex(
242+
parseInt(updateIndexValue.current, 10),
243+
updateLabelValue.current
244+
)
245+
}
246+
>
247+
<Text style={styles.buttonText}>Update Label</Text>
248+
</TouchableOpacity>
249+
</View>
250+
</ScrollView>
251+
</View>
252+
);
253+
}
254+
255+
MenuListExample.metadata = {
256+
name: 'Menu List',
257+
description: 'Data binding lists demo adapted from rive-react codesandbox',
258+
} satisfies Metadata;
259+
260+
const styles = StyleSheet.create({
261+
container: {
262+
flex: 1,
263+
backgroundColor: '#1a1a2e',
264+
},
265+
rive: {
266+
height: 350,
267+
width: '100%',
268+
},
269+
controls: {
270+
flex: 1,
271+
padding: 16,
272+
backgroundColor: '#16213e',
273+
},
274+
listLength: {
275+
fontSize: 18,
276+
fontWeight: 'bold',
277+
textAlign: 'center',
278+
marginBottom: 16,
279+
color: '#fff',
280+
},
281+
controlGroup: {
282+
flexDirection: 'row',
283+
flexWrap: 'wrap',
284+
alignItems: 'center',
285+
gap: 8,
286+
marginBottom: 12,
287+
paddingBottom: 12,
288+
borderBottomWidth: 1,
289+
borderBottomColor: '#333',
290+
},
291+
input: {
292+
flex: 1,
293+
minWidth: 100,
294+
backgroundColor: '#fff',
295+
paddingVertical: 8,
296+
paddingHorizontal: 12,
297+
borderRadius: 6,
298+
fontSize: 14,
299+
},
300+
inputSmall: {
301+
width: 50,
302+
backgroundColor: '#fff',
303+
paddingVertical: 8,
304+
paddingHorizontal: 12,
305+
borderRadius: 6,
306+
fontSize: 14,
307+
textAlign: 'center',
308+
},
309+
button: {
310+
backgroundColor: '#0f4c75',
311+
paddingVertical: 10,
312+
paddingHorizontal: 14,
313+
borderRadius: 6,
314+
},
315+
buttonText: {
316+
color: '#fff',
317+
fontSize: 13,
318+
fontWeight: 'bold',
319+
},
320+
errorText: {
321+
color: 'red',
322+
textAlign: 'center',
323+
padding: 20,
324+
fontSize: 16,
325+
},
326+
});

example/src/pages/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ export { default as ManyViewModels } from './ManyViewModels';
1010
export { default as ResponsiveLayouts } from './ResponsiveLayouts';
1111
export { default as SharedValueListenerExample } from './SharedValueListenerExample';
1212
export { default as DataBindingListExample } from './DataBindingListExample';
13+
export { default as MenuListExample } from './MenuListExample';

0 commit comments

Comments
 (0)