'use strict'; import type { Component, ComponentClass, ComponentType, FunctionComponent, MutableRefObject, } from 'react'; import React from 'react'; import { findNodeHandle, Platform } from 'react-native'; import { WorkletEventHandler } from '../reanimated2/WorkletEventHandler'; import '../reanimated2/layoutReanimation/animationsManager'; import invariant from 'invariant'; import { adaptViewConfig } from '../ConfigHelper'; import { RNRenderer } from '../reanimated2/platform-specific/RNRenderer'; import { enableLayoutAnimations } from '../reanimated2/core'; import { SharedTransition, LayoutAnimationType, } from '../reanimated2/layoutReanimation'; import type { StyleProps, ShadowNodeWrapper } from '../reanimated2/commonTypes'; import { getShadowNodeWrapperFromRef } from '../reanimated2/fabricUtils'; import { removeFromPropsRegistry } from '../reanimated2/PropsRegistry'; import { getReduceMotionFromConfig } from '../reanimated2/animation/util'; import { maybeBuild } from '../animationBuilder'; import { SkipEnteringContext } from '../reanimated2/component/LayoutAnimationConfig'; import type { AnimateProps } from '../reanimated2'; import JSPropsUpdater from './JSPropsUpdater'; import type { AnimatedComponentProps, AnimatedProps, InitialComponentProps, AnimatedComponentRef, IAnimatedComponentInternal, ViewInfo, } from './commonTypes'; import { has, flattenArray } from './utils'; import setAndForwardRef from './setAndForwardRef'; import { isFabric, isJest, isWeb, shouldBeUseWeb, } from '../reanimated2/PlatformChecker'; import { InlinePropManager } from './InlinePropManager'; import { PropsFilter } from './PropsFilter'; import { startWebLayoutAnimation, tryActivateLayoutTransition, configureWebLayoutAnimations, getReducedMotionFromConfig, saveSnapshot, } from '../reanimated2/layoutReanimation/web'; import { updateLayoutAnimations } from '../reanimated2/UpdateLayoutAnimations'; import type { CustomConfig } from '../reanimated2/layoutReanimation/web/config'; import type { FlatList, FlatListProps } from 'react-native'; import { addHTMLMutationObserver } from '../reanimated2/layoutReanimation/web/domUtils'; import { getViewInfo } from './getViewInfo'; const IS_WEB = isWeb(); if (IS_WEB) { configureWebLayoutAnimations(); } function onlyAnimatedStyles(styles: StyleProps[]): StyleProps[] { return styles.filter((style) => style?.viewDescriptors); } type Options

= { setNativeProps: (ref: AnimatedComponentRef, props: P) => void; }; /** * Lets you create an Animated version of any React Native component. * * @param component - The component you want to make animatable. * @returns A component that Reanimated is capable of animating. * @see https://docs.swmansion.com/react-native-reanimated/docs/core/createAnimatedComponent */ // Don't change the order of overloads, since such a change breaks current behavior export function createAnimatedComponent

( component: FunctionComponent

, options?: Options

): FunctionComponent>; export function createAnimatedComponent

( component: ComponentClass

, options?: Options

): ComponentClass>; export function createAnimatedComponent

( // Actually ComponentType

= ComponentClass

| FunctionComponent

but we need this overload too // since some external components (like FastImage) are typed just as ComponentType component: ComponentType

, options?: Options

): FunctionComponent> | ComponentClass>; /** * @deprecated Please use `Animated.FlatList` component instead of calling `Animated.createAnimatedComponent(FlatList)` manually. */ // @ts-ignore This is required to create this overload, since type of createAnimatedComponent is incorrect and doesn't include typeof FlatList export function createAnimatedComponent( component: typeof FlatList, options?: Options ): ComponentClass>>; export function createAnimatedComponent( Component: ComponentType, options?: Options ): any { invariant( typeof Component !== 'function' || (Component.prototype && Component.prototype.isReactComponent), `Looks like you're passing a function component \`${Component.name}\` to \`createAnimatedComponent\` function which supports only class components. Please wrap your function component with \`React.forwardRef()\` or use a class component instead.` ); class AnimatedComponent extends React.Component> implements IAnimatedComponentInternal { _styles: StyleProps[] | null = null; _animatedProps?: Partial>; _componentViewTag = -1; _eventViewTag = -1; _isFirstRender = true; jestAnimatedStyle: { value: StyleProps } = { value: {} }; _component: AnimatedComponentRef | HTMLElement | null = null; _sharedElementTransition: SharedTransition | null = null; _jsPropsUpdater = new JSPropsUpdater(); _InlinePropManager = new InlinePropManager(); _PropsFilter = new PropsFilter(); _viewInfo?: ViewInfo; static displayName: string; static contextType = SkipEnteringContext; context!: React.ContextType; constructor(props: AnimatedComponentProps) { super(props); if (isJest()) { this.jestAnimatedStyle = { value: {} }; } } componentDidMount() { this._setComponentViewTag(); this._setEventViewTag(); this._attachNativeEvents(); this._jsPropsUpdater.addOnJSPropsChangeListener(this); this._attachAnimatedStyles(); this._InlinePropManager.attachInlineProps(this, this._getViewInfo()); const layout = this.props.layout; if (layout) { this._configureLayoutTransition(); } if (IS_WEB) { if (this.props.exiting) { saveSnapshot(this._component as HTMLElement); } if ( !this.props.entering || getReducedMotionFromConfig(this.props.entering as CustomConfig) ) { this._isFirstRender = false; return; } startWebLayoutAnimation( this.props, this._component as HTMLElement, LayoutAnimationType.ENTERING ); } this._isFirstRender = false; } componentWillUnmount() { this._detachNativeEvents(); this._jsPropsUpdater.removeOnJSPropsChangeListener(this); this._detachStyles(); this._InlinePropManager.detachInlineProps(); if (this.props.sharedTransitionTag) { this._configureSharedTransition(true); } this._sharedElementTransition?.unregisterTransition( this._componentViewTag, true ); const exiting = this.props.exiting; if ( IS_WEB && this._component && this.props.exiting && !getReducedMotionFromConfig(this.props.exiting as CustomConfig) ) { addHTMLMutationObserver(); startWebLayoutAnimation( this.props, this._component as HTMLElement, LayoutAnimationType.EXITING ); } else if (exiting) { const reduceMotionInExiting = 'getReduceMotion' in exiting && typeof exiting.getReduceMotion === 'function' ? getReduceMotionFromConfig(exiting.getReduceMotion()) : getReduceMotionFromConfig(); if (!reduceMotionInExiting) { updateLayoutAnimations( this._componentViewTag, LayoutAnimationType.EXITING, maybeBuild( exiting, this.props?.style, AnimatedComponent.displayName ) ); } } } _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); } } } _detachStyles() { if (IS_WEB && this._styles !== null) { for (const style of this._styles) { style.viewsRef.remove(this); } } else if (this._componentViewTag !== -1 && this._styles !== null) { for (const style of this._styles) { style.viewDescriptors.remove(this._componentViewTag); } if (this.props.animatedProps?.viewDescriptors) { this.props.animatedProps.viewDescriptors.remove( this._componentViewTag ); } if (isFabric()) { removeFromPropsRegistry(this._componentViewTag); } } } _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); } else { (this._component as AnimatedComponentRef)?.setNativeProps?.(props); } } _getViewInfo(): ViewInfo { if (this._viewInfo !== undefined) { return this._viewInfo; } let viewTag: number | HTMLElement | null; let viewName: string | null; let shadowNodeWrapper: ShadowNodeWrapper | null = null; let viewConfig; // Component can specify ref which should be animated when animated version of the component is created. // Otherwise, we animate the component itself. const component = (this._component as AnimatedComponentRef) ?.getAnimatableRef ? (this._component as AnimatedComponentRef).getAnimatableRef?.() : this; if (IS_WEB) { // At this point I assume that `_setComponentRef` was already called and `_component` is set. // `this._component` on web represents HTMLElement of our component, that's why we use casting viewTag = this._component as HTMLElement; viewName = null; shadowNodeWrapper = null; viewConfig = 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(component); if (!hostInstance) { throw new Error( '[Reanimated] Cannot find host instance for this component. Maybe it renders nothing?' ); } const viewInfo = getViewInfo(hostInstance); viewTag = viewInfo.viewTag; viewName = viewInfo.viewName; viewConfig = viewInfo.viewConfig; shadowNodeWrapper = isFabric() ? getShadowNodeWrapperFromRef(this) : null; } this._viewInfo = { viewTag, viewName, shadowNodeWrapper, viewConfig }; return this._viewInfo; } _attachAnimatedStyles() { const styles = this.props.style ? onlyAnimatedStyles(flattenArray(this.props.style)) : []; const prevStyles = this._styles; this._styles = styles; const prevAnimatedProps = this._animatedProps; this._animatedProps = this.props.animatedProps; const { viewTag, viewName, shadowNodeWrapper, viewConfig } = this._getViewInfo(); // update UI props whitelist for this view const hasReanimated2Props = this.props.animatedProps?.viewDescriptors || styles.length; if (hasReanimated2Props && viewConfig) { adaptViewConfig(viewConfig); } this._componentViewTag = viewTag as number; // remove old styles if (prevStyles) { // in most of the cases, views have only a single animated style and it remains unchanged const hasOneSameStyle = styles.length === 1 && prevStyles.length === 1 && styles[0] === prevStyles[0]; if (!hasOneSameStyle) { // otherwise, remove each style that is not present in new styles for (const prevStyle of prevStyles) { const isPresent = styles.some((style) => style === prevStyle); if (!isPresent) { prevStyle.viewDescriptors.remove(viewTag); } } } } styles.forEach((style) => { style.viewDescriptors.add({ tag: viewTag, name: viewName, shadowNodeWrapper, }); 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.jestAnimatedStyle.value = { ...this.jestAnimatedStyle.value, ...style.initial.value, }; style.jestAnimatedStyle.current = this.jestAnimatedStyle; } }); // detach old animatedProps if (prevAnimatedProps && prevAnimatedProps !== this.props.animatedProps) { prevAnimatedProps.viewDescriptors!.remove(viewTag as number); } // attach animatedProps property if (this.props.animatedProps?.viewDescriptors) { this.props.animatedProps.viewDescriptors.add({ tag: viewTag as number, name: viewName!, shadowNodeWrapper: shadowNodeWrapper!, }); } } componentDidUpdate( prevProps: AnimatedComponentProps, _prevState: Readonly, // This type comes straight from React // eslint-disable-next-line @typescript-eslint/no-explicit-any snapshot: DOMRect | null ) { const layout = this.props.layout; const oldLayout = prevProps.layout; if (layout !== oldLayout) { this._configureLayoutTransition(); } if ( this.props.sharedTransitionTag !== undefined || prevProps.sharedTransitionTag !== undefined ) { this._configureSharedTransition(); } this._updateNativeEvents(prevProps); this._attachAnimatedStyles(); this._InlinePropManager.attachInlineProps(this, this._getViewInfo()); if (IS_WEB && this.props.exiting) { saveSnapshot(this._component as HTMLElement); } // Snapshot won't be undefined because it comes from getSnapshotBeforeUpdate method if ( IS_WEB && snapshot !== null && this.props.layout && !getReducedMotionFromConfig(this.props.layout as CustomConfig) ) { tryActivateLayoutTransition( this.props, this._component as HTMLElement, snapshot ); } } _configureLayoutTransition() { const layout = this.props.layout ? maybeBuild( this.props.layout, undefined /* We don't have to warn user if style has common properties with animation for LAYOUT */, AnimatedComponent.displayName ) : undefined; updateLayoutAnimations( this._componentViewTag, LayoutAnimationType.LAYOUT, layout ); } _configureSharedTransition(isUnmounting = false) { if (IS_WEB) { return; } const { sharedTransitionTag } = this.props; if (!sharedTransitionTag) { this._sharedElementTransition?.unregisterTransition( this._componentViewTag, isUnmounting ); this._sharedElementTransition = null; return; } const sharedElementTransition = this.props.sharedTransitionStyle ?? this._sharedElementTransition ?? new SharedTransition(); sharedElementTransition.registerTransition( this._componentViewTag, sharedTransitionTag, isUnmounting ); this._sharedElementTransition = sharedElementTransition; } _setComponentRef = setAndForwardRef({ getForwardedRef: () => this.props.forwardedRef as MutableRefObject< Component, Record, unknown> >, setLocalRef: (ref) => { // TODO update config const tag = IS_WEB ? (ref as HTMLElement) : findNodeHandle(ref as Component); this._componentViewTag = tag as number; const { layout, entering, exiting, sharedTransitionTag } = this.props; if ( (layout || entering || exiting || sharedTransitionTag) && tag != null ) { if (!shouldBeUseWeb()) { enableLayoutAnimations(true, false); } if (sharedTransitionTag) { this._configureSharedTransition(); } const skipEntering = this.context?.current; if (entering && !skipEntering) { updateLayoutAnimations( tag as number, LayoutAnimationType.ENTERING, maybeBuild( entering, this.props?.style, AnimatedComponent.displayName ) ); } } if (ref !== this._component) { this._component = ref; } }, }); // This is a component lifecycle method from React, therefore we are not calling it directly. // It is called before the component gets rerendered. This way we can access components' position before it changed // and later on, in componentDidUpdate, calculate translation for layout transition. getSnapshotBeforeUpdate() { if ( IS_WEB && (this._component as HTMLElement)?.getBoundingClientRect !== undefined ) { return (this._component as HTMLElement).getBoundingClientRect(); } return null; } render() { const filteredProps = this._PropsFilter.filterNonAnimatedProps(this); if (isJest()) { filteredProps.jestAnimatedStyle = this.jestAnimatedStyle; } // Layout animations on web are set inside `componentDidMount` method, which is called after first render. // Because of that we can encounter a situation in which component is visible for a short amount of time, and later on animation triggers. // I've tested that on various browsers and devices and it did not happen to me. To be sure that it won't happen to someone else, // I've decided to hide component at first render. Its visibility is reset in `componentDidMount`. if ( this._isFirstRender && IS_WEB && filteredProps.entering && !getReducedMotionFromConfig(filteredProps.entering as CustomConfig) ) { filteredProps.style = { ...(filteredProps.style ?? {}), visibility: 'hidden', // Hide component until `componentDidMount` triggers }; } const platformProps = Platform.select({ web: {}, default: { collapsable: false }, }); return ( void} {...platformProps} /> ); } } AnimatedComponent.displayName = `AnimatedComponent(${ Component.displayName || Component.name || 'Component' })`; return React.forwardRef((props, ref) => { return ( ); }); }