Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ ktlint_standard_import-ordering = disabled
ktlint_standard_string-template-indent = disabled
ktlint_standard_backing-property-naming = disabled
ktlint_standard_no-consecutive-comments = disabled
ktlint_standard_no-empty-first-line-in-class-body = disabled

[nitrogen/generated/**/*.kt]
ktlint = disabled
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.margelo.nitro.rive

import androidx.annotation.Keep
import app.rive.runtime.kotlin.core.BindableArtboard
import com.facebook.proguard.annotations.DoNotStrip

@Keep
@DoNotStrip
class HybridBindableArtboard(internal var bindableArtboard: BindableArtboard?) : HybridBindableArtboardSpec() {

override val artboardName: String
get() = bindableArtboard?.name
?: throw IllegalStateException("BindableArtboard has been disposed")

override fun dispose() {
bindableArtboard?.release()
bindableArtboard = null
}

protected fun finalize() {
dispose()
}
}
12 changes: 12 additions & 0 deletions android/src/main/java/com/margelo/nitro/rive/HybridRiveFile.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,18 @@ class HybridRiveFile : HybridRiveFileSpec() {
}
}

override val artboardCount: Double
get() = riveFile?.artboardNames?.size?.toDouble() ?: 0.0

override val artboardNames: Array<String>
get() = riveFile?.artboardNames?.toTypedArray() ?: emptyArray()

override fun getBindableArtboard(name: String): HybridBindableArtboardSpec {
val file = riveFile ?: throw IllegalStateException("RiveFile not loaded")
val bindable = file.createBindableArtboardByName(name)
return HybridBindableArtboard(bindable)
}

fun registerView(view: HybridRiveView) {
weakViews.add(WeakReference(view))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.margelo.nitro.rive

import androidx.annotation.Keep
import app.rive.runtime.kotlin.core.ViewModelArtboardProperty
import com.facebook.proguard.annotations.DoNotStrip

@Keep
@DoNotStrip
class HybridViewModelArtboardProperty(private val property: ViewModelArtboardProperty) :
HybridViewModelArtboardPropertySpec() {

override fun set(artboard: HybridBindableArtboardSpec?) {
val bindable = (artboard as? HybridBindableArtboard)?.bindableArtboard
property.set(bindable)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,8 @@ class HybridViewModelInstance(val viewModelInstance: ViewModelInstance) : Hybrid
override fun listProperty(path: String) = getPropertyOrNull {
HybridViewModelListProperty(viewModelInstance.getListProperty(path))
}

override fun artboardProperty(path: String) = getPropertyOrNull {
HybridViewModelArtboardProperty(viewModelInstance.getArtboardProperty(path))
}
}
Binary file added example/assets/swap_character_assets.riv
Binary file not shown.
Binary file added example/assets/swap_character_main.riv
Binary file not shown.
8 changes: 4 additions & 4 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1880,7 +1880,7 @@ PODS:
- ReactCommon/turbomodule/core
- RNWorklets
- Yoga
- RNRive (0.1.1):
- RNRive (0.1.2):
- DoubleConversion
- glog
- hermes-engine
Expand Down Expand Up @@ -2235,7 +2235,7 @@ SPEC CHECKSUMS:
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
hermes-engine: 314be5250afa5692b57b4dd1705959e1973a8ebe
NitroModules: 0af9a8516f3d8f101976d60e1f34e2a22f401600
RCT-Folly: e78785aa9ba2ed998ea4151e314036f6c49e6d82
RCT-Folly: 36fe2295e44b10d831836cc0d1daec5f8abcf809
RCTDeprecation: 83ffb90c23ee5cea353bd32008a7bca100908f8c
RCTRequired: eb7c0aba998009f47a540bec9e9d69a54f68136e
RCTTypeSafety: 659ae318c09de0477fd27bbc9e140071c7ea5c93
Expand Down Expand Up @@ -2302,10 +2302,10 @@ SPEC CHECKSUMS:
RNCPicker: 28c076ae12a1056269ec0305fe35fac3086c477d
RNGestureHandler: 6b39f4e43e4b3a0fb86de9531d090ff205a011d5
RNReanimated: 66b68ebe3baf7ec9e716bd059d700726f250d344
RNRive: b7f13eb7d102bb436e8e3d59c9830a4746c86858
RNRive: b056121a82044307a6f2030a9616f50a6cb6d9ec
RNWorklets: b1faafefb82d9f29c4018404a0fb33974b494a7b
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Yoga: c758bfb934100bb4bf9cbaccb52557cee35e8bdf
Yoga: 9f110fc4b7aa538663cba3c14cbb1c335f43c13f

PODFILE CHECKSUM: 6974e58448067deb1048e3b4490e929f624eea3c

Expand Down
280 changes: 280 additions & 0 deletions example/src/pages/DataBindingArtboardsExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
import {
View,
Text,
StyleSheet,
ActivityIndicator,
Pressable,
} from 'react-native';
import { useState, useMemo, useEffect, useRef } from 'react';
import {
Fit,
RiveView,
useRiveFile,
type RiveFile,
type BindableArtboard,
} from '@rive-app/react-native';
import { type Metadata } from '../helpers/metadata';

/**
* Data Binding Artboards Example
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really good explanation. We should also update the other examples to link to the marketplace!

*
* Demonstrates swapping artboards at runtime using data binding.
* Based on: https://rive.app/docs/runtimes/data-binding#artboards
*
* The main Rive file includes a view model with a property of type `Artboard`
* called "CharacterArtboard". This property can be set to any artboard from
* either the main file or an external file.
*
* Rive source files:
* - Main: https://rive.app/marketplace/24641-46042-data-binding-artboards/
* - Assets: https://rive.app/marketplace/24642-47536-data-binding-artboards/
*/

export default function DataBindingArtboardsExample() {
// Main scene file - contains the Card view model with CharacterArtboard property
const {
riveFile: mainFile,
isLoading: mainLoading,
error: mainError,
} = useRiveFile(require('../../assets/swap_character_main.riv'));

// Assets file - contains "Character 1" and "Character 2" artboards
const {
riveFile: assetsFile,
isLoading: assetsLoading,
error: assetsError,
} = useRiveFile(require('../../assets/swap_character_assets.riv'));

const isLoading = mainLoading || assetsLoading;
const error = mainError || assetsError;

if (isLoading) {
return (
<View style={styles.container}>
<ActivityIndicator size="large" color="#0000ff" />
<Text style={styles.loadingText}>Loading Rive files...</Text>
</View>
);
}

if (error || !mainFile || !assetsFile) {
return (
<View style={styles.container}>
<Text style={styles.errorText}>
{error || 'Failed to load Rive files'}
</Text>
</View>
);
}

return <ArtboardSwapper mainFile={mainFile} assetsFile={assetsFile} />;
}

function ArtboardSwapper({
mainFile,
assetsFile,
}: {
mainFile: RiveFile;
assetsFile: RiveFile;
}) {
// Get the view model from the "Main" artboard and create an instance
// IMPORTANT: Must memoize to prevent creating new instance on every render
const viewModel = useMemo(
() => mainFile.defaultArtboardViewModel(),
[mainFile]
);
const instance = useMemo(
() => viewModel?.createDefaultInstance(),
[viewModel]
);
const [currentArtboard, setCurrentArtboard] = useState<string>('Dragon');
const initializedRef = useRef(false);

// Set initial artboard on mount
useEffect(() => {
if (initializedRef.current || !instance) return;
initializedRef.current = true;

const artboardProp = instance.artboardProperty('CharacterArtboard');
if (artboardProp) {
try {
const bindable = assetsFile.getBindableArtboard('Character 1');
artboardProp.set(bindable);
} catch (e) {
console.error(`Failed to set initial artboard: ${e}`);
}
}
}, [instance, assetsFile]);

// Map display names to actual artboard names
const artboardOptions = [
{ label: 'Dragon', artboard: 'Character 1', fromAssets: true },
{ label: 'Gator', artboard: 'Character 2', fromAssets: true },
{ label: 'Placeholder', artboard: 'Placeholder', fromAssets: false },
];

const swapArtboard = (option: (typeof artboardOptions)[number]) => {
if (!instance) return;

const artboardProp = instance.artboardProperty('CharacterArtboard');
if (!artboardProp) {
console.error('Artboard property "CharacterArtboard" not found');
return;
}

try {
const sourceFile = option.fromAssets ? assetsFile : mainFile;
const bindable: BindableArtboard = sourceFile.getBindableArtboard(
option.artboard
);
artboardProp.set(bindable);
setCurrentArtboard(option.label);
} catch (e) {
console.error(`Failed to swap artboard: ${e}`);
}
};

if (!instance || !viewModel) {
return (
<View style={styles.container}>
<Text style={styles.errorText}>
{!viewModel
? 'No view model found in main file'
: 'Failed to create instance'}
</Text>
</View>
);
}

return (
<View style={styles.container}>
<Text style={styles.title}>Data Binding Artboards</Text>
<Text style={styles.subtitle}>
Swap artboards at runtime from different files
</Text>

<View style={styles.riveContainer}>
<RiveView
style={styles.rive}
autoPlay={true}
dataBind={instance}
fit={Fit.Layout}
layoutScaleFactor={1.0}
file={mainFile}
artboardName="Main"
stateMachineName="State Machine 1"
/>
</View>

<View style={styles.infoContainer}>
<Text style={styles.infoText}>Current: {currentArtboard}</Text>
</View>

<View style={styles.buttonContainer}>
{artboardOptions.map((option) => (
<Pressable
key={option.label}
style={[
styles.button,
!option.fromAssets && styles.secondaryButton,
currentArtboard === option.label && styles.buttonActive,
]}
onPress={() => swapArtboard(option)}
>
<Text
style={[
styles.buttonText,
currentArtboard === option.label && styles.buttonTextActive,
]}
>
{option.label}
{option.fromAssets ? ' (external)' : ' (internal)'}
</Text>
</Pressable>
))}
</View>
</View>
);
}

DataBindingArtboardsExample.metadata = {
name: 'Data Binding Artboards',
description: 'Swap artboards at runtime using data binding properties',
} satisfies Metadata;

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
padding: 16,
},
title: {
fontSize: 20,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 4,
},
subtitle: {
fontSize: 14,
color: '#666',
textAlign: 'center',
marginBottom: 16,
},
riveContainer: {
flex: 1,
backgroundColor: '#f5f5f5',
borderRadius: 8,
overflow: 'hidden',
},
rive: {
flex: 1,
},
infoContainer: {
marginVertical: 12,
padding: 12,
backgroundColor: '#f0f0f0',
borderRadius: 8,
},
infoText: {
fontSize: 14,
color: '#333',
fontWeight: '600',
},
buttonContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
},
button: {
paddingHorizontal: 16,
paddingVertical: 10,
backgroundColor: '#007AFF',
borderRadius: 8,
},
secondaryButton: {
backgroundColor: '#5856D6',
},
buttonActive: {
backgroundColor: '#34C759',
},
buttonText: {
fontSize: 14,
fontWeight: '600',
color: '#fff',
},
buttonTextActive: {
color: '#fff',
},
loadingText: {
marginTop: 12,
textAlign: 'center',
color: '#666',
},
errorText: {
color: 'red',
textAlign: 'center',
fontSize: 16,
fontWeight: 'bold',
marginBottom: 16,
},
});
1 change: 1 addition & 0 deletions example/src/pages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export { default as ManyViewModels } from './ManyViewModels';
export { default as ResponsiveLayouts } from './ResponsiveLayouts';
export { default as SharedValueListenerExample } from './SharedValueListenerExample';
export { default as MenuListExample } from './MenuListExample';
export { default as DataBindingArtboardsExample } from './DataBindingArtboardsExample';
Loading
Loading