Skip to content

Commit 515070e

Browse files
authored
feat: add data binding artboards support (#95)
## Summary - Add support for swapping artboards at runtime via data binding properties - Enables composing complex scenes from artboards in different Rive files ## New APIs - `RiveFile.getBindableArtboard(name)` - create bindable artboard reference - `RiveFile.artboardNames` / `artboardCount` - enumerate artboards - `ViewModelInstance.artboardProperty(path)` - get artboard property - `ViewModelArtboardProperty.set(artboard)` - swap artboard at runtime ## Example ```tsx const { riveFile: mainFile } = useRiveFile(require('./scene.riv')); const { riveFile: assetsFile } = useRiveFile(require('./characters.riv')); const instance = mainFile?.defaultArtboardViewModel()?.createDefaultInstance(); const artboardProp = instance?.artboardProperty('CharacterArtboard'); // Swap to a different character const character = assetsFile?.getBindableArtboard('Character 1'); artboardProp?.set(character); ``` https://github.com/user-attachments/assets/ce44488d-c659-4c5c-8cd8-e40e268c24d6 ## References - [Rive Data Binding Docs](https://rive.app/docs/runtimes/data-binding#artboards) - [rive-react example](https://codesandbox.io/p/sandbox/rive-react-data-binding-artboards-kmvzh8)
1 parent c960517 commit 515070e

58 files changed

Lines changed: 1850 additions & 7 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.editorconfig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ ktlint_standard_import-ordering = disabled
3232
ktlint_standard_string-template-indent = disabled
3333
ktlint_standard_backing-property-naming = disabled
3434
ktlint_standard_no-consecutive-comments = disabled
35+
ktlint_standard_no-empty-first-line-in-class-body = disabled
3536

3637
[nitrogen/generated/**/*.kt]
3738
ktlint = disabled
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.margelo.nitro.rive
2+
3+
import androidx.annotation.Keep
4+
import app.rive.runtime.kotlin.core.BindableArtboard
5+
import com.facebook.proguard.annotations.DoNotStrip
6+
7+
@Keep
8+
@DoNotStrip
9+
class HybridBindableArtboard(internal var bindableArtboard: BindableArtboard?) : HybridBindableArtboardSpec() {
10+
11+
override val artboardName: String
12+
get() = bindableArtboard?.name
13+
?: throw IllegalStateException("BindableArtboard has been disposed")
14+
15+
override fun dispose() {
16+
bindableArtboard?.release()
17+
bindableArtboard = null
18+
}
19+
20+
protected fun finalize() {
21+
dispose()
22+
}
23+
}

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ class HybridRiveFile : HybridRiveFileSpec() {
4848
}
4949
}
5050

