import React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react'; // This import path isn't handled by @types/react-native // @ts-ignore import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource'; import { requireNativeComponent, UIManager, findNodeHandle, ViewStyle, NativeSyntheticEvent, StyleSheet, View, StyleProp, NativeModules, NativeEventEmitter, EmitterSubscription, Platform, } from 'react-native'; import { RiveRef, Direction, LoopMode, RNRiveError, ViewManagerMethod, RiveGeneralEvent, RiveOpenUrlEvent, RiveRendererInterface, FilesHandledMapping, RiveAssetPropType, RiveRGBA, PropertyType, DataBindBy, AutoBind, } from './types'; import { convertErrorFromNativeToRN, XOR } from './helpers'; import { Alignment, Fit } from './types'; import { getPropertyTypeString, intToRiveRGBA, parseColor, parsePossibleSources, } from './utils'; export type PropertyCallback = (value: any) => void; export class RiveNativeEventEmitter { constructor( public emitter: NativeEventEmitter, public riveRef: React.MutableRefObject ) {} private nativeSubscriptions: { [key: string]: EmitterSubscription; } = {}; private callbacks: { [key: string]: PropertyCallback[]; } = {}; addListener( path: string, propertyType: PropertyType, reactTag: number | null, callback: (value: T) => void ) { // const reactTag = findNodeHandle(this.riveRef.current); if (!reactTag) { console.warn( '[Rive] RiveRef viewTag is null. Cannot register property listener.' ); return; } // "Unique" key for the property listener // The key is a combination of the property type and the path const key = this.generatePropertyKey(path, propertyType, reactTag); // Registering the callback for this key if it doesn't exist // or if the callback is not already registered if (!this.callbacks[key]) { this.callbacks[key] = [callback]; } else if (!this.callbacks[key].includes(callback)) { this.callbacks[key].push(callback); } // Registering a native listener for this key if the listener // is not already registered if (!this.nativeSubscriptions[key]) { let subscription = this.emitter.addListener(key, (value) => { // Call all the callbacks registered for this key this.callbacks[key]?.forEach((storedCallback) => { storedCallback(value); }); }); UIManager.dispatchViewManagerCommand( reactTag, 'registerPropertyListener', // Name of the native command [path, getPropertyTypeString(propertyType)] ); this.nativeSubscriptions[key] = subscription; } } generatePropertyKey( path: string, propertyType: PropertyType, reactTag: number ): string { return `${getPropertyTypeString(propertyType)}:${path}:${reactTag}`; } removeListener( path: string, propertyType: PropertyType, reactTag: number | null, callback: (value: T) => void ) { if (!reactTag) { console.warn( '[Rive] RiveRef viewTag is null. Cannot unregister property listener.' ); return; } const key = this.generatePropertyKey(path, propertyType, reactTag); if (this.callbacks[key]) { // Remove the callback from the list of callbacks this.callbacks[key] = this.callbacks[key].filter( (storedCallback) => storedCallback !== callback ); // If there are no more callbacks for this key, remove the native listener if (this.callbacks[key].length === 0) { this.nativeSubscriptions[key]?.remove(); delete this.nativeSubscriptions[key]; delete this.callbacks[key]; } } } } export function useRive(): [(node: RiveRef) => void, RiveRef | null] { const [ref, setRef] = useState(null); const setRiveRef = useCallback<(node: RiveRef) => void>((node) => { if (!node || !node.internalNativeEmitter) { return; } let viewTag = node.viewTag(); if (viewTag === null) { console.warn('[Rive] RiveRef viewTag is null.'); return; } const nativeEmitter = node.internalNativeEmitter(); if (!nativeEmitter) { console.warn('[Rive] Native event emitter is not initialized.'); return; } // A listener that is called when the native view is loaded and Rive // is ready to be used. const subscription = nativeEmitter.emitter.addListener( `RiveReactNativeLoaded:${viewTag}`, () => { setRef(node); subscription.remove(); // Remove the listener after the event is reported } ); }, []); return [setRiveRef, ref]; } export function useRiveBoolean( riveRef: RiveRef | null, path: string ): [boolean | undefined, (value: boolean) => void] { return useRivePropertyListener(riveRef, path, PropertyType.Boolean); } export function useRiveString( riveRef: RiveRef | null, path: string ): [string | undefined, (value: string) => void] { return useRivePropertyListener(riveRef, path, PropertyType.String); } export function useRiveNumber( riveRef: RiveRef | null, path: string ): [number | undefined, (value: number) => void] { return useRivePropertyListener(riveRef, path, PropertyType.Number); } export function useRiveEnum( riveRef: RiveRef | null, path: string ): [string | undefined, (value: string) => void] { return useRivePropertyListener(riveRef, path, PropertyType.Enum); } export function useRiveColor( riveRef: RiveRef | null, path: string ): [RiveRGBA | undefined, (value: RiveRGBA | string) => void] { return useRivePropertyListener(riveRef, path, PropertyType.Color); } export function useRiveTrigger( riveRef: RiveRef | null, path: string, onTrigger?: () => void ): (() => void) | undefined { const triggerCallback = useCallback(() => { onTrigger?.(); }, [onTrigger]); useEffect(() => { const listener = riveRef?.internalNativeEmitter?.(); if (!listener) return () => {}; const reactTag = findNodeHandle(riveRef.viewTag()); if (!reactTag) return () => {}; listener.addListener( path, PropertyType.Trigger, reactTag, triggerCallback ); return () => { listener.removeListener( path, PropertyType.Trigger, reactTag, triggerCallback ); }; }, [riveRef, path, triggerCallback]); // Function to fire the trigger const trigger = useCallback(() => { if (!riveRef) { if (__DEV__) { console.warn( `[Rive] Tried to trigger "${path}" before riveRef was available.` ); } return; } riveRef.trigger(path); }, [riveRef, path]); return riveRef ? trigger : undefined; } function useRivePropertyListener( riveRef: RiveRef | null, path: string, propertyType: PropertyType ): [T | undefined, (value: T) => void] { const [value, setValue] = useState(undefined); // Listener callback to update state for non-color properties const listenerCallback = useCallback((newValue: T) => { setValue(newValue); }, []); // Listener callback to update state for color properties const listenerCallbackWithColor = useCallback((newValue: number) => { const rgbaValue = intToRiveRGBA(newValue); setValue(rgbaValue as T); }, []); useEffect(() => { const listener = riveRef?.internalNativeEmitter?.(); if (!listener) return () => {}; const reactTag = findNodeHandle(riveRef.viewTag()); if (propertyType === PropertyType.Color) { listener.addListener( path, propertyType, reactTag, listenerCallbackWithColor ); return () => { listener.removeListener( path, propertyType, reactTag, listenerCallbackWithColor ); }; } else { listener.addListener(path, propertyType, reactTag, listenerCallback); return () => { listener.removeListener( path, propertyType, reactTag, listenerCallback ); }; } }, [ riveRef, path, propertyType, listenerCallback, listenerCallbackWithColor, ]); // Setter function const setPropertyValue = useCallback( (newValue: T) => { if (!riveRef) { if (__DEV__) { console.warn( `[Rive] Tried to set property "${path}" before riveRef was available.` ); } return; } switch (propertyType) { case PropertyType.Number: riveRef.setNumber(path, newValue as number); break; case PropertyType.Boolean: riveRef.setBoolean(path, newValue as boolean); break; case PropertyType.String: riveRef.setString(path, newValue as string); break; case PropertyType.Enum: riveRef.setEnum(path, newValue as string); break; case PropertyType.Color: const parsedColor = typeof newValue === 'string' ? parseColor(newValue) : newValue; riveRef.setColor(path, parsedColor as RiveRGBA); break; default: if (__DEV__) { console.warn( `[Rive] Unsupported property type in generic listener: ${propertyType}` ); } } }, [riveRef, path, propertyType] ); return [value, setPropertyValue]; } const { RiveReactNativeRendererModule, RiveReactNativeModule, RiveReactNativeEventModule, } = NativeModules; const nativeEventEmitter = Platform.OS === 'android' ? new NativeEventEmitter() // Not needed for Android : new NativeEventEmitter(RiveReactNativeEventModule); export const RiveRenderer = RiveReactNativeRendererModule as RiveRendererInterface; type RiveProps = { onPlay?: ( event: NativeSyntheticEvent<{ animationName: string; isStateMachine: boolean; }> ) => void; onPause?: ( event: NativeSyntheticEvent<{ animationName: string; isStateMachine: boolean; }> ) => void; onStop?: ( event: NativeSyntheticEvent<{ animationName: string; isStateMachine: boolean; }> ) => void; onLoopEnd?: ( event: NativeSyntheticEvent<{ animationName: string; loopMode: LoopMode; }> ) => void; onStateChanged?: ( event: NativeSyntheticEvent<{ stateMachineName: string; stateName: string; }> ) => void; onRiveEventReceived?: ( event: NativeSyntheticEvent<{ riveEvent: RiveGeneralEvent | RiveOpenUrlEvent; }> ) => void; onError?: ( event: NativeSyntheticEvent<{ type: string; message: string; }> ) => void; isUserHandlingErrors: boolean; autoplay?: boolean; fit: Fit; layoutScaleFactor?: number; alignment: Alignment; artboardName?: string; referencedAssets?: FilesHandledMapping; dataBinding?: DataBindBy; animationName?: string; stateMachineName?: string; ref: any; resourceName?: string; url?: string; style?: StyleProp; testID?: string; }; const VIEW_NAME = 'RiveReactNativeView'; type Props = { onPlay?: (animationName: string, isStateMachine: boolean) => void; onPause?: (animationName: string, isStateMachine: boolean) => void; onStop?: (animationName: string, isStateMachine: boolean) => void; onLoopEnd?: (animationName: string, loopMode: LoopMode) => void; onStateChanged?: (stateMachineName: string, stateName: string) => void; onRiveEventReceived?: (event: RiveGeneralEvent | RiveOpenUrlEvent) => void; onError?: (rnRiveError: RNRiveError) => void; fit?: Fit; layoutScaleFactor?: number; style?: ViewStyle; testID?: string; alignment?: Alignment; artboardName?: string; /** * @experimental This is an experimental feature and may change without a major version update (breaking change). */ referencedAssets?: FilesHandledMapping; dataBinding?: DataBindBy; animationName?: string; stateMachineName?: string; autoplay?: boolean; children?: React.ReactNode; } & XOR< XOR<{ resourceName: string }, { url: string }>, { source: number | { uri: string } } >; export const RiveViewManager = requireNativeComponent(VIEW_NAME); const RiveContainer = React.forwardRef( ( { children, onPlay, onPause, onStop, onLoopEnd, onStateChanged, onRiveEventReceived, onError, style, autoplay = true, resourceName: resourceNameProp, url: urlProp, alignment = Alignment.Center, fit = Fit.Contain, layoutScaleFactor, artboardName, referencedAssets: referencedAssets, dataBinding = AutoBind(false), animationName, source, stateMachineName, testID, }, ref ) => { const assetID = typeof source === 'number' ? source : null; const sourceURI = typeof source === 'object' ? source.uri : null; const { resourceName, url } = useMemo(() => { if (resourceNameProp) { return { resourceName: resourceNameProp }; } if (urlProp) { return { url: urlProp }; } const assetURI = assetID ? resolveAssetSource(assetID)?.uri : sourceURI; if (!assetURI) { return {}; } // handle http address and dev server if (assetURI.match(/^https?:\/\//)) { return { url: assetURI }; } // handle iOS bundled asset if (assetURI.match(/^file:\/\//)) { // strip resource name for assets embedded in the app at build time const strippedName = assetURI.match(/.*\.app\/(.*)\.riv/)?.[1]; if (strippedName) { return { resourceName: strippedName }; } // fallback to url for downloaded assets (e.g. EAS Updates) return { url: assetURI }; } // handle Android bundled asset or resource name uri return { resourceName: assetURI, }; }, [assetID, sourceURI, resourceNameProp, urlProp]); if (!resourceName && !url) { throw new Error( 'Invalid Rive resource. Please provide a valid resource.' ); } const riveRef = useRef(null); const isUserHandlingErrors = onError !== undefined; const onPlayHandler = useCallback( ( event: NativeSyntheticEvent<{ animationName: string; isStateMachine: boolean; }> ) => { const { animationName: eventAnimationName, isStateMachine } = event.nativeEvent; onPlay?.(eventAnimationName, isStateMachine); }, [onPlay] ); const onPauseHandler = useCallback( ( event: NativeSyntheticEvent<{ animationName: string; isStateMachine: boolean; }> ) => { const { animationName: eventAnimationName, isStateMachine } = event.nativeEvent; onPause?.(eventAnimationName, isStateMachine); }, [onPause] ); const onStopHandler = useCallback( ( event: NativeSyntheticEvent<{ animationName: string; isStateMachine: boolean; }> ) => { const { animationName: eventAnimationName, isStateMachine } = event.nativeEvent; onStop?.(eventAnimationName, isStateMachine); }, [onStop] ); const onLoopEndHandler = useCallback( ( event: NativeSyntheticEvent<{ animationName: string; loopMode: LoopMode; }> ) => { const { animationName: eventAnimationName, loopMode } = event.nativeEvent; onLoopEnd?.(eventAnimationName, loopMode); }, [onLoopEnd] ); const onStateChangedHandler = useCallback( ( event: NativeSyntheticEvent<{ stateMachineName: string; stateName: string; }> ) => { const { stateMachineName: eventStateMachineName, stateName } = event.nativeEvent; onStateChanged?.(eventStateMachineName, stateName); }, [onStateChanged] ); const onRiveEventReceivedHandler = useCallback( ( event: NativeSyntheticEvent<{ riveEvent: RiveGeneralEvent | RiveOpenUrlEvent; }> ) => { const { riveEvent } = event.nativeEvent; onRiveEventReceived?.(riveEvent); }, [onRiveEventReceived] ); const onErrorHandler = useCallback( (event: NativeSyntheticEvent<{ type: string; message: string }>) => { const { type, message } = event.nativeEvent; const rnRiveError = convertErrorFromNativeToRN({ type, message }); if (rnRiveError !== null) { onError?.(rnRiveError); } else { console.warn( '[Rive] Unknown error type received from native: ', type ); } }, [onError] ); const play = useCallback( ( animationName = '', loop = LoopMode.Auto, direction = Direction.Auto, isStateMachine = false ) => { UIManager.dispatchViewManagerCommand( findNodeHandle(riveRef.current), ViewManagerMethod.play, [animationName, loop, direction, isStateMachine] ); }, [] ); const pause = useCallback(() => { UIManager.dispatchViewManagerCommand( findNodeHandle(riveRef.current), ViewManagerMethod.pause, [] ); }, []); const stop = useCallback(() => { UIManager.dispatchViewManagerCommand( findNodeHandle(riveRef.current), ViewManagerMethod.stop, [] ); }, []); const reset = useCallback(() => { UIManager.dispatchViewManagerCommand( findNodeHandle(riveRef.current), ViewManagerMethod.reset, [] ); }, []); const fireState = useCallback( (triggerStateMachineName, inputName) => { UIManager.dispatchViewManagerCommand( findNodeHandle(riveRef.current), ViewManagerMethod.fireState, [triggerStateMachineName, inputName] ); }, [] ); const setInputState = useCallback( (triggerStateMachineName, inputName, value) => { if (typeof value === 'boolean') { UIManager.dispatchViewManagerCommand( findNodeHandle(riveRef.current), ViewManagerMethod.setBooleanState, [triggerStateMachineName, inputName, value] ); } else if (typeof value === 'number') { UIManager.dispatchViewManagerCommand( findNodeHandle(riveRef.current), ViewManagerMethod.setNumberState, [triggerStateMachineName, inputName, value] ); } }, [] ); const getBooleanState = useCallback( async (inputName): Promise => { try { const result = await RiveReactNativeModule.getBooleanState( findNodeHandle(riveRef.current), inputName ); return result; } catch (error) { console.error( `Error getting boolean state for input: ${inputName}`, error ); return null; } }, [] ); const getNumberState = useCallback( async (inputName): Promise => { try { const result = await RiveReactNativeModule.getNumberState( findNodeHandle(riveRef.current), inputName ); return result; } catch (error) { console.error( `Error getting number state for input: ${inputName}`, error ); return null; } }, [] ); const getBooleanStateAtPath = useCallback( async (inputName, path): Promise => { try { const result = await RiveReactNativeModule.getBooleanStateAtPath( findNodeHandle(riveRef.current), inputName, path ); return result; } catch (error) { console.error( `Error getting boolean state for input: ${inputName} at path: ${path}`, error ); return null; } }, [] ); const getNumberStateAtPath = useCallback( async (inputName, path): Promise => { try { const result = await RiveReactNativeModule.getNumberStateAtPath( findNodeHandle(riveRef.current), inputName, path ); return result; } catch (error) { console.error( `Error getting number state for input: ${inputName} at path: ${path}`, error ); return null; } }, [] ); const fireStateAtPath = useCallback< RiveRef[ViewManagerMethod.fireStateAtPath] >((inputName, path) => { UIManager.dispatchViewManagerCommand( findNodeHandle(riveRef.current), ViewManagerMethod.fireStateAtPath, [inputName, path] ); }, []); const setInputStateAtPath = useCallback( (inputName, value, path) => { if (typeof value === 'boolean') { UIManager.dispatchViewManagerCommand( findNodeHandle(riveRef.current), ViewManagerMethod.setBooleanStateAtPath, [inputName, value, path] ); } else if (typeof value === 'number') { UIManager.dispatchViewManagerCommand( findNodeHandle(riveRef.current), ViewManagerMethod.setNumberStateAtPath, [inputName, value, path] ); } }, [] ); const touchBegan = useCallback( (x: number, y: number) => { if (!isNaN(x) && !isNaN(y)) { UIManager.dispatchViewManagerCommand( findNodeHandle(riveRef.current), ViewManagerMethod.touchBegan, [x, y] ); } }, [] ); const touchEnded = useCallback( (x: number, y: number) => { if (!isNaN(x) && !isNaN(y)) { UIManager.dispatchViewManagerCommand( findNodeHandle(riveRef.current), ViewManagerMethod.touchEnded, [x, y] ); } }, [] ); const setTextRunValue = useCallback< RiveRef[ViewManagerMethod.setTextRunValue] >((textRunName: string, textValue: string) => { if (textRunName) { UIManager.dispatchViewManagerCommand( findNodeHandle(riveRef.current), ViewManagerMethod.setTextRunValue, [textRunName, textValue] ); } }, []); const setTextRunValueAtPath = useCallback< RiveRef[ViewManagerMethod.setTextRunValueAtPath] >((textRunName: string, textValue: string, path: string) => { if (textRunName) { UIManager.dispatchViewManagerCommand( findNodeHandle(riveRef.current), ViewManagerMethod.setTextRunValueAtPath, [textRunName, textValue, path] ); } }, []); const setBoolean = useCallback( (path: string, value: boolean) => { UIManager.dispatchViewManagerCommand( findNodeHandle(riveRef.current), ViewManagerMethod.setBooleanPropertyValue, [path, value] ); }, [] ); const setString = useCallback( (path: string, value: String) => { UIManager.dispatchViewManagerCommand( findNodeHandle(riveRef.current), ViewManagerMethod.setStringPropertyValue, [path, value] ); }, [] ); const setNumber = useCallback( (path: string, value: number) => { UIManager.dispatchViewManagerCommand( findNodeHandle(riveRef.current), ViewManagerMethod.setNumberPropertyValue, [path, value] ); }, [] ); const setColor = useCallback( (path: string, color: RiveRGBA | string) => { let parsedColor = typeof color === 'string' ? parseColor(color) : color; UIManager.dispatchViewManagerCommand( findNodeHandle(riveRef.current), ViewManagerMethod.setColorPropertyValue, [path, parsedColor.r, parsedColor.g, parsedColor.b, parsedColor.a] ); }, [] ); const setEnum = useCallback( (path: string, value: string) => { UIManager.dispatchViewManagerCommand( findNodeHandle(riveRef.current), ViewManagerMethod.setEnumPropertyValue, [path, value] ); }, [] ); const trigger = useCallback((path: string) => { UIManager.dispatchViewManagerCommand( findNodeHandle(riveRef.current), ViewManagerMethod.fireTriggerProperty, [path] ); }, []); const internalNativeEmitter = useCallback< RiveRef['internalNativeEmitter'] >(() => { if (!riveRef.current._propertyEmitter) { riveRef.current._propertyEmitter = new RiveNativeEventEmitter( nativeEventEmitter, riveRef ); } return riveRef.current._propertyEmitter; }, [riveRef]); const viewTag = useCallback(() => { return findNodeHandle(riveRef.current); }, [riveRef]); useImperativeHandle( ref, () => ({ setInputState, getBooleanState, getBooleanStateAtPath, getNumberState, getNumberStateAtPath, setInputStateAtPath, fireState, fireStateAtPath, play, pause, stop, reset, touchBegan, touchEnded, setTextRunValue, setTextRunValueAtPath, setBoolean, setString, setNumber, setColor, setEnum, trigger, internalNativeEmitter, viewTag, }), [ play, pause, stop, reset, setInputState, getBooleanState, getBooleanStateAtPath, getNumberState, getNumberStateAtPath, setInputStateAtPath, fireState, fireStateAtPath, touchBegan, touchEnded, setTextRunValue, setTextRunValueAtPath, setBoolean, setString, setNumber, setColor, setEnum, trigger, internalNativeEmitter, viewTag, ] ); function transformFilesHandledMapping( mapping?: FilesHandledMapping ): FilesHandledMapping | undefined { const transformedMapping: FilesHandledMapping = {}; if (mapping === undefined) { return undefined; } Object.keys(mapping).forEach((key) => { const option = mapping[key]; transformedMapping[key] = { ...option, source: parsePossibleSources(option.source as RiveAssetPropType), }; }); return transformedMapping; } const convertedAssetHandledSources = transformFilesHandledMapping(referencedAssets); return ( {children} ); } ); const styles = StyleSheet.create({ children: { position: 'absolute', width: '100%', height: '100%', }, container: { flexGrow: 1, }, animation: { flex: 1, }, }); export default RiveContainer;