diff --git a/android/src/main/java/com/margelo/nitro/rive/HybridRiveView.kt b/android/src/main/java/com/margelo/nitro/rive/HybridRiveView.kt index f1abb1ff..a703a3ee 100644 --- a/android/src/main/java/com/margelo/nitro/rive/HybridRiveView.kt +++ b/android/src/main/java/com/margelo/nitro/rive/HybridRiveView.kt @@ -118,6 +118,8 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() { override fun reset() = asyncExecuteOnUiThread { view.reset() } + override fun playIfNeeded() = view.playIfNeeded() + override fun onEventListener(onEvent: (event: UnifiedRiveEvent) -> Unit) = executeOnUiThread { view.addEventListener(onEvent) } diff --git a/android/src/main/java/com/rive/RiveReactNativeView.kt b/android/src/main/java/com/rive/RiveReactNativeView.kt index 5137ee9e..1856ee0d 100644 --- a/android/src/main/java/com/rive/RiveReactNativeView.kt +++ b/android/src/main/java/com/rive/RiveReactNativeView.kt @@ -190,6 +190,14 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { fun reset() = riveAnimationView?.reset() + fun playIfNeeded() { + if (riveAnimationView?.isPlaying == false) { + riveAnimationView?.post { + riveAnimationView?.play() + } + } + } + fun addEventListener(onEvent: (event: RNEvent) -> Unit) { val eventListener = object : RiveFileController.RiveEventListener { override fun notifyEvent(event: RiveEvent) { diff --git a/example/assets/rive/movecircle.riv b/example/assets/rive/movecircle.riv new file mode 100644 index 00000000..9e73a553 Binary files /dev/null and b/example/assets/rive/movecircle.riv differ diff --git a/example/babel.config.js b/example/babel.config.js index 96620cc4..5b4caa62 100644 --- a/example/babel.config.js +++ b/example/babel.config.js @@ -7,7 +7,10 @@ const root = path.resolve(__dirname, '..'); module.exports = getConfig( { presets: ['module:@react-native/babel-preset'], - plugins: [['babel-plugin-react-compiler', {}]], + plugins: [ + ['babel-plugin-react-compiler', {}], + 'react-native-reanimated/plugin', + ], }, { root, pkg } ); diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 24d3ea4f..b2313807 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -8,7 +8,7 @@ PODS: - hermes-engine (0.79.2): - hermes-engine/Pre-built (= 0.79.2) - hermes-engine/Pre-built (0.79.2) - - NitroModules (0.31.4): + - NitroModules (0.31.10): - DoubleConversion - glog - hermes-engine @@ -1803,6 +1803,83 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - RNReanimated (4.1.5): + - 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 + - RNReanimated/reanimated (= 4.1.5) + - RNWorklets + - Yoga + - RNReanimated/reanimated (4.1.5): + - 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 + - RNReanimated/reanimated/apple (= 4.1.5) + - RNWorklets + - Yoga + - RNReanimated/reanimated/apple (4.1.5): + - 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 + - RNWorklets + - Yoga - RNRive (0.1.0): - DoubleConversion - glog @@ -1829,6 +1906,80 @@ PODS: - ReactCommon/turbomodule/core - RiveRuntime (= 6.12.0) - Yoga + - RNWorklets (0.6.1): + - 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 + - RNWorklets/worklets (= 0.6.1) + - Yoga + - RNWorklets/worklets (0.6.1): + - 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 + - RNWorklets/worklets/apple (= 0.6.1) + - Yoga + - RNWorklets/worklets/apple (0.6.1): + - 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 - SocketRocket (0.7.1) - Yoga (0.0.0) @@ -1908,7 +2059,9 @@ DEPENDENCIES: - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) - "RNCPicker (from `../node_modules/@react-native-picker/picker`)" - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) + - RNReanimated (from `../node_modules/react-native-reanimated`) - RNRive (from `../..`) + - RNWorklets (from `../node_modules/react-native-worklets`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: @@ -2064,8 +2217,12 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-picker/picker" RNGestureHandler: :path: "../node_modules/react-native-gesture-handler" + RNReanimated: + :path: "../node_modules/react-native-reanimated" RNRive: :path: "../.." + RNWorklets: + :path: "../node_modules/react-native-worklets" Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" @@ -2077,7 +2234,7 @@ SPEC CHECKSUMS: fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 hermes-engine: 314be5250afa5692b57b4dd1705959e1973a8ebe - NitroModules: 8229091083785ae690dc9071f07fc412e0cb8243 + NitroModules: 7f50ee216f8403e8eb243acfc504f3f856d6914c RCT-Folly: 36fe2295e44b10d831836cc0d1daec5f8abcf809 RCTDeprecation: 83ffb90c23ee5cea353bd32008a7bca100908f8c RCTRequired: eb7c0aba998009f47a540bec9e9d69a54f68136e @@ -2144,7 +2301,9 @@ SPEC CHECKSUMS: RiveRuntime: 8d819993126145fbf5a73089e7634b14b9aa577f RNCPicker: 83c74db2de8274d8a8f3e18d91dea174a708f8c4 RNGestureHandler: bff91bb5ab5688265c70f74180ef718b94f33fe3 + RNReanimated: 9a24892f34ea317264883806d2e3de7ce34eab90 RNRive: 6ced82b0b2ff90cbb87813849e9c05761d038119 + RNWorklets: ddf16938b1ed7e878563a4fc8a690968ef3d27f1 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: 9f110fc4b7aa538663cba3c14cbb1c335f43c13f diff --git a/example/package.json b/example/package.json index 665a1bfd..f73d6cc5 100644 --- a/example/package.json +++ b/example/package.json @@ -15,9 +15,11 @@ "@react-navigation/stack": "^7.3.2", "react": "19.0.0", "react-native": "0.79.2", - "react-native-gesture-handler": "^2.25.0", - "react-native-nitro-modules": "^0.31.3", - "react-native-safe-area-context": "^5.4.0" + "react-native-gesture-handler": "2.29.1", + "react-native-nitro-modules": "0.31.10", + "react-native-reanimated": "4.1.5", + "react-native-safe-area-context": "^5.4.0", + "react-native-worklets": "0.6.1" }, "devDependencies": { "@babel/core": "^7.25.2", diff --git a/example/src/pages/SharedValueListenerExample.tsx b/example/src/pages/SharedValueListenerExample.tsx new file mode 100644 index 00000000..3a3e4f0e --- /dev/null +++ b/example/src/pages/SharedValueListenerExample.tsx @@ -0,0 +1,232 @@ +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); + }, + }} + /> + + + + + + +