51+
override val artboardCount: Double
52+
get() = riveFile?.artboardNames?.size?.toDouble() ?: 0.0
53+
54+
override val artboardNames: Array<String>
55+
get() = riveFile?.artboardNames?.toTypedArray() ?: emptyArray()
56+
57+
override fun getBindableArtboard(name: String): HybridBindableArtboardSpec {
58+
val file = riveFile ?: throw IllegalStateException("RiveFile not loaded")
59+
val bindable = file.createBindableArtboardByName(name)
60+
return HybridBindableArtboard(bindable)
61+
}
62+
5163
fun registerView(view: HybridRiveView) {
5264
weakViews.add(WeakReference(view))
5365
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.margelo.nitro.rive
2+
3+
import androidx.annotation.Keep
4+
import app.rive.runtime.kotlin.core.ViewModelArtboardProperty
5+
import com.facebook.proguard.annotations.DoNotStrip
6+
7+
@Keep
8+
@DoNotStrip
9+
class HybridViewModelArtboardProperty(private val property: ViewModelArtboardProperty) :
10+
HybridViewModelArtboardPropertySpec() {
11+
12+
override fun set(artboard: HybridBindableArtboardSpec?) {
13+
val bindable = (artboard as? HybridBindableArtboard)?.bindableArtboard
14+
property.set(bindable)
15+
}
16+
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,8 @@ class HybridViewModelInstance(val viewModelInstance: ViewModelInstance) : Hybrid
5252
override fun listProperty(path: String) = getPropertyOrNull {
5353
HybridViewModelListProperty(viewModelInstance.getListProperty(path))
5454
}
55+
56+
override fun artboardProperty(path: String) = getPropertyOrNull {
57+
HybridViewModelArtboardProperty(viewModelInstance.getArtboardProperty(path))
58+
}
5559
}
553 KB
Binary file not shown.
2.19 MB
Binary file not shown.

example/ios/Podfile.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1880,7 +1880,7 @@ PODS:
18801880
- ReactCommon/turbomodule/core
18811881
- RNWorklets
18821882
- Yoga
1883-
- RNRive (0.1.1):
1883+
- RNRive (0.1.2):
18841884
- DoubleConversion
18851885
- glog
18861886
- hermes-engine
@@ -2235,7 +2235,7 @@ SPEC CHECKSUMS:
22352235
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
22362236
hermes-engine: 314be5250afa5692b57b4dd1705959e1973a8ebe
22372237
NitroModules: 0af9a8516f3d8f101976d60e1f34e2a22f401600
2238-
RCT-Folly: e78785aa9ba2ed998ea4151e314036f6c49e6d82
2238+
RCT-Folly: 36fe2295e44b10d831836cc0d1daec5f8abcf809
22392239
RCTDeprecation: 83ffb90c23ee5cea353bd32008a7bca100908f8c
22402240
RCTRequired: eb7c0aba998009f47a540bec9e9d69a54f68136e
22412241
RCTTypeSafety: 659ae318c09de0477fd27bbc9e140071c7ea5c93
@@ -2302,10 +2302,10 @@ SPEC CHECKSUMS:
23022302
RNCPicker: 28c076ae12a1056269ec0305fe35fac3086c477d
23032303
RNGestureHandler: 6b39f4e43e4b3a0fb86de9531d090ff205a011d5
23042304
RNReanimated: 66b68ebe3baf7ec9e716bd059d700726f250d344
2305-
RNRive: b7f13eb7d102bb436e8e3d59c9830a4746c86858
2305+
RNRive: b056121a82044307a6f2030a9616f50a6cb6d9ec
23062306
RNWorklets: b1faafefb82d9f29c4018404a0fb33974b494a7b
23072307
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
2308-
Yoga: c758bfb934100bb4bf9cbaccb52557cee35e8bdf
2308+
Yoga: 9f110fc4b7aa538663cba3c14cbb1c335f43c13f
23092309

23102310
PODFILE CHECKSUM: 6974e58448067deb1048e3b4490e929f624eea3c
23112311

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
import {
2+
View,
3+
Text,
4+
StyleSheet,
5+
ActivityIndicator,
6+
Pressable,
7+
} from 'react-native';
8+
import { useState, useMemo, useEffect, useRef } from 'react';
9+
import {
10+
Fit,
11+
RiveView,
12+
useRiveFile,
13+
type RiveFile,
14+
type BindableArtboard,
15+
} from '@rive-app/react-native';
16+
import { type Metadata } from '../helpers/metadata';
17+
18+
/**
19+
* Data Binding Artboards Example
20+
*
21+
* Demonstrates swapping artboards at runtime using data binding.
22+
* Based on: https://rive.app/docs/runtimes/data-binding#artboards
23+
*
24+
* The main Rive file includes a view model with a property of type `Artboard`
25+
* called "CharacterArtboard". This property can be set to any artboard from
26+
* either the main file or an external file.
27+
*
28+
* Rive source files:
29+
* - Main: https://rive.app/marketplace/24641-46042-data-binding-artboards/
30+
* - Assets: https://rive.app/marketplace/24642-47536-data-binding-artboards/
31+
*/
32+
33+
export default function DataBindingArtboardsExample() {
34+
// Main scene file - contains the Card view model with CharacterArtboard property
35+
const {
36+
riveFile: mainFile,
37+
isLoading: mainLoading,
38+
error: mainError,
39+
} = useRiveFile(require('../../assets/swap_character_main.riv'));
40+
41+
// Assets file - contains "Character 1" and "Character 2" artboards
42+
const {
43+
riveFile: assetsFile,
44+
isLoading: assetsLoading,
45+
error: assetsError,
46+
} = useRiveFile(require('../../assets/swap_character_assets.riv'));
47+
48+
const isLoading = mainLoading || assetsLoading;
49+
const error = mainError || assetsError;
50+
51+
if (isLoading) {
52+
return (
53+
<View style={styles.container}>
54+
<ActivityIndicator size="large" color="#0000ff" />
55+
<Text style={styles.loadingText}>Loading Rive files...</Text>
56+
</View>
57+
);
58+
}
59+
60+
if (error || !mainFile || !assetsFile) {
61+
return (
62+
<View style={styles.container}>
63+
<Text style={styles.errorText}>
64+
{error || 'Failed to load Rive files'}
65+
</Text>
66+
</View>
67+
);
68+
}
69+
70+
return <ArtboardSwapper mainFile={mainFile} assetsFile={assetsFile} />;
71+
}
72+
73+
function ArtboardSwapper({
74+
mainFile,
75+
assetsFile,
76+
}: {
77+
mainFile: RiveFile;
78+
assetsFile: RiveFile;
79+
}) {
80+
// Get the view model from the "Main" artboard and create an instance
81+
// IMPORTANT: Must memoize to prevent creating new instance on every render
82+
const viewModel = useMemo(
83+
() => mainFile.defaultArtboardViewModel(),
84+
[mainFile]
85+
);
86+
const instance = useMemo(
87+
() => viewModel?.createDefaultInstance(),
88+
[viewModel]
89+
);
90+
const [currentArtboard, setCurrentArtboard] = useState<string>('Dragon');
91+
const initializedRef = useRef(false);
92+
93+
// Set initial artboard on mount
94+
useEffect(() => {
95+
if (initializedRef.current || !instance) return;
96+
initializedRef.current = true;
97+
98+
const artboardProp = instance.artboardProperty('CharacterArtboard');
99+
if (artboardProp) {
100+
try {
101+
const bindable = assetsFile.getBindableArtboard('Character 1');
102+
artboardProp.set(bindable);
103+
} catch (e) {
104+
console.error(`Failed to set initial artboard: ${e}`);
105+
}
106+
}
107+
}, [instance, assetsFile]);
108+
109+
// Map display names to actual artboard names
110+
const artboardOptions = [
111+
{ label: 'Dragon', artboard: 'Character 1', fromAssets: true },
112+
{ label: 'Gator', artboard: 'Character 2', fromAssets: true },
113+
{ label: 'Placeholder', artboard: 'Placeholder', fromAssets: false },
114+
];
115+
116+
const swapArtboard = (option: (typeof artboardOptions)[number]) => {
117+
if (!instance) return;
118+
119+
const artboardProp = instance.artboardProperty('CharacterArtboard');
120+
if (!artboardProp) {
121+
console.error('Artboard property "CharacterArtboard" not found');
122+
return;
123+
}
124+
125+
try {
126+
const sourceFile = option.fromAssets ? assetsFile : mainFile;
127+
const bindable: BindableArtboard = sourceFile.getBindableArtboard(
128+
option.artboard
129+
);
130+
artboardProp.set(bindable);
131+
setCurrentArtboard(option.label);
132+
} catch (e) {
133+
console.error(`Failed to swap artboard: ${e}`);
134+
}
135+
};
136+
137+
if (!instance || !viewModel) {
138+
return (
139+
<View style={styles.container}>
140+
<Text style={styles.errorText}>
141+
{!viewModel
142+
? 'No view model found in main file'
143+
: 'Failed to create instance'}
144+
</Text>
145+
</View>
146+
);
147+
}
148+
149+
return (
150+
<View style={styles.container}>
151+
<Text style={styles.title}>Data Binding Artboards</Text>
152+
<Text style={styles.subtitle}>
153+
Swap artboards at runtime from different files
154+
</Text>
155+
156+
<View style={styles.riveContainer}>
157+
<RiveView
158+
style={styles.rive}
159+
autoPlay={true}
160+
dataBind={instance}
161+
fit={Fit.Layout}
162+
layoutScaleFactor={1.0}
163+
file={mainFile}
164+
artboardName="Main"
165+
stateMachineName="State Machine 1"
166+
/>
167+
</View>
168+
169+
<View style={styles.infoContainer}>
170+
<Text style={styles.infoText}>Current: {currentArtboard}</Text>
171+
</View>
172+
173+
<View style={styles.buttonContainer}>
174+
{artboardOptions.map((option) => (
175+
<Pressable
176+
key={option.label}
177+
style={[
178+
styles.button,
179+
!option.fromAssets && styles.secondaryButton,
180+
currentArtboard === option.label && styles.buttonActive,
181+
]}
182+
onPress={() => swapArtboard(option)}
183+
>
184+
<Text
185+
style={[
186+
styles.buttonText,
187+
currentArtboard === option.label && styles.buttonTextActive,
188+
]}
189+
>
190+
{option.label}
191+
{option.fromAssets ? ' (external)' : ' (internal)'}
192+
</Text>
193+
</Pressable>
194+
))}
195+
</View>
196+
</View>
197+
);
198+
}
199+
200+
DataBindingArtboardsExample.metadata = {
201+
name: 'Data Binding Artboards',
202+
description: 'Swap artboards at runtime using data binding properties',
203+
} satisfies Metadata;
204+
205+
const styles = StyleSheet.create({
206+
container: {
207+
flex: 1,
208+
backgroundColor: '#fff',
209+
padding: 16,
210+
},
211+
title: {
212+
fontSize: 20,
213+
fontWeight: 'bold',
214+
textAlign: 'center',
215+
marginBottom: 4,
216+
},
217+
subtitle: {
218+
fontSize: 14,
219+
color: '#666',
220+
textAlign: 'center',
221+
marginBottom: 16,
222+
},
223+
riveContainer: {
224+
flex: 1,
225+
backgroundColor: '#f5f5f5',
226+
borderRadius: 8,
227+
overflow: 'hidden',
228+
},
229+
rive: {
230+
flex: 1,
231+
},
232+
infoContainer: {
233+
marginVertical: 12,
234+
padding: 12,
235+
backgroundColor: '#f0f0f0',
236+
borderRadius: 8,
237+
},
238+
infoText: {
239+
fontSize: 14,
240+
color: '#333',
241+
fontWeight: '600',
242+
},
243+
buttonContainer: {
244+
flexDirection: 'row',
245+
flexWrap: 'wrap',
246+
gap: 8,
247+
},
248+
button: {
249+
paddingHorizontal: 16,
250+
paddingVertical: 10,
251+
backgroundColor: '#007AFF',
252+
borderRadius: 8,
253+
},
254+
secondaryButton: {
255+
backgroundColor: '#5856D6',
256+
},
257+
buttonActive: {
258+
backgroundColor: '#34C759',
259+
},
260+
buttonText: {
261+
fontSize: 14,
262+
fontWeight: '600',
263+
color: '#fff',
264+
},
265+
buttonTextActive: {
266+
color: '#fff',
267+
},
268+
loadingText: {
269+
marginTop: 12,
270+
textAlign: 'center',
271+
color: '#666',
272+
},
273+
errorText: {
274+
color: 'red',
275+
textAlign: 'center',
276+
fontSize: 16,
277+
fontWeight: 'bold',
278+
marginBottom: 16,
279+
},
280+
});

example/src/pages/index.ts

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

0 commit comments

Comments
 (0)