diff --git a/packages/react-native-reanimated/src/createAnimatedComponent/NativeEventsManager.ts b/packages/react-native-reanimated/src/createAnimatedComponent/NativeEventsManager.ts new file mode 100644 index 00000000000..35ce8a68a99 --- /dev/null +++ b/packages/react-native-reanimated/src/createAnimatedComponent/NativeEventsManager.ts @@ -0,0 +1,138 @@ +'use strict'; +import type { + INativeEventsManager, + IAnimatedComponentInternal, + AnimatedComponentProps, + InitialComponentProps, + AnimatedComponentRef, +} from './commonTypes'; +import { has } from './utils'; +import { WorkletEventHandler } from '../WorkletEventHandler'; +import { findNodeHandle } from 'react-native'; + +export class NativeEventsManager implements INativeEventsManager { + readonly #managedComponent: ManagedAnimatedComponent; + readonly #componentOptions?: ComponentOptions; + #eventViewTag = -1; + + constructor(component: ManagedAnimatedComponent, options?: ComponentOptions) { + this.#managedComponent = component; + this.#componentOptions = options; + this.#eventViewTag = this.getEventViewTag(); + } + + public attachEvents() { + executeForEachEventHandler(this.#managedComponent.props, (key, handler) => { + handler.registerForEvents(this.#eventViewTag, key); + }); + } + + public detachEvents() { + executeForEachEventHandler( + this.#managedComponent.props, + (_key, handler) => { + handler.unregisterFromEvents(this.#eventViewTag); + } + ); + } + + public updateEvents( + prevProps: AnimatedComponentProps + ) { + const computedEventTag = this.getEventViewTag(); + // If the event view tag changes, we need to completely re-mount all events + if (this.#eventViewTag !== computedEventTag) { + // Remove all bindings from previous props that ran on the old viewTag + executeForEachEventHandler(prevProps, (_key, handler) => { + handler.unregisterFromEvents(this.#eventViewTag); + }); + // We don't need to unregister from current (new) props, because their events weren't registered yet + // Replace the view tag + this.#eventViewTag = computedEventTag; + // Attach the events with a new viewTag + this.attachEvents(); + return; + } + + executeForEachEventHandler(prevProps, (key, prevHandler) => { + const newProp = this.#managedComponent.props[key]; + if (!newProp) { + // Prop got deleted + prevHandler.unregisterFromEvents(this.#eventViewTag); + } else if ( + isWorkletEventHandler(newProp) && + newProp.workletEventHandler !== prevHandler + ) { + // Prop got changed + prevHandler.unregisterFromEvents(this.#eventViewTag); + newProp.workletEventHandler.registerForEvents(this.#eventViewTag); + } + }); + + executeForEachEventHandler(this.#managedComponent.props, (key, handler) => { + if (!prevProps[key]) { + // Prop got added + handler.registerForEvents(this.#eventViewTag); + } + }); + } + + private getEventViewTag() { + // Get the tag for registering events - since the event emitting view can be nested inside the main component + const componentAnimatedRef = this.#managedComponent + ._component as AnimatedComponentRef; + let newTag: number; + if (componentAnimatedRef.getScrollableNode) { + const scrollableNode = componentAnimatedRef.getScrollableNode(); + newTag = findNodeHandle(scrollableNode) ?? -1; + } else { + newTag = + findNodeHandle( + this.#componentOptions?.setNativeProps + ? this.#managedComponent + : componentAnimatedRef + ) ?? -1; + } + return newTag; + } +} + +function isWorkletEventHandler( + prop: unknown +): prop is WorkletEventHandlerHolder { + return ( + has('workletEventHandler', prop) && + prop.workletEventHandler instanceof WorkletEventHandler + ); +} + +function executeForEachEventHandler( + props: AnimatedComponentProps, + callback: ( + key: string, + handler: InstanceType + ) => void +) { + for (const key in props) { + const prop = props[key]; + if (isWorkletEventHandler(prop)) { + callback(key, prop.workletEventHandler); + } + } +} + +type ManagedAnimatedComponent = React.Component< + AnimatedComponentProps +> & + IAnimatedComponentInternal; + +type ComponentOptions = { + setNativeProps: ( + ref: AnimatedComponentRef, + props: InitialComponentProps + ) => void; +}; + +type WorkletEventHandlerHolder = { + workletEventHandler: InstanceType; +}; diff --git a/packages/react-native-reanimated/src/createAnimatedComponent/commonTypes.ts b/packages/react-native-reanimated/src/createAnimatedComponent/commonTypes.ts index 8d65e6a2ae7..982c71b9db3 100644 --- a/packages/react-native-reanimated/src/createAnimatedComponent/commonTypes.ts +++ b/packages/react-native-reanimated/src/createAnimatedComponent/commonTypes.ts @@ -53,6 +53,12 @@ export interface IJSPropsUpdater { ): void; } +export interface INativeEventsManager { + attachEvents(): void; + detachEvents(): void; + updateEvents(prevProps: AnimatedComponentProps): void; +} + export type LayoutAnimationStaticContext = { presetName: string; }; @@ -95,8 +101,10 @@ export interface AnimatedComponentRef extends Component { export interface IAnimatedComponentInternal { _styles: StyleProps[] | null; _animatedProps?: Partial>; + /** + * Used for Shared Element Transitions, Layout Animations and Animated Styles. It is not related to event handling. + */ _componentViewTag: number; - _eventViewTag: number; _isFirstRender: boolean; jestAnimatedStyle: { value: StyleProps }; _component: AnimatedComponentRef | HTMLElement | null; @@ -104,6 +112,10 @@ export interface IAnimatedComponentInternal { _jsPropsUpdater: IJSPropsUpdater; _InlinePropManager: IInlinePropManager; _PropsFilter: IPropsFilter; + /** + * Doesn't exist on web. + */ + _NativeEventsManager?: INativeEventsManager; _viewInfo?: ViewInfo; context: React.ContextType; } diff --git a/packages/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx b/packages/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx index 1985bc57180..e97f5579154 100644 --- a/packages/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx +++ b/packages/react-native-reanimated/src/createAnimatedComponent/createAnimatedComponent.tsx @@ -8,7 +8,6 @@ import type { } from 'react'; import React from 'react'; import { findNodeHandle, Platform } from 'react-native'; -import { WorkletEventHandler } from '../WorkletEventHandler'; import '../layoutReanimation/animationsManager'; import invariant from 'invariant'; import { adaptViewConfig } from '../ConfigHelper'; @@ -30,8 +29,9 @@ import type { AnimatedComponentRef, IAnimatedComponentInternal, ViewInfo, + INativeEventsManager, } from './commonTypes'; -import { has, flattenArray } from './utils'; +import { flattenArray } from './utils'; import setAndForwardRef from './setAndForwardRef'; import { isFabric, isJest, isWeb, shouldBeUseWeb } from '../PlatformChecker'; import { InlinePropManager } from './InlinePropManager'; @@ -48,6 +48,7 @@ import type { CustomConfig } from '../layoutReanimation/web/config'; import type { FlatList, FlatListProps } from 'react-native'; import { addHTMLMutationObserver } from '../layoutReanimation/web/domUtils'; import { getViewInfo } from './getViewInfo'; +import { NativeEventsManager } from './NativeEventsManager'; const IS_WEB = isWeb(); @@ -115,7 +116,6 @@ export function createAnimatedComponent( _styles: StyleProps[] | null = null; _animatedProps?: Partial>; _componentViewTag = -1; - _eventViewTag = -1; _isFirstRender = true; jestAnimatedStyle: { value: StyleProps } = { value: {} }; _component: AnimatedComponentRef | HTMLElement | null = null; @@ -123,6 +123,7 @@ export function createAnimatedComponent( _jsPropsUpdater = new JSPropsUpdater(); _InlinePropManager = new InlinePropManager(); _PropsFilter = new PropsFilter(); + _NativeEventsManager?: INativeEventsManager; _viewInfo?: ViewInfo; static displayName: string; static contextType = SkipEnteringContext; @@ -136,9 +137,12 @@ export function createAnimatedComponent( } componentDidMount() { - this._setComponentViewTag(); - this._setEventViewTag(); - this._attachNativeEvents(); + this._componentViewTag = this._getComponentViewTag(); + if (!IS_WEB) { + // It exists only on native platforms. We initialize it here because the ref to the animated component is available only post-mount + this._NativeEventsManager = new NativeEventsManager(this, options); + } + this._NativeEventsManager?.attachEvents(); this._jsPropsUpdater.addOnJSPropsChangeListener(this); this._attachAnimatedStyles(); this._InlinePropManager.attachInlineProps(this, this._getViewInfo()); @@ -172,7 +176,7 @@ export function createAnimatedComponent( } componentWillUnmount() { - this._detachNativeEvents(); + this._NativeEventsManager?.detachEvents(); this._jsPropsUpdater.removeOnJSPropsChangeListener(this); this._detachStyles(); this._InlinePropManager.detachInlineProps(); @@ -218,46 +222,8 @@ export function createAnimatedComponent( } } - _setComponentViewTag() { - this._componentViewTag = this._getViewInfo().viewTag as number; - } - - _setEventViewTag() { - // Setting the tag for registering events - since the event emitting view can be nested inside the main component - const componentAnimatedRef = this._component as AnimatedComponentRef; - if (componentAnimatedRef.getScrollableNode) { - const scrollableNode = componentAnimatedRef.getScrollableNode(); - this._eventViewTag = findNodeHandle(scrollableNode) ?? -1; - } else { - this._eventViewTag = - findNodeHandle( - options?.setNativeProps ? this : componentAnimatedRef - ) ?? -1; - } - } - - _attachNativeEvents() { - for (const key in this.props) { - const prop = this.props[key]; - if ( - has('workletEventHandler', prop) && - prop.workletEventHandler instanceof WorkletEventHandler - ) { - prop.workletEventHandler.registerForEvents(this._eventViewTag, key); - } - } - } - - _detachNativeEvents() { - for (const key in this.props) { - const prop = this.props[key]; - if ( - has('workletEventHandler', prop) && - prop.workletEventHandler instanceof WorkletEventHandler - ) { - prop.workletEventHandler.unregisterFromEvents(this._eventViewTag); - } - } + _getComponentViewTag() { + return this._getViewInfo().viewTag as number; } _detachStyles() { @@ -280,48 +246,6 @@ export function createAnimatedComponent( } } - _updateNativeEvents( - prevProps: AnimatedComponentProps - ) { - for (const key in prevProps) { - const prevProp = prevProps[key]; - if ( - has('workletEventHandler', prevProp) && - prevProp.workletEventHandler instanceof WorkletEventHandler - ) { - const newProp = this.props[key]; - if (!newProp) { - // Prop got deleted - prevProp.workletEventHandler.unregisterFromEvents( - this._eventViewTag - ); - } else if ( - has('workletEventHandler', newProp) && - newProp.workletEventHandler instanceof WorkletEventHandler && - newProp.workletEventHandler !== prevProp.workletEventHandler - ) { - // Prop got changed - prevProp.workletEventHandler.unregisterFromEvents( - this._eventViewTag - ); - newProp.workletEventHandler.registerForEvents(this._eventViewTag); - } - } - } - - for (const key in this.props) { - const newProp = this.props[key]; - if ( - has('workletEventHandler', newProp) && - newProp.workletEventHandler instanceof WorkletEventHandler && - !prevProps[key] - ) { - // Prop got added - newProp.workletEventHandler.registerForEvents(this._eventViewTag); - } - } - } - _updateFromNative(props: StyleProps) { if (options?.setNativeProps) { options.setNativeProps(this._component as AnimatedComponentRef, props); @@ -469,7 +393,7 @@ export function createAnimatedComponent( ) { this._configureSharedTransition(); } - this._updateNativeEvents(prevProps); + this._NativeEventsManager?.updateEvents(prevProps); this._attachAnimatedStyles(); this._InlinePropManager.attachInlineProps(this, this._getViewInfo());