'use strict'; import type React from 'react'; import { Fragment } from 'react'; import { checkStyleOverwriting, maybeBuild } from '../animationBuilder'; import { IS_JEST, logger } from '../common'; import type { StyleProps } from '../commonTypes'; import { LayoutAnimationType } from '../commonTypes'; import { SkipEnteringContext } from '../component/LayoutAnimationConfig'; import ReanimatedAnimatedComponent from '../css/component/AnimatedComponent'; import { getStaticFeatureFlag } from '../featureFlags'; import type { AnimatedStyleHandle } from '../hook/commonTypes'; import { SharedTransition } from '../layoutReanimation/SharedTransition'; import { PropsRegistryGarbageCollector } from '../PropsRegistryGarbageCollector'; import { updateLayoutAnimations } from '../UpdateLayoutAnimations'; import type { AnimatedComponentProps, AnimatedComponentRef, AnimatedProps, AnyComponent, IAnimatedComponentInternal, INativeEventsManager, InitialComponentProps, LayoutAnimationOrBuilder, NestedArray, } from './commonTypes'; import { InlinePropManager } from './InlinePropManager'; import jsPropsUpdater from './JSPropsUpdater'; import { NativeEventsManager } from './NativeEventsManager'; import { PropsFilter } from './PropsFilter'; import { filterStyles, flattenArray } from './utils'; let id = 0; const FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS = getStaticFeatureFlag('FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS') && !getStaticFeatureFlag('USE_ANIMATION_BACKEND'); export type Options

= { setNativeProps?: (ref: AnimatedComponentRef, props: P) => void; jsProps?: string[]; }; export default class AnimatedComponent extends ReanimatedAnimatedComponent< AnimatedComponentProps, { settledProps?: StyleProps; settledStyle?: StyleProps } > implements IAnimatedComponentInternal { _options?: Options; _displayName: string; _animatedStyles: StyleProps[] = []; _prevAnimatedStyles: StyleProps[] = []; _animatedProps: Partial>[] = []; _prevAnimatedProps: Partial>[] = []; _isFirstRender = true; jestInlineStyle: NestedArray | undefined; jestAnimatedStyle: { value: StyleProps } = { value: {} }; jestAnimatedProps: { value: AnimatedProps } = { value: {} }; _InlinePropManager = new InlinePropManager(); _PropsFilter = new PropsFilter(); _NativeEventsManager?: INativeEventsManager; _hasWarnedAboutLayoutAnimationStyleOverwriting?: boolean; static contextType = SkipEnteringContext; context!: React.ContextType; reanimatedID = id++; _sharedTransition?: SharedTransition; _sharedTransitionTag?: string; constructor( ChildComponent: AnyComponent, props: AnimatedComponentProps, displayName: string, options?: Options ) { super(ChildComponent, props); this._options = options; this._displayName = displayName; if (FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS) { this.state = {}; } if (IS_JEST) { this.jestAnimatedStyle = { value: {} }; this.jestAnimatedProps = { value: {} }; } this._configureSharedTransition(true); const entering = this.props.entering; const skipEntering = this.context?.current; if (!skipEntering) { this._configureLayoutAnimation(LayoutAnimationType.ENTERING, entering); } } componentDidMount() { super.componentDidMount(); // 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, this._options); this._NativeEventsManager?.attachEvents(); this._updateAnimatedStylesAndProps(); this._InlinePropManager.attachInlineProps(this, this._getViewInfo()); if (FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS) { const viewTag = this.getComponentViewTag(); if (viewTag !== -1) { PropsRegistryGarbageCollector.registerView(viewTag, this); } } if (this._options?.jsProps?.length) { jsPropsUpdater.registerComponent(this, this._options.jsProps); } this._configureLayoutAnimation( LayoutAnimationType.LAYOUT, this.props.layout ); this._configureLayoutAnimation( LayoutAnimationType.EXITING, this.props.exiting ); this._isFirstRender = false; } componentWillUnmount() { super.componentWillUnmount(); this._NativeEventsManager?.detachEvents(); this._detachStyles(); this._InlinePropManager.detachInlineProps(); if (FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS) { const viewTag = this.getComponentViewTag(); if (viewTag !== -1) { PropsRegistryGarbageCollector.unregisterView(viewTag); } } if (this._options?.jsProps?.length) { jsPropsUpdater.unregisterComponent(this); } } _syncStylePropsBackToReact(settledUpdates: StyleProps) { if (!FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS) { return; } // Settled updates arrive as one bag with no style-vs-props origin, so // partition them by the keys of attached animated props — only values // from `useAnimatedProps` or shared values passed as inline top-level // props may be passed as top-level props, all the other ones are // style-only. const animatedPropsKeys = new Set([ ...this._animatedProps.flatMap((animatedProp) => Object.keys(animatedProp.initial?.value ?? {}) ), ...Object.keys(this._InlinePropManager._inlineTopLevelProps), ]); if (animatedPropsKeys.size === 0) { this.setState((state) => ({ settledStyle: { ...state.settledStyle, ...settledUpdates }, })); return; } this.setState((state) => { const settledProps = { ...state.settledProps }; const settledStyle = { ...state.settledStyle }; for (const key in settledUpdates) { if (animatedPropsKeys.has(key)) { settledProps[key] = settledUpdates[key]; } else { settledStyle[key] = settledUpdates[key]; } } return { settledProps, settledStyle }; }); // TODO(future): revert changes when animated styles are detached } _detachStyles() { const viewTag = this.getComponentViewTag(); if (viewTag !== -1) { for (const style of this._animatedStyles) { style.viewDescriptors.remove(viewTag); } for (const animatedProp of this._animatedProps) { animatedProp?.viewDescriptors?.remove(viewTag); } } } setNativeProps(props: StyleProps) { if (this._options?.setNativeProps) { this._options.setNativeProps( this._componentRef as AnimatedComponentRef, props ); } else { (this._componentRef as AnimatedComponentRef)?.setNativeProps?.(props); } } _handleAnimatedStylesUpdate( prevStyles: StyleProps[], currentStyles: StyleProps[], jestAnimatedStyleOrProps: { value: StyleProps } ) { const { viewTag, shadowNodeWrapper } = this._getViewInfo(); const newStyles = new Set(currentStyles); const isStyleAttached = (style: StyleProps) => style.viewDescriptors.has(viewTag); // remove old styles if (prevStyles) { // in most of the cases, views have only a single animated style and it remains unchanged const hasOneSameStyle = currentStyles.length === 1 && prevStyles.length === 1 && currentStyles[0] === prevStyles[0]; if (hasOneSameStyle && isStyleAttached(prevStyles[0])) { return; } // otherwise, remove each style that is not present in new styles for (const prevStyle of prevStyles) { const isPresent = currentStyles.some((style) => { if (style === prevStyle && isStyleAttached(style)) { newStyles.delete(style); return true; } return false; }); if (!isPresent) { prevStyle.viewDescriptors.remove(viewTag); } } } newStyles.forEach((style) => { style.viewDescriptors.add( { tag: viewTag, shadowNodeWrapper, }, style.styleUpdaterContainer ); if (IS_JEST) { /** * 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); */ Object.assign(jestAnimatedStyleOrProps.value, style.initial.value); style.jestAnimatedValues.current = jestAnimatedStyleOrProps; } }); } _updateAnimatedStylesAndProps() { this._handleAnimatedStylesUpdate( this._prevAnimatedStyles, this._animatedStyles, this.jestAnimatedStyle ); this._handleAnimatedStylesUpdate( this._prevAnimatedProps, this._animatedProps, this.jestAnimatedProps ); } componentDidUpdate(prevProps: AnimatedComponentProps) { // The ref callback clears the cached view info when the host view is // replaced; the snapshot then registered this update's configs for the old // tag. if (this._viewInfo === undefined) { this._configureLayoutAnimation( LayoutAnimationType.LAYOUT, this.props.layout ); this._configureLayoutAnimation( LayoutAnimationType.EXITING, this.props.exiting ); this._sharedTransition = undefined; this._configureSharedTransition(); } this._NativeEventsManager?.updateEvents(prevProps); this._updateAnimatedStylesAndProps(); this._InlinePropManager.attachInlineProps(this, this._getViewInfo()); } _updateStyles(props: AnimatedComponentProps): void { const filteredStyles = filterStyles(flattenArray(props.style ?? [])); this._prevAnimatedStyles = this._animatedStyles; this._animatedStyles = filteredStyles.animatedStyles; const filteredAnimatedProps = filterStyles( flattenArray(props.animatedProps ?? []) ); this._prevAnimatedProps = this._animatedProps; this._animatedProps = filteredAnimatedProps.animatedStyles; if (filteredAnimatedProps.cssStyle) { if (__DEV__ && filteredStyles.cssStyle) { logger.warn( 'AnimatedComponent: CSS properties cannot be used in style and animatedProps at the same time. Using properties from the style object.' ); this._cssStyle = filteredStyles.cssStyle; return; } // Add all remaining props to cssStyle object // (e.g. SVG components are styled via top level props, not via style object) const mergedProps = { ...props, ...filteredAnimatedProps.cssStyle, }; delete mergedProps.style; delete mergedProps.animatedProps; this._cssStyle = mergedProps; } else { this._cssStyle = filteredStyles.cssStyle ?? {}; } } _configureLayoutAnimation( type: LayoutAnimationType, currentConfig: LayoutAnimationOrBuilder | undefined, previousConfig?: LayoutAnimationOrBuilder ) { if (currentConfig === previousConfig) { return; } if ( __DEV__ && currentConfig && type !== LayoutAnimationType.LAYOUT && this.props.style && !this._hasWarnedAboutLayoutAnimationStyleOverwriting ) { const onWarn = () => (this._hasWarnedAboutLayoutAnimationStyleOverwriting = true); checkStyleOverwriting( currentConfig, this.props.style, this._displayName, onWarn ); } updateLayoutAnimations( type === LayoutAnimationType.ENTERING ? this.reanimatedID : this.getComponentViewTag(), type, currentConfig && maybeBuild(currentConfig) ); } _configureSharedTransition(useNativeId?: boolean) { if (!getStaticFeatureFlag('ENABLE_SHARED_ELEMENT_TRANSITIONS')) { return; } if (!this.props.sharedTransitionTag) { if (this._sharedTransitionTag) { updateLayoutAnimations( useNativeId ? this.reanimatedID : this.getComponentViewTag(), useNativeId ? LayoutAnimationType.SHARED_ELEMENT_TRANSITION_NATIVE_ID : LayoutAnimationType.SHARED_ELEMENT_TRANSITION, undefined, undefined, undefined ); this._sharedTransitionTag = undefined; } return; } this._sharedTransitionTag = this.props.sharedTransitionTag; const sharedTransition = this.props.sharedTransitionStyle ?? this._sharedTransition ?? new SharedTransition(); if (this._sharedTransition !== sharedTransition) { updateLayoutAnimations( useNativeId ? this.reanimatedID : this.getComponentViewTag(), useNativeId ? LayoutAnimationType.SHARED_ELEMENT_TRANSITION_NATIVE_ID : LayoutAnimationType.SHARED_ELEMENT_TRANSITION, maybeBuild(sharedTransition), undefined, this.props.sharedTransitionTag ); this._sharedTransition = sharedTransition; } } // Android pulls the mounting transaction inside the React commit, so the // configs for this update must be published here, not in componentDidUpdate. getSnapshotBeforeUpdate( prevProps: AnimatedComponentProps ) { this._configureLayoutAnimation( LayoutAnimationType.LAYOUT, this.props.layout, prevProps.layout ); this._configureLayoutAnimation( LayoutAnimationType.EXITING, this.props.exiting, prevProps.exiting ); this._configureSharedTransition(); // `getSnapshotBeforeUpdate` has to return value which is not `undefined`. return null; } render() { const filteredProps = this._PropsFilter.filterNonAnimatedProps(this); if (IS_JEST) { filteredProps.jestAnimatedStyle = this.jestAnimatedStyle; filteredProps.jestAnimatedProps = this.jestAnimatedProps; } const skipEntering = this.context?.current; let nativeID, jestProps; if (IS_JEST) { jestProps = { jestInlineStyle: this.props.style && filterOutAnimatedStyles(this.props.style), jestAnimatedStyle: this.jestAnimatedStyle, jestAnimatedProps: this.jestAnimatedProps, }; } else if (!skipEntering) { nativeID = `${this.reanimatedID}`; } // TODO: Remove need for this \/\/\/\/. // RNSVG expects Gradient elem to have stops passed as children. When we want to animate them, // we provide them using `gradient` prop. // Hack below gets rid of RNSVG warnings about not having children. if ( this.ChildComponent.displayName === 'RadialGradient' || this.ChildComponent.displayName === 'LinearGradient' ) { if (filteredProps.children === undefined) { filteredProps.children = ; } } if (FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS && this.state.settledStyle) { return super.render({ nativeID, ...filteredProps, ...this.state.settledProps, style: [...flattenArray(filteredProps.style), this.state.settledStyle], ...jestProps, }); } return super.render({ nativeID, ...filteredProps, ...jestProps, }); } } function filterOutAnimatedStyles( style: NestedArray ): NestedArray { if (!style) { return style; } if (!Array.isArray(style)) { return style?.viewDescriptors ? {} : style; } return style .filter( (styleElement) => !(styleElement && 'viewDescriptors' in styleElement) ) .map((styleElement) => { if (Array.isArray(styleElement)) { return filterOutAnimatedStyles(styleElement); } return styleElement; }); }