import React, { Component, ComponentType, MutableRefObject, Ref } from 'react'; import { findNodeHandle, Platform, StyleSheet } from 'react-native'; import ReanimatedEventEmitter from './ReanimatedEventEmitter'; // @ts-ignore JS file import AnimatedEvent from './reanimated1/core/AnimatedEvent'; // @ts-ignore JS file import AnimatedNode from './reanimated1/core/AnimatedNode'; // @ts-ignore JS file import AnimatedValue from './reanimated1/core/AnimatedValue'; // @ts-ignore JS file import { createOrReusePropsNode } from './reanimated1/core/AnimatedProps'; import WorkletEventHandler from './reanimated2/WorkletEventHandler'; import setAndForwardRef from './setAndForwardRef'; import './reanimated2/layoutReanimation/LayoutAnimationRepository'; import invariant from 'invariant'; import { adaptViewConfig } from './ConfigHelper'; import { RNRenderer } from './reanimated2/platform-specific/RNRenderer'; import { makeMutable, runOnUI } from './reanimated2/core'; import { DefaultEntering, DefaultExiting, DefaultLayout, } from './reanimated2/layoutReanimation/defaultAnimations/Default'; import { isJest, isChromeDebugger } from './reanimated2/PlatformChecker'; import { initialUpdaterRun } from './reanimated2/animation'; import { BaseAnimationBuilder, EntryExitAnimationFunction, LayoutAnimationFunction, } from './reanimated2/layoutReanimation'; import { SharedValue, StyleProps } from './reanimated2/commonTypes'; import { ViewDescriptorsSet, ViewRefSet, } from './reanimated2/ViewDescriptorsSet'; const NODE_MAPPING = new Map(); interface ListenerData { viewTag: number; props: StyleProps; } function listener(data: ListenerData) { const component = NODE_MAPPING.get(data.viewTag); component && component._updateFromNative(data.props); } function dummyListener() { // empty listener we use to assign to listener properties for which animated // event is used. } function hasAnimatedNodes(value: unknown): boolean { if (value instanceof AnimatedNode) { return true; } if (Array.isArray(value)) { return value.some((item) => hasAnimatedNodes(item)); } if (value && typeof value === 'object') { return Object.keys(value).some((key) => hasAnimatedNodes((value as Record)[key]) ); } return false; } type NestedArray = T | NestedArray[]; function flattenArray(array: NestedArray): T[] { if (!Array.isArray(array)) { return [array]; } const resultArr: T[] = []; const _flattenArray = (arr: NestedArray[]): void => { arr.forEach((item) => { if (Array.isArray(item)) { _flattenArray(item); } else { resultArr.push(item); } }); }; _flattenArray(array); return resultArr; } const has = ( key: K, x: unknown ): x is { [key in K]: unknown } => { if (typeof x === 'function' || typeof x === 'object') { if (x === null || x === undefined) { return false; } else { return key in x; } } return false; }; interface AnimatedProps extends Record { viewDescriptors?: ViewDescriptorsSet; viewsRef?: ViewRefSet; initial?: SharedValue; } export type AnimatedComponentProps

> = P & { forwardedRef?: Ref; style?: NestedArray; animatedProps?: Partial>; animatedStyle?: StyleProps; layout?: | BaseAnimationBuilder | LayoutAnimationFunction | typeof BaseAnimationBuilder; entering?: | BaseAnimationBuilder | typeof BaseAnimationBuilder | EntryExitAnimationFunction | Keyframe; exiting?: | BaseAnimationBuilder | typeof BaseAnimationBuilder | EntryExitAnimationFunction | Keyframe; }; type Options

= { setNativeProps: (ref: ComponentRef, props: P) => void; }; interface ComponentRef extends Component { setNativeProps?: (props: Record) => void; getScrollableNode?: () => ComponentRef; } interface InitialComponentProps extends Record { ref?: Ref; collapsable?: boolean; } interface PropsAnimated { __onEvaluate: () => StyleProps; __detach: () => void; __getValue: () => StyleProps; update: () => void; setNativeView: (view: Component) => void; } export default function createAnimatedComponent( Component: ComponentType, options?: Options ): ComponentType> { invariant( typeof Component !== 'function' || (Component.prototype && Component.prototype.isReactComponent), '`createAnimatedComponent` does not support stateless functional components; ' + 'use a class component instead.' ); class AnimatedComponent extends React.Component< AnimatedComponentProps > { _invokeAnimatedPropsCallbackOnMount = false; _styles: StyleProps[] | null = null; _viewTag = -1; _isFirstRender = true; animatedStyle: { value: StyleProps } = { value: {} }; sv: SharedValue> | null; _propsAnimated?: PropsAnimated; _component: ComponentRef | null = null; static displayName: string; constructor(props: AnimatedComponentProps) { super(props); this._attachProps(this.props); if (isJest()) { this.animatedStyle = { value: {} }; } this.sv = makeMutable({}); } componentWillUnmount() { this._detachPropUpdater(); this._propsAnimated && this._propsAnimated.__detach(); this._detachNativeEvents(); this._detachStyles(); this.sv = null; } componentDidMount() { if (this._invokeAnimatedPropsCallbackOnMount) { this._invokeAnimatedPropsCallbackOnMount = false; this._animatedPropsCallback(); } this._propsAnimated && // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this._propsAnimated.setNativeView(this._component!); this._attachNativeEvents(); this._attachPropUpdater(); this._attachAnimatedStyles(); } _getEventViewRef() { // Make sure to get the scrollable node for components that implement // `ScrollResponder.Mixin`. return this._component?.getScrollableNode ? this._component.getScrollableNode() : this._component; } _attachNativeEvents() { const node = this._getEventViewRef(); const viewTag = findNodeHandle(options?.setNativeProps ? this : node); for (const key in this.props) { const prop = this.props[key]; if (prop instanceof AnimatedEvent) { (prop as AnimatedEvent).attachEvent(node, key); } else if ( has('current', prop) && prop.current instanceof WorkletEventHandler ) { prop.current.registerForEvents(viewTag as number, key); } } } _detachNativeEvents() { const node = this._getEventViewRef(); for (const key in this.props) { const prop = this.props[key]; if (prop instanceof AnimatedEvent) { (prop as AnimatedEvent).detachEvent(node, key); } else if ( has('current', prop) && prop.current instanceof WorkletEventHandler ) { prop.current.unregisterFromEvents(); } } } _detachStyles() { if (Platform.OS === 'web' && this._styles !== null) { for (const style of this._styles) { if (style?.viewsRef) { style.viewsRef.remove(this); } } } else if (this._viewTag !== -1 && this._styles !== null) { for (const style of this._styles) { if (style?.viewDescriptors) { style.viewDescriptors.remove(this._viewTag); } } if (this.props.animatedProps?.viewDescriptors) { this.props.animatedProps.viewDescriptors.remove(this._viewTag); } } } _reattachNativeEvents( prevProps: AnimatedComponentProps ) { const node = this._getEventViewRef(); const attached = new Set(); const nextEvts = new Set(); let viewTag: number | undefined; for (const key in this.props) { const prop = this.props[key]; if (prop instanceof AnimatedEvent) { nextEvts.add((prop as AnimatedEvent).__nodeID); } else if ( has('current', prop) && prop.current instanceof WorkletEventHandler ) { if (viewTag === undefined) { viewTag = prop.current.viewTag; } } } for (const key in prevProps) { const prop = this.props[key]; if (prop instanceof AnimatedEvent) { if (!nextEvts.has((prop as AnimatedEvent).__nodeID)) { // event was in prev props but not in current props, we detach (prop as AnimatedEvent).detachEvent(node, key); } else { // event was in prev and is still in current props attached.add((prop as AnimatedEvent).__nodeID); } } else if ( has('current', prop) && prop.current instanceof WorkletEventHandler && prop.current.reattachNeeded ) { prop.current.unregisterFromEvents(); } } for (const key in this.props) { const prop = this.props[key]; if ( prop instanceof AnimatedEvent && !attached.has((prop as AnimatedEvent).__nodeID) ) { // not yet attached (prop as AnimatedEvent).attachEvent(node, key); } else if ( has('current', prop) && prop.current instanceof WorkletEventHandler && prop.current.reattachNeeded ) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion prop.current.registerForEvents(viewTag!, key); prop.current.reattachNeeded = false; } } } // The system is best designed when setNativeProps is implemented. It is // able to avoid re-rendering and directly set the attributes that changed. // However, setNativeProps can only be implemented on native components // If you want to animate a composite component, you need to re-render it. // In this case, we have a fallback that uses forceUpdate. _animatedPropsCallback = () => { if (this._component == null) { // AnimatedProps is created in will-mount because it's used in render. // But this callback may be invoked before mount in async mode, // In which case we should defer the setNativeProps() call. // React may throw away uncommitted work in async mode, // So a deferred call won't always be invoked. this._invokeAnimatedPropsCallbackOnMount = true; } else if (typeof this._component.setNativeProps !== 'function') { this.forceUpdate(); } else { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this._component.setNativeProps(this._propsAnimated!.__getValue()); } }; _attachProps(nextProps: StyleProps) { const oldPropsAnimated = this._propsAnimated; this._propsAnimated = createOrReusePropsNode( nextProps, this._animatedPropsCallback, oldPropsAnimated ); // If prop node has been reused we don't need to call into "__detach" if (oldPropsAnimated !== this._propsAnimated) { // When you call detach, it removes the element from the parent list // of children. If it goes to 0, then the parent also detaches itself // and so on. // An optimization is to attach the new elements and THEN detach the old // ones instead of detaching and THEN attaching. // This way the intermediate state isn't to go to 0 and trigger // this expensive recursive detaching to then re-attach everything on // the very next operation. oldPropsAnimated && oldPropsAnimated.__detach(); } } _updateFromNative(props: StyleProps) { if (options?.setNativeProps) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion options.setNativeProps(this._component!, props); } else { // eslint-disable-next-line no-unused-expressions this._component?.setNativeProps?.(props); } } _attachPropUpdater() { const viewTag = findNodeHandle(this); NODE_MAPPING.set(viewTag, this); if (NODE_MAPPING.size === 1) { ReanimatedEventEmitter.addListener('onReanimatedPropsChange', listener); } } _attachAnimatedStyles() { const styles = flattenArray(this.props.style ?? []); this._styles = styles; let viewTag: number | null; let viewName: string | null; if (Platform.OS === 'web') { viewTag = findNodeHandle(this); viewName = null; } else { // hostInstance can be null for a component that doesn't render anything (render function returns null). Example: svg Stop: https://github.com/react-native-svg/react-native-svg/blob/develop/src/elements/Stop.tsx const hostInstance = RNRenderer.findHostInstance_DEPRECATED(this); if (!hostInstance) { throw new Error( 'Cannot find host instance for this component. Maybe it renders nothing?' ); } // we can access view tag in the same way it's accessed here https://github.com/facebook/react/blob/e3f4eb7272d4ca0ee49f27577156b57eeb07cf73/packages/react-native-renderer/src/ReactFabric.js#L146 viewTag = hostInstance?._nativeTag; /** * RN uses viewConfig for components for storing different properties of the component(example: https://github.com/facebook/react-native/blob/master/Libraries/Components/ScrollView/ScrollViewViewConfig.js#L16). * The name we're looking for is in the field named uiViewClassName. */ viewName = hostInstance?.viewConfig?.uiViewClassName; // update UI props whitelist for this view if ( hostInstance && this._hasReanimated2Props(styles) && hostInstance.viewConfig ) { adaptViewConfig(hostInstance.viewConfig); } } this._viewTag = viewTag as number; styles.forEach((style) => { if (style?.viewDescriptors) { style.viewDescriptors.add({ tag: viewTag, name: viewName }); if (isJest()) { /** * We need to connect Jest's TestObject instance whose contains just props object * with the updateProps() function where we update the properties of the component. * We can't update props object directly because TestObject contains a copy of props - look at render function: * const props = this._filterNonAnimatedProps(this.props); */ this.animatedStyle.value = { ...this.animatedStyle.value, ...style.initial.value, }; style.animatedStyle.current = this.animatedStyle; } } }); // attach animatedProps property if (this.props.animatedProps?.viewDescriptors) { this.props.animatedProps.viewDescriptors.add({ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion tag: viewTag!, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion name: viewName!, }); } } _hasReanimated2Props(flattenStyles: StyleProps[]) { if (this.props.animatedProps?.viewDescriptors) { return true; } if (this.props.style) { for (const style of flattenStyles) { // eslint-disable-next-line no-prototype-builtins if (style?.hasOwnProperty('viewDescriptors')) { return true; } } } return false; } _detachPropUpdater() { const viewTag = findNodeHandle(this); NODE_MAPPING.delete(viewTag); if (NODE_MAPPING.size === 0) { ReanimatedEventEmitter.removeAllListeners('onReanimatedPropsChange'); } } componentDidUpdate( prevProps: AnimatedComponentProps ) { this._attachProps(this.props); this._reattachNativeEvents(prevProps); this._propsAnimated && // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this._propsAnimated.setNativeView(this._component!); this._attachAnimatedStyles(); } _setComponentRef = setAndForwardRef({ getForwardedRef: () => this.props.forwardedRef as MutableRefObject< Component, Record, unknown> >, setLocalRef: (ref) => { // TODO update config const tag = findNodeHandle(ref); if ( (this.props.layout || this.props.entering || this.props.exiting) && tag != null ) { let layout = this.props.layout ? this.props.layout : DefaultLayout; let entering = this.props.entering ? this.props.entering : DefaultEntering; let exiting = this.props.exiting ? this.props.exiting : DefaultExiting; if (has('build', layout)) { layout = layout.build(); } if (has('build', entering)) { entering = entering.build() as EntryExitAnimationFunction; } if (has('build', exiting)) { exiting = exiting.build() as EntryExitAnimationFunction; } const config = { layout, entering, exiting, sv: this.sv, }; runOnUI(() => { 'worklet'; global.LayoutAnimationRepository.registerConfig(tag, config); })(); } if (ref !== this._component) { this._component = ref; } }, }); _filterNonAnimatedStyle(inputStyle: StyleProps) { const style: StyleProps = {}; for (const key in inputStyle) { const value = inputStyle[key]; if (!hasAnimatedNodes(value)) { style[key] = value; } else if (value instanceof AnimatedValue) { // if any style in animated component is set directly to the `Value` we set those styles to the first value of `Value` node in order // to avoid flash of default styles when `Value` is being asynchrounously sent via bridge and initialized in the native side. style[key] = value._startingValue; } } return style; } _filterNonAnimatedProps( inputProps: AnimatedComponentProps ): Record { const props: Record = {}; for (const key in inputProps) { const value = inputProps[key]; if (key === 'style') { const styleProp = inputProps.style; const styles = flattenArray(styleProp ?? []); const processedStyle: StyleProps = styles.map((style) => { if (style && style.viewDescriptors) { // this is how we recognize styles returned by useAnimatedStyle style.viewsRef.add(this); if (this._isFirstRender) { return { ...style.initial.value, ...initialUpdaterRun(style.initial.updater), }; } else { return style.initial.value; } } else { return style; } }); props[key] = this._filterNonAnimatedStyle( StyleSheet.flatten(processedStyle) ); } else if (key === 'animatedProps') { const animatedProp = inputProps.animatedProps as Partial< AnimatedComponentProps >; if (animatedProp.initial !== undefined) { Object.keys(animatedProp.initial.value).forEach((key) => { props[key] = animatedProp.initial?.value[key]; animatedProp.viewsRef?.add(this); }); } } else if (value instanceof AnimatedEvent) { // we cannot filter out event listeners completely as some components // rely on having a callback registered in order to generate events // alltogether. Therefore we provide a dummy callback here to allow // native event dispatcher to hijack events. props[key] = dummyListener; } else if ( has('current', value) && value.current instanceof WorkletEventHandler ) { if (value.current.eventNames.length > 0) { value.current.eventNames.forEach((eventName) => { props[eventName] = has('listeners', value.current) ? (value.current.listeners as Record)[ eventName ] : dummyListener; }); } else { props[key] = dummyListener; } } else if (!(value instanceof AnimatedNode)) { if (key !== 'onGestureHandlerStateChange' || !isChromeDebugger()) { props[key] = value; } } else if (value instanceof AnimatedValue) { // if any prop in animated component is set directly to the `Value` we set those props to the first value of `Value` node in order // to avoid default values for a short moment when `Value` is being asynchrounously sent via bridge and initialized in the native side. props[key] = (value as AnimatedValue)._startingValue; } } return props; } render() { const props = this._filterNonAnimatedProps(this.props); if (isJest()) { props.animatedStyle = this.animatedStyle; } if (this._isFirstRender) { this._isFirstRender = false; } const platformProps = Platform.select({ web: {}, default: { collapsable: false }, }); return ( ); } } AnimatedComponent.displayName = `AnimatedComponent(${ Component.displayName || Component.name || 'Component' })`; return React.forwardRef((props, ref) => { return ( ); }); }