diff --git a/.gitignore b/.gitignore index e22a908a..782cb7ef 100644 --- a/.gitignore +++ b/.gitignore @@ -86,3 +86,7 @@ android/generated # ktlint binary cache .ktlint/ + +# Local reproducers (not checked in) +example/src/reproducers/local/* +!example/src/reproducers/local/.gitkeep diff --git a/example/__tests__/rive.harness.ts b/example/__tests__/rive.harness.ts index e6b3f56e..f4281e30 100644 --- a/example/__tests__/rive.harness.ts +++ b/example/__tests__/rive.harness.ts @@ -13,7 +13,7 @@ describe('RiveFile Loading', () => { it('fromURL works', async () => { const file = await RiveFileFactory.fromURL( - 'http://localhost:8081/assets/assets/rive/viewmodelproperty.riv', + 'https://cdn.rive.app/animations/vehicles.riv', undefined ); expect(file).toBeDefined(); diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index e77a58c9..27bbf120 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1755,6 +1755,30 @@ PODS: - React-perflogger (= 0.79.2) - React-utils (= 0.79.2) - RiveRuntime (6.13.0) + - RNCAsyncStorage (2.2.0): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - RNCPicker (2.11.4): - DoubleConversion - glog @@ -1880,7 +1904,7 @@ PODS: - ReactCommon/turbomodule/core - RNWorklets - Yoga - - RNRive (0.1.4): + - RNRive (0.1.5): - DoubleConversion - glog - hermes-engine @@ -2057,6 +2081,7 @@ DEPENDENCIES: - ReactAppDependencyProvider (from `build/generated/ios`) - ReactCodegen (from `build/generated/ios`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) + - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" - "RNCPicker (from `../node_modules/@react-native-picker/picker`)" - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNReanimated (from `../node_modules/react-native-reanimated`) @@ -2213,6 +2238,8 @@ EXTERNAL SOURCES: :path: build/generated/ios ReactCommon: :path: "../node_modules/react-native/ReactCommon" + RNCAsyncStorage: + :path: "../node_modules/@react-native-async-storage/async-storage" RNCPicker: :path: "../node_modules/@react-native-picker/picker" RNGestureHandler: @@ -2234,78 +2261,79 @@ SPEC CHECKSUMS: fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 hermes-engine: 314be5250afa5692b57b4dd1705959e1973a8ebe - NitroModules: 0af9a8516f3d8f101976d60e1f34e2a22f401600 - RCT-Folly: e78785aa9ba2ed998ea4151e314036f6c49e6d82 + NitroModules: 7f50ee216f8403e8eb243acfc504f3f856d6914c + RCT-Folly: 36fe2295e44b10d831836cc0d1daec5f8abcf809 RCTDeprecation: 83ffb90c23ee5cea353bd32008a7bca100908f8c RCTRequired: eb7c0aba998009f47a540bec9e9d69a54f68136e RCTTypeSafety: 659ae318c09de0477fd27bbc9e140071c7ea5c93 React: c2d3aa44c49bb34e4dfd49d3ee92da5ebacc1c1c React-callinvoker: 1bdfb7549b5af266d85757193b5069f60659ef9d - React-Core: 10597593fdbae06f0089881e025a172e51d4a769 - React-CoreModules: 6907b255529dd46895cf687daa67b24484a612c2 - React-cxxreact: a9f5b8180d6955bc3f6a3fcd657c4d9b4d95c1f6 + React-Core: 7150cf9b6a5af063b37003062689f1691e79c020 + React-CoreModules: 15a85e6665d61678942da6ae485b351f4c699049 + React-cxxreact: 74f9de59259ac951923f5726aa14f0398f167af9 React-debug: e74e76912b91e08d580c481c34881899ccf63da9 - React-defaultsnativemodule: 11f6ee2cf69bf3af9d0f28a6253def33d21b5266 - React-domnativemodule: f940bbc4fa9e134190acbf3a4a9f95621b5a8f51 - React-Fabric: 6f5c357bf3a42ff11f8844ad3fc7a1eb04f4b9de - React-FabricComponents: 10e0c0209822ac9e69412913a8af1ca33573379b - React-FabricImage: f582e764072dfa4715ae8c42979a5bace9cbcc12 + React-defaultsnativemodule: 628285212bbd65417d40ad6a9f8781830fda6c98 + React-domnativemodule: 185d9808198405c176784aaf33403d713bd24fb7 + React-Fabric: c814804affbe1952e16149ddd20256e1bccae67e + React-FabricComponents: 81ef47d596966121784afec9924f9562a29b1691 + React-FabricImage: f14f371d678aa557101def954ac3ba27e48948ff React-featureflags: d5facceff8f8f6de430e0acecf4979a9a0839ba9 - React-featureflagsnativemodule: a7dd141f1ef4b7c1331af0035689fbc742a49ff4 - React-graphics: 36ae3407172c1c77cea29265d2b12b90aaef6aa0 - React-hermes: 9116d4e6d07abeb519a2852672de087f44da8f12 - React-idlecallbacksnativemodule: ae7f5ffc6cf2d2058b007b78248e5b08172ad5c3 - React-ImageManager: 9daee0dc99ad6a001d4b9e691fbf37107e2b7b54 - React-jserrorhandler: 1e6211581071edaf4ecd5303147328120c73f4dc - React-jsi: 753ba30c902f3a41fa7f956aca8eea3317a44ee6 - React-jsiexecutor: 47520714aa7d9589c51c0f3713dfbfca4895d4f9 - React-jsinspector: cfd27107f6d6f1076a57d88c932401251560fe5f - React-jsinspectortracing: 76a7d791f3c0c09a0d2bf6f46dfb0e79a4fcc0ac - React-jsitooling: 995e826570dd58f802251490486ebd3244a037ab - React-jsitracing: 094ae3d8c123cea67b50211c945b7c0443d3e97b - React-logger: 8edfcedc100544791cd82692ca5a574240a16219 - React-Mapbuffer: c3f4b608e4a59dd2f6a416ef4d47a14400194468 - React-microtasksnativemodule: 054f34e9b82f02bd40f09cebd4083828b5b2beb6 - react-native-safe-area-context: 0b8555c40461feb7198e999912a3446602e7c601 - React-NativeModulesApple: 2c4377e139522c3d73f5df582e4f051a838ff25e + React-featureflagsnativemodule: 96f0ab285382d95c90f663e02526a5ceefa95a11 + React-graphics: 1a66ee0a3f093b125b853f6370296fadcaf6f233 + React-hermes: 8b86e5f54a65ecb69cdf22b3a00a11562eda82d2 + React-idlecallbacksnativemodule: 5c25ab145c602264d00cb26a397ab52e0efa031c + React-ImageManager: 15e34bd5ef1ac4a18e96660817ef70a7f99ee8c2 + React-jserrorhandler: 02cdf2cd45350108be1ffd2b164578936dbbdff7 + React-jsi: 6af1987cfbb1b6621664fdbf6c7b62bd4d38c923 + React-jsiexecutor: 51f372998e0303585cb0317232b938d694663cbd + React-jsinspector: 3539ad976d073bfaa8a7d2fa9bef35e70e55033e + React-jsinspectortracing: e8dbacaf67c201f23052ca1c2bae2f7b84dec443 + React-jsitooling: 95a34f41e3c249d42181de13b4f8d854f178ca9f + React-jsitracing: 25b029cf5cad488252d46da19dd8c4c134fd5fe4 + React-logger: 368570a253f00879a1e4fea24ed4047e72e7bbf3 + React-Mapbuffer: c04fcda1c6281fc0a6824c7dcc1633dd217ac1ec + React-microtasksnativemodule: ca2804a25fdcefffa0aa942aa23ab53b99614a34 + react-native-safe-area-context: bc59472155ffb889a1ffe16c19a04c0cd451562b + React-NativeModulesApple: 452b86b29fae99ed0a4015dca3ad9cd222f88abf React-oscompat: ef5df1c734f19b8003e149317d041b8ce1f7d29c - React-perflogger: 9a151e0b4c933c9205fd648c246506a83f31395d - React-performancetimeline: 5b0dfc0acba29ea0269ddb34cd6dd59d3b8a1c66 + React-perflogger: 6fd2f6811533e9c19a61e855c3033eecbf4ad2a0 + React-performancetimeline: abf31259d794c9274b3ea19c5016186925eec6c4 React-RCTActionSheet: a499b0d6d9793886b67ba3e16046a3fef2cdbbc3 - React-RCTAnimation: cc64adc259aabc3354b73065e2231d796dfce576 - React-RCTAppDelegate: 9d523da768f1c9e84c5f3b7e3624d097dfb0e16b - React-RCTBlob: e727f53eeefded7e6432eb76bd22b57bc880e5d1 - React-RCTFabric: 58590aa4fdb4ad546c06a7449b486cf6844e991f - React-RCTFBReactNativeSpec: 9064c63d99e467a3893e328ba3612745c3c3a338 - React-RCTImage: 7159cbdbb18a09d97ba1a611416eced75b3ccb29 - React-RCTLinking: 46293afdb859bccc63e1d3dedc6901a3c04ef360 - React-RCTNetwork: 4a6cd18f5bcd0363657789c64043123a896b1170 - React-RCTRuntime: 5ab904fd749aa52f267ef771d265612582a17880 - React-RCTSettings: 61e361dc85136d1cb0e148b7541993d2ee950ea7 - React-RCTText: abd1e196c3167175e6baef18199c6d9d8ac54b4e - React-RCTVibration: 490e0dcb01a3fe4a0dfb7bc51ad5856d8b84f343 + React-RCTAnimation: 2595dcb10a82216a511b54742f8c28d793852ac6 + React-RCTAppDelegate: f03604b70f57c9469a84a159d8abecf793a5bcff + React-RCTBlob: e00f9b4e2f151938f4d9864cf33ebf24ac03328a + React-RCTFabric: 3945d116fd271598db262d4e6ed5691d431ed9e8 + React-RCTFBReactNativeSpec: 0f4d4f0da938101f2ca9d5333a8f46e527ad2819 + React-RCTImage: dac5e9f8ec476aefe6e60ee640ebc1dfaf1a4dbe + React-RCTLinking: 494b785a40d952a1dfbe712f43214376e5f0e408 + React-RCTNetwork: b3d7c30cd21793e268db107dd0980cb61b3c1c44 + React-RCTRuntime: a8ff419d437228e7b8a793b14f9d711e1cbb82af + React-RCTSettings: a060c7e381a3896104761b8eed7e284d95e37df3 + React-RCTText: 4f272b72dbb61f390d8c8274528f9fdbff983806 + React-RCTVibration: 0e5326220719aca12473d703aa46693e3b4ce67a React-rendererconsistency: 351fdbc5c1fe4da24243d939094a80f0e149c7a1 - React-renderercss: 3438814bee838ae7840a633ab085ac81699fd5cf - React-rendererdebug: 0ac2b9419ad6f88444f066d4b476180af311fb1e + React-renderercss: d333f2ada83969591100d91ec6b23ca2e17e1507 + React-rendererdebug: 039e5949b72ba63c703de020701e3fd152434c61 React-rncore: 57ed480649bb678d8bdc386d20fee8bf2b0c307c - React-RuntimeApple: 8b7a9788f31548298ba1990620fe06b40de65ad7 - React-RuntimeCore: e03d96fbd57ce69fd9bca8c925942194a5126dbc + React-RuntimeApple: 344a5e1105256000afabaa8df12c3e4cab880340 + React-RuntimeCore: 0e48fb5e5160acc0334c7a723a42d42cef4b58b6 React-runtimeexecutor: d60846710facedd1edb70c08b738119b3ee2c6c2 - React-RuntimeHermes: aab794755d9f6efd249b61f3af4417296904e3ba - React-runtimescheduler: c3cd124fa5db7c37f601ee49ca0d97019acd8788 + React-RuntimeHermes: 064286a03871d932c99738e0f8ef854962ab4b99 + React-runtimescheduler: e917ab17ae08c204af1ebf8f669b7e411b0220c8 React-timing: a90f4654cbda9c628614f9bee68967f1768bd6a5 - React-utils: a612d50555b6f0f90c74b7d79954019ad47f5de6 - ReactAppDependencyProvider: 04d5eb15eb46be6720e17a4a7fa92940a776e584 - ReactCodegen: c63eda03ba1d94353fb97b031fc84f75a0d125ba - ReactCommon: 76d2dc87136d0a667678668b86f0fca0c16fdeb0 + React-utils: 51c4e71608b8133fecc9a15801d244ae7bdf3758 + ReactAppDependencyProvider: d5dcc564f129632276bd3184e60f053fcd574d6b + ReactCodegen: fda99a79c866370190e162083a35602fdc314e5d + ReactCommon: 4d0da92a5eb8da86c08e3ec34bd23ab439fb2461 RiveRuntime: 903690a5ba698b2a7e8d462e8aa7ceeba862614c - RNCPicker: 28c076ae12a1056269ec0305fe35fac3086c477d - RNGestureHandler: 6b39f4e43e4b3a0fb86de9531d090ff205a011d5 - RNReanimated: 66b68ebe3baf7ec9e716bd059d700726f250d344 - RNRive: 18b5afca5883b3511187e5024bb58849f74e49fa - RNWorklets: b1faafefb82d9f29c4018404a0fb33974b494a7b + RNCAsyncStorage: 2cf7d05f5b1bc38680b6c83971e535a6ae9c5bc7 + RNCPicker: 83c74db2de8274d8a8f3e18d91dea174a708f8c4 + RNGestureHandler: bff91bb5ab5688265c70f74180ef718b94f33fe3 + RNReanimated: 9a24892f34ea317264883806d2e3de7ce34eab90 + RNRive: 73aa1ec7d3ef4da1030e81643808adb538bc05ca + RNWorklets: ddf16938b1ed7e878563a4fc8a690968ef3d27f1 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - Yoga: c758bfb934100bb4bf9cbaccb52557cee35e8bdf + Yoga: 9f110fc4b7aa538663cba3c14cbb1c335f43c13f PODFILE CHECKSUM: 6974e58448067deb1048e3b4490e929f624eea3c diff --git a/example/package.json b/example/package.json index 39cbb3f9..7018208b 100644 --- a/example/package.json +++ b/example/package.json @@ -12,6 +12,7 @@ "test:harness:android": "react-native-harness --harnessRunner android" }, "dependencies": { + "@react-native-async-storage/async-storage": "^2.1.2", "@react-native-picker/picker": "^2.11.4", "@react-navigation/native": "^7.1.9", "@react-navigation/stack": "^7.3.2", diff --git a/example/src/App.tsx b/example/src/App.tsx index 8291510c..0d193a0a 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,13 +1,20 @@ +import { useEffect, useState } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, - ScrollView, + ActionSheetIOS, + Platform, + Alert, } from 'react-native'; -import { NavigationContainer } from '@react-navigation/native'; +import { NavigationContainer, useNavigation } from '@react-navigation/native'; import { createStackNavigator } from '@react-navigation/stack'; -import { PagesList } from './PagesList'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { PagesList, type PageItem } from './PagesList'; +import { HomeMenu } from './shared/HomeMenu'; + +const LAST_OPENED_KEY = '@rive_example_last_opened'; type RootStackParamList = { Home: undefined; @@ -17,22 +24,74 @@ type RootStackParamList = { const Stack = createStackNavigator(); +function invokeGC() { + if (typeof global.gc === 'function') { + global.gc(); + Alert.alert('GC', 'Garbage collection invoked'); + } else { + Alert.alert('GC', 'GC not available (Hermes debugger not attached)'); + } +} + +function HeaderMenuButton() { + const navigation = useNavigation(); + + const openTests = () => { + navigation.navigate('TestsPage'); + }; + + const showDevMenu = () => { + const options = ['Run Tests', 'Invoke GC', 'Cancel']; + const cancelButtonIndex = 2; + + if (Platform.OS === 'ios') { + ActionSheetIOS.showActionSheetWithOptions( + { options, cancelButtonIndex }, + (buttonIndex) => { + if (buttonIndex === 0) { + openTests(); + } else if (buttonIndex === 1) { + invokeGC(); + } + } + ); + } else { + Alert.alert('Dev Menu', undefined, [ + { text: 'Run Tests', onPress: openTests }, + { text: 'Invoke GC', onPress: invokeGC }, + { text: 'Cancel', style: 'cancel' }, + ]); + } + }; + + return ( + + 🔧 + + ); +} + function HomeScreen({ navigation }: { navigation: any }) { + const [lastOpened, setLastOpened] = useState(null); + + useEffect(() => { + AsyncStorage.getItem(LAST_OPENED_KEY).then((id) => { + if (id) { + const page = PagesList.find((p) => p.id === id); + if (page) setLastOpened(page); + } + }); + }, []); + + const handleNavigate = (id: string) => { + AsyncStorage.setItem(LAST_OPENED_KEY, id); + navigation.navigate(id); + }; + return ( - - Rive React Native Examples - - {PagesList.map(({ id, name }) => ( - navigation.navigate(id)} - > - {name} - - ))} - - + + + ); } @@ -53,7 +112,10 @@ export default function App() { {PagesList.map(({ id, component, name }) => ( { + const module = context(key) as { default: PageType }; + const Component = module.default; + const id = key.replace(/^\.\//, '').replace(/\.tsx$/, ''); + + return { + id, + name: Component.metadata?.name ?? id, + description: Component.metadata?.description ?? '', + order: Component.metadata?.order ?? 999, + category, + component: Component, + }; + }) + .sort((a, b) => a.order - b.order); +} + +const demosContext = require.context('./demos', false, /\.tsx$/); +const exercisersContext = require.context('./exercisers', false, /\.tsx$/); +const testsContext = require.context('./tests', false, /\.tsx$/); +const reproducersContext = require.context('./reproducers', false, /\.tsx$/); + +// Try to load local reproducers (gitignored) +let localReproducersContext: __MetroModuleApi.RequireContext | null = null; +try { + localReproducersContext = require.context( + './reproducers/local', + false, + /\.tsx$/ + ); +} catch { + // folder may be empty or not exist +} -export const PagesList: PageItem[] = pagesContext.keys().map((key) => { - const module = pagesContext(key) as { default: PageType }; - const Component = module.default; - const id = key.replace(/^\.\//, '').replace(/\.tsx$/, ''); +export const DemosList = loadPagesFromContext(demosContext, 'demos'); +export const ExercisersList = loadPagesFromContext( + exercisersContext, + 'exercisers' +); +export const ReproducersList = [ + ...loadPagesFromContext(reproducersContext, 'reproducers'), + ...(localReproducersContext + ? loadPagesFromContext(localReproducersContext, 'reproducers') + : []), +]; - return { - id, - name: Component.metadata?.name ?? id, - description: Component.metadata?.description ?? '', - component: Component, - }; -}); +export const TestsList = loadPagesFromContext(testsContext, 'tests'); + +export const PagesList: PageItem[] = [ + ...DemosList, + ...ExercisersList, + ...TestsList, + ...ReproducersList, +]; + +export const PagesListByCategory: Record = { + demos: DemosList, + exercisers: ExercisersList, + tests: TestsList, + reproducers: ReproducersList, +}; diff --git a/example/src/pages/DataBindingArtboardsExample.tsx b/example/src/demos/DataBindingArtboardsExample.tsx similarity index 93% rename from example/src/pages/DataBindingArtboardsExample.tsx rename to example/src/demos/DataBindingArtboardsExample.tsx index 8ed7a177..9af9b70b 100644 --- a/example/src/pages/DataBindingArtboardsExample.tsx +++ b/example/src/demos/DataBindingArtboardsExample.tsx @@ -13,21 +13,21 @@ import { type RiveFile, type BindableArtboard, } from '@rive-app/react-native'; -import { type Metadata } from '../helpers/metadata'; +import { type Metadata } from '../shared/metadata'; /** * Data Binding Artboards Example * * 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: + * Marketplace: * - Main: https://rive.app/marketplace/24641-46042-data-binding-artboards/ * - Assets: https://rive.app/marketplace/24642-47536-data-binding-artboards/ + * + * Docs: https://rive.app/docs/runtimes/data-binding#artboards */ export default function DataBindingArtboardsExample() { @@ -176,17 +176,11 @@ function ArtboardSwapper({ key={option.label} style={[ styles.button, - !option.fromAssets && styles.secondaryButton, currentArtboard === option.label && styles.buttonActive, ]} onPress={() => swapArtboard(option)} > - + {option.label} {option.fromAssets ? ' (external)' : ' (internal)'} @@ -251,9 +245,6 @@ const styles = StyleSheet.create({ backgroundColor: '#007AFF', borderRadius: 8, }, - secondaryButton: { - backgroundColor: '#5856D6', - }, buttonActive: { backgroundColor: '#34C759', }, @@ -262,9 +253,6 @@ const styles = StyleSheet.create({ fontWeight: '600', color: '#fff', }, - buttonTextActive: { - color: '#fff', - }, loadingText: { marginTop: 12, textAlign: 'center', diff --git a/example/src/pages/QuickStart.tsx b/example/src/demos/QuickStart.tsx similarity index 97% rename from example/src/pages/QuickStart.tsx rename to example/src/demos/QuickStart.tsx index 3b37eb22..95c2493e 100644 --- a/example/src/pages/QuickStart.tsx +++ b/example/src/demos/QuickStart.tsx @@ -16,7 +16,7 @@ import { useViewModelInstance, Fit, } from '@rive-app/react-native'; -import type { Metadata } from '../helpers/metadata'; +import type { Metadata } from '../shared/metadata'; export default function QuickStart() { const { riveFile } = useRiveFile( @@ -73,6 +73,7 @@ export default function QuickStart() { QuickStart.metadata = { name: 'Quick Start', description: 'Basic data binding example with health and game over trigger', + order: 0, } satisfies Metadata; const styles = StyleSheet.create({ diff --git a/example/src/pages/ScriptingExample.tsx b/example/src/demos/ScriptingExample.tsx similarity index 94% rename from example/src/pages/ScriptingExample.tsx rename to example/src/demos/ScriptingExample.tsx index c4c29963..508da4a4 100644 --- a/example/src/pages/ScriptingExample.tsx +++ b/example/src/demos/ScriptingExample.tsx @@ -7,7 +7,7 @@ import { View, StyleSheet } from 'react-native'; import { RiveView, useRiveFile, DataBindMode } from '@rive-app/react-native'; -import type { Metadata } from '../helpers/metadata'; +import type { Metadata } from '../shared/metadata'; export default function ScriptingExample() { const { riveFile } = useRiveFile(require('../../assets/rive/blinko.riv')); diff --git a/example/src/pages/ManyViewModels.tsx b/example/src/exercisers/ManyViewModels.tsx similarity index 99% rename from example/src/pages/ManyViewModels.tsx rename to example/src/exercisers/ManyViewModels.tsx index 5d2e32d6..0fd994f5 100644 --- a/example/src/pages/ManyViewModels.tsx +++ b/example/src/exercisers/ManyViewModels.tsx @@ -1,6 +1,6 @@ import { StyleSheet, View, Text, TouchableOpacity, Button } from 'react-native'; import { useState, useMemo, useRef, useEffect } from 'react'; -import type { Metadata } from '../helpers/metadata'; +import type { Metadata } from '../shared/metadata'; import { DataBindMode, RiveView, diff --git a/example/src/pages/MenuListExample.tsx b/example/src/exercisers/MenuListExample.tsx similarity index 99% rename from example/src/pages/MenuListExample.tsx rename to example/src/exercisers/MenuListExample.tsx index 6d5d4436..c6cf5dfc 100644 --- a/example/src/pages/MenuListExample.tsx +++ b/example/src/exercisers/MenuListExample.tsx @@ -17,7 +17,7 @@ import { useRiveList, useViewModelInstance, } from '@rive-app/react-native'; -import { type Metadata } from '../helpers/metadata'; +import { type Metadata } from '../shared/metadata'; export default function MenuListExample() { const { riveFile, isLoading, error } = useRiveFile( diff --git a/example/src/pages/NestedViewModelExample.tsx b/example/src/exercisers/NestedViewModelExample.tsx similarity index 99% rename from example/src/pages/NestedViewModelExample.tsx rename to example/src/exercisers/NestedViewModelExample.tsx index aae1264a..c90ef875 100644 --- a/example/src/pages/NestedViewModelExample.tsx +++ b/example/src/exercisers/NestedViewModelExample.tsx @@ -16,7 +16,7 @@ import { type RiveFile, type RiveViewRef, } from '@rive-app/react-native'; -import { type Metadata } from '../helpers/metadata'; +import { type Metadata } from '../shared/metadata'; export default function NestedViewModelExample() { const { riveFile, isLoading, error } = useRiveFile( diff --git a/example/src/pages/OutOfBandAssets.tsx b/example/src/exercisers/OutOfBandAssets.tsx similarity index 98% rename from example/src/pages/OutOfBandAssets.tsx rename to example/src/exercisers/OutOfBandAssets.tsx index 0fab9ae5..7c3fe7e3 100644 --- a/example/src/pages/OutOfBandAssets.tsx +++ b/example/src/exercisers/OutOfBandAssets.tsx @@ -12,7 +12,7 @@ import { RiveView, } from '@rive-app/react-native'; import { Picker } from '@react-native-picker/picker'; -import { type Metadata } from '../helpers/metadata'; +import { type Metadata } from '../shared/metadata'; export default function OutOfBandAssetsExample() { const [uri, setUri] = React.useState('https://picsum.photos/id/372/500/500'); diff --git a/example/src/pages/OutOfBandAssetsWithSuspense.tsx b/example/src/exercisers/OutOfBandAssetsWithSuspense.tsx similarity index 99% rename from example/src/pages/OutOfBandAssetsWithSuspense.tsx rename to example/src/exercisers/OutOfBandAssetsWithSuspense.tsx index 2edba225..383c470e 100644 --- a/example/src/pages/OutOfBandAssetsWithSuspense.tsx +++ b/example/src/exercisers/OutOfBandAssetsWithSuspense.tsx @@ -15,7 +15,7 @@ import { type RiveImage, } from '@rive-app/react-native'; import { Picker } from '@react-native-picker/picker'; -import { type Metadata } from '../helpers/metadata'; +import { type Metadata } from '../shared/metadata'; class ErrorBoundary extends React.Component< { diff --git a/example/src/pages/ResponsiveLayouts.tsx b/example/src/exercisers/ResponsiveLayouts.tsx similarity index 98% rename from example/src/pages/ResponsiveLayouts.tsx rename to example/src/exercisers/ResponsiveLayouts.tsx index 0cdcde83..92132ad4 100644 --- a/example/src/pages/ResponsiveLayouts.tsx +++ b/example/src/exercisers/ResponsiveLayouts.tsx @@ -13,7 +13,7 @@ import { useRiveFile, type RiveViewRef, } from '@rive-app/react-native'; -import { type Metadata } from '../helpers/metadata'; +import { type Metadata } from '../shared/metadata'; /** * Demonstrates responsive layouts using Fit.Layout and layoutScaleFactor diff --git a/example/src/pages/RiveDataBindingExample.tsx b/example/src/exercisers/RiveDataBindingExample.tsx similarity index 98% rename from example/src/pages/RiveDataBindingExample.tsx rename to example/src/exercisers/RiveDataBindingExample.tsx index 2526ac00..d700eecb 100644 --- a/example/src/pages/RiveDataBindingExample.tsx +++ b/example/src/exercisers/RiveDataBindingExample.tsx @@ -11,7 +11,7 @@ import { useRiveTrigger, useRiveFile, } from '@rive-app/react-native'; -import { type Metadata } from '../helpers/metadata'; +import { type Metadata } from '../shared/metadata'; export default function WithRiveFile() { const { riveFile, isLoading, error } = useRiveFile( diff --git a/example/src/pages/RiveEventsExample.tsx b/example/src/exercisers/RiveEventsExample.tsx similarity index 98% rename from example/src/pages/RiveEventsExample.tsx rename to example/src/exercisers/RiveEventsExample.tsx index dc8fe5ed..413ef290 100644 --- a/example/src/pages/RiveEventsExample.tsx +++ b/example/src/exercisers/RiveEventsExample.tsx @@ -8,7 +8,7 @@ import { type RiveEvent, RiveEventType, } from '@rive-app/react-native'; -import { type Metadata } from '../helpers/metadata'; +import { type Metadata } from '../shared/metadata'; /** * @deprecated Rive events at runtime is deprecated. Use data binding instead. diff --git a/example/src/pages/RiveFileLoadingExample.tsx b/example/src/exercisers/RiveFileLoadingExample.tsx similarity index 97% rename from example/src/pages/RiveFileLoadingExample.tsx rename to example/src/exercisers/RiveFileLoadingExample.tsx index 5c8f91eb..6ea04ab5 100644 --- a/example/src/pages/RiveFileLoadingExample.tsx +++ b/example/src/exercisers/RiveFileLoadingExample.tsx @@ -13,8 +13,8 @@ import { type RiveFileInput, } from '@rive-app/react-native'; import { useState, useEffect } from 'react'; -import { downloadFileAsArrayBuffer } from '../helpers/fileHelpers'; -import { type Metadata } from '../helpers/metadata'; +import { downloadFileAsArrayBuffer } from '../shared/fileHelpers'; +import { type Metadata } from '../shared/metadata'; const LOADING_METHODS = { SOURCE: 'Source', diff --git a/example/src/pages/RiveStateMachineInputsExample.tsx b/example/src/exercisers/RiveStateMachineInputsExample.tsx similarity index 97% rename from example/src/pages/RiveStateMachineInputsExample.tsx rename to example/src/exercisers/RiveStateMachineInputsExample.tsx index a410234e..4a0ae8ee 100644 --- a/example/src/pages/RiveStateMachineInputsExample.tsx +++ b/example/src/exercisers/RiveStateMachineInputsExample.tsx @@ -1,7 +1,7 @@ import { View, Text, StyleSheet, ActivityIndicator } from 'react-native'; import { useEffect } from 'react'; import { Fit, RiveView, useRive, useRiveFile } from '@rive-app/react-native'; -import { type Metadata } from '../helpers/metadata'; +import { type Metadata } from '../shared/metadata'; /** * @deprecated Setting state machine inputs at runtime is deprecated. Use data binding instead. diff --git a/example/src/pages/RiveTextRunExample.tsx b/example/src/exercisers/RiveTextRunExample.tsx similarity index 97% rename from example/src/pages/RiveTextRunExample.tsx rename to example/src/exercisers/RiveTextRunExample.tsx index df514bb4..d0c98f8a 100644 --- a/example/src/pages/RiveTextRunExample.tsx +++ b/example/src/exercisers/RiveTextRunExample.tsx @@ -1,7 +1,7 @@ import { View, Text, StyleSheet, ActivityIndicator } from 'react-native'; import { useEffect } from 'react'; import { Fit, RiveView, useRive, useRiveFile } from '@rive-app/react-native'; -import { type Metadata } from '../helpers/metadata'; +import { type Metadata } from '../shared/metadata'; /** * @deprecated Setting text run values is deprecated. Use data binding instead. diff --git a/example/src/exercisers/ViewRecyclingTest.tsx b/example/src/exercisers/ViewRecyclingTest.tsx new file mode 100644 index 00000000..0852f81d --- /dev/null +++ b/example/src/exercisers/ViewRecyclingTest.tsx @@ -0,0 +1,254 @@ +/** + * View Recycling Bug Reproducer + * + * Tests that RiveView properly resets props when navigating between screens. + * Bug: When navigating OutOfBandAssets → QuickStart → DataBindingArtboards, + * the file from OutOfBandAssets persists while artboardName from + * DataBindingArtboards is applied, causing "Artboard 'Main' not found". + * + * This exerciser simulates that flow with inline navigation. + */ + +import { useState } from 'react'; +import { View, Text, StyleSheet, Pressable } from 'react-native'; +import { RiveView, useRiveFile, Fit } from '@rive-app/react-native'; +import { type Metadata } from '../shared/metadata'; + +type Screen = 'A' | 'B' | 'C'; + +const screens: Record< + Screen, + { title: string; artboardName?: string; file: string } +> = { + A: { + title: 'Screen A (artboard: "Artboard")', + artboardName: 'Artboard', + file: 'out_of_band', + }, + B: { + title: 'Screen B (no artboardName)', + artboardName: undefined, + file: 'quick_start', + }, + C: { + title: 'Screen C (artboard: "Main")', + artboardName: 'Main', + file: 'swap_character_main', + }, +}; + +function ScreenA() { + const { riveFile } = useRiveFile( + require('../../assets/rive/out_of_band.riv') + ); + if (!riveFile) return Loading...; + return ( + + ); +} + +function ScreenB() { + const { riveFile } = useRiveFile( + require('../../assets/rive/quick_start.riv') + ); + if (!riveFile) return Loading...; + return ( + + ); +} + +function ScreenC() { + const { riveFile } = useRiveFile( + require('../../assets/swap_character_main.riv') + ); + if (!riveFile) return Loading...; + return ( + + ); +} + +export default function ViewRecyclingTest() { + const [currentScreen, setCurrentScreen] = useState('A'); + const [error, setError] = useState(null); + + const navigate = (screen: Screen) => { + setError(null); + setCurrentScreen(screen); + }; + + const runSequence = async () => { + setError(null); + // Simulate the problematic navigation sequence + navigate('A'); + await new Promise((r) => setTimeout(r, 500)); + navigate('B'); + await new Promise((r) => setTimeout(r, 500)); + navigate('C'); // This should NOT error with "Artboard 'Main' not found" + }; + + return ( + + View Recycling Test + + Tests that props reset properly when navigating between RiveViews + + + + {(['A', 'B', 'C'] as Screen[]).map((screen) => ( + navigate(screen)} + > + {screen} + + ))} + + + + Run A → B → C Sequence + + + + {screens[currentScreen].title} + + artboardName: {screens[currentScreen].artboardName ?? '(default)'} + + file: {screens[currentScreen].file} + + + {error && ( + + {error} + + )} + + + {currentScreen === 'A' && } + {currentScreen === 'B' && } + {currentScreen === 'C' && } + + + + Bug reproduction: Navigate A → B → C quickly. If view recycling is + broken, Screen C will error with "Artboard 'Main' not found" because the + file from Screen A persists. + + + ); +} + +ViewRecyclingTest.metadata = { + name: 'View Recycling Test', + description: + 'Tests RiveView prop reset during navigation to catch recycling bugs', +} satisfies Metadata; + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 16, + backgroundColor: '#fff', + }, + title: { + fontSize: 20, + fontWeight: 'bold', + textAlign: 'center', + }, + subtitle: { + fontSize: 14, + color: '#666', + textAlign: 'center', + marginBottom: 16, + }, + buttonRow: { + flexDirection: 'row', + justifyContent: 'center', + gap: 8, + marginBottom: 8, + }, + button: { + paddingHorizontal: 24, + paddingVertical: 12, + backgroundColor: '#007AFF', + borderRadius: 8, + }, + buttonActive: { + backgroundColor: '#34C759', + }, + sequenceButton: { + paddingHorizontal: 16, + paddingVertical: 12, + backgroundColor: '#FF9500', + borderRadius: 8, + alignSelf: 'center', + marginBottom: 16, + }, + buttonText: { + color: '#fff', + fontWeight: '600', + fontSize: 16, + }, + info: { + backgroundColor: '#f0f0f0', + padding: 12, + borderRadius: 8, + marginBottom: 16, + }, + infoTitle: { + fontWeight: 'bold', + fontSize: 16, + marginBottom: 4, + }, + infoText: { + fontSize: 14, + color: '#333', + }, + riveContainer: { + flex: 1, + backgroundColor: '#f5f5f5', + borderRadius: 8, + overflow: 'hidden', + marginBottom: 16, + }, + rive: { + flex: 1, + }, + instructions: { + fontSize: 12, + color: '#666', + textAlign: 'center', + fontStyle: 'italic', + }, + errorContainer: { + backgroundColor: '#FFEBEE', + padding: 12, + borderRadius: 8, + marginBottom: 16, + }, + errorText: { + color: '#D32F2F', + fontWeight: '600', + }, +}); diff --git a/example/src/helpers/metadata.ts b/example/src/helpers/metadata.ts deleted file mode 100644 index c10991cc..00000000 --- a/example/src/helpers/metadata.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type Metadata = { - name: string; - description: string; -}; diff --git a/example/src/pages/SharedValueListenerExample.tsx b/example/src/pages/SharedValueListenerExample.tsx deleted file mode 100644 index 3a3e4f0e..00000000 --- a/example/src/pages/SharedValueListenerExample.tsx +++ /dev/null @@ -1,232 +0,0 @@ -import { - View, - Text, - StyleSheet, - Button, - ActivityIndicator, -} from 'react-native'; -import { type Metadata } from '../helpers/metadata'; -import Animated, { - useSharedValue, - useAnimatedReaction, - useAnimatedStyle, - withSpring, -} from 'react-native-reanimated'; -import { - Gesture, - GestureDetector, - GestureHandlerRootView, -} from 'react-native-gesture-handler'; -import { useCallback, useEffect, useMemo } from 'react'; -import { - NitroModules, - type BoxedHybridObject, -} from 'react-native-nitro-modules'; -import { - Fit, - RiveView, - useRiveFile, - type RiveFile, - type RiveViewRef, - type ViewModelInstance, -} from '@rive-app/react-native'; - -export default function SharedValueListenerExample() { - const { riveFile, isLoading, error } = useRiveFile( - require('../../assets/rive/movecircle.riv') - ); - - return ( - - {isLoading ? ( - - ) : riveFile ? ( - - ) : ( - {error || 'Unexpected error'} - )} - - ); -} - -function WithViewModelSetup({ file }: { file: RiveFile }) { - const viewModel = useMemo(() => file.defaultArtboardViewModel(), [file]); - const instance = useMemo( - () => viewModel?.createDefaultInstance(), - [viewModel] - ); - - if (!instance || !viewModel) { - return ( - - {!viewModel - ? 'No view model found' - : 'Failed to create view model instance'} - - ); - } - - return ; -} - -function AnimatedRiveExample({ - instance, - file, -}: { - instance: ViewModelInstance; - file: RiveFile; -}) { - const progress = useSharedValue(0); - const startY = useSharedValue(0); - const viewRef = useSharedValue | null>(null); - - const boxedProperty = useMemo(() => { - const posYProperty = instance.numberProperty('posY'); - if (!posYProperty) { - return null; - } - return NitroModules.box(posYProperty); - }, [instance]); - - useAnimatedReaction( - () => progress.value, - (value: number) => { - 'worklet'; - if (!boxedProperty) return; - const property = boxedProperty.unbox(); - property.value = value; - - viewRef.value?.unbox()?.playIfNeeded(); - }, - [boxedProperty] - ); - - const panGesture = Gesture.Pan() - .onStart(() => { - 'worklet'; - startY.value = progress.value; - }) - .onUpdate((event) => { - 'worklet'; - progress.value = startY.value + event.translationY * 3; - }) - .onEnd((event) => { - 'worklet'; - // Use velocity from gesture to set initial velocity of spring - progress.value = withSpring(progress.value > 400 ? 800 : 0, { - damping: 10, - stiffness: 100, - velocity: event.velocityY * 3, - }); - }); - - const circleStyle = useAnimatedStyle(() => ({ - transform: [{ translateY: progress.value / 3 }], - })); - - const animateTo800 = useCallback(() => { - progress.value = withSpring(800, { - damping: 8, - stiffness: 80, - }); - }, [progress]); - - const animateTo0 = () => { - progress.value = withSpring(0, { - damping: 8, - stiffness: 80, - }); - }; - - useEffect(() => { - animateTo800(); - }, [animateTo800]); - - return ( - - - Drag the blue circle to control position. Release to spring with - velocity. (Red = Rive, Blue = React Native) - - - - { - viewRef.value = NitroModules.box(ref); - }, - }} - /> - - - - - - -