react-native-reanimated
Version:
More powerful alternative to Animated library for React Native.
468 lines (456 loc) • 20.1 kB
JavaScript
import "../layoutReanimation/animationsManager.js";
import invariant from 'invariant';
import React from 'react';
import { Platform } from 'react-native';
import { getReduceMotionFromConfig } from "../animation/util.js";
import { maybeBuild } from "../animationBuilder.js";
import { LayoutAnimationType } from "../commonTypes.js";
import { SkipEnteringContext } from "../component/LayoutAnimationConfig.js";
import { adaptViewConfig } from "../ConfigHelper.js";
import { enableLayoutAnimations, markNodeAsRemovable, unmarkNodeAsRemovable } from "../core.js";
import { ReanimatedError } from "../errors.js";
import { getShadowNodeWrapperFromRef } from '../fabricUtils';
import { SharedTransition } from "../layoutReanimation/index.js";
import { configureWebLayoutAnimations, getReducedMotionFromConfig, saveSnapshot, startWebLayoutAnimation, tryActivateLayoutTransition } from "../layoutReanimation/web/index.js";
import { addHTMLMutationObserver } from "../layoutReanimation/web/domUtils.js";
import { findHostInstance } from '../platform-specific/findHostInstance';
import { isFabric, isJest, isReact19, isWeb, shouldBeUseWeb } from "../PlatformChecker.js";
import { componentWithRef } from "../reactUtils.js";
import { updateLayoutAnimations } from "../UpdateLayoutAnimations.js";
import { getViewInfo } from "./getViewInfo.js";
import { InlinePropManager } from "./InlinePropManager.js";
import JSPropsUpdater from './JSPropsUpdater';
import { NativeEventsManager } from "./NativeEventsManager.js";
import { PropsFilter } from "./PropsFilter.js";
import setAndForwardRef from "./setAndForwardRef.js";
import { flattenArray } from "./utils.js";
const IS_WEB = isWeb();
const IS_JEST = isJest();
const IS_REACT_19 = isReact19();
const SHOULD_BE_USE_WEB = shouldBeUseWeb();
if (IS_WEB) {
configureWebLayoutAnimations();
}
function onlyAnimatedStyles(styles) {
return styles.filter(style => style?.viewDescriptors);
}
/**
* 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
/**
* @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
let id = 0;
export function createAnimatedComponent(Component, options) {
if (!IS_REACT_19) {
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 {
_styles = null;
_isFirstRender = true;
jestAnimatedStyle = {
value: {}
};
jestAnimatedProps = {
value: {}
};
_componentRef = null;
_hasAnimatedRef = false;
// Used only on web
_componentDOMRef = null;
_sharedElementTransition = null;
_jsPropsUpdater = new JSPropsUpdater();
_InlinePropManager = new InlinePropManager();
_PropsFilter = new PropsFilter();
static contextType = SkipEnteringContext;
reanimatedID = id++;
_willUnmount = false;
constructor(props) {
super(props);
if (IS_JEST) {
this.jestAnimatedStyle = {
value: {}
};
this.jestAnimatedProps = {
value: {}
};
}
const entering = this.props.entering;
const skipEntering = this.context?.current;
if (!entering || getReducedMotionFromConfig(entering) || skipEntering || !isFabric()) {
return;
}
// This call is responsible for configuring entering animations on Fabric.
updateLayoutAnimations(this.reanimatedID, LayoutAnimationType.ENTERING, maybeBuild(entering, this.props?.style, AnimatedComponent.displayName));
}
componentDidMount() {
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());
const layout = this.props.layout;
if (layout) {
this._configureLayoutTransition();
}
if (IS_WEB) {
if (this.props.exiting && this._componentDOMRef) {
saveSnapshot(this._componentDOMRef);
}
if (!this.props.entering || getReducedMotionFromConfig(this.props.entering)) {
this._isFirstRender = false;
return;
}
const skipEntering = this.context?.current;
if (!skipEntering) {
startWebLayoutAnimation(this.props, this._componentDOMRef, LayoutAnimationType.ENTERING);
} else if (this._componentDOMRef) {
this._componentDOMRef.style.visibility = 'initial';
}
}
const viewTag = this._viewInfo?.viewTag;
if (!SHOULD_BE_USE_WEB && isFabric() && this._willUnmount && typeof viewTag === 'number') {
unmarkNodeAsRemovable(viewTag);
}
this._isFirstRender = false;
}
componentWillUnmount() {
this._NativeEventsManager?.detachEvents();
this._jsPropsUpdater.removeOnJSPropsChangeListener(this);
this._detachStyles();
this._InlinePropManager.detachInlineProps();
if (this.props.sharedTransitionTag) {
this._configureSharedTransition(true);
}
this._sharedElementTransition?.unregisterTransition(this.getComponentViewTag(), true);
const exiting = this.props.exiting;
if (IS_WEB && this._componentDOMRef && exiting && !getReducedMotionFromConfig(exiting)) {
addHTMLMutationObserver();
startWebLayoutAnimation(this.props, this._componentDOMRef, LayoutAnimationType.EXITING);
} else if (exiting && !IS_WEB && !isFabric()) {
const reduceMotionInExiting = 'getReduceMotion' in exiting && typeof exiting.getReduceMotion === 'function' ? getReduceMotionFromConfig(exiting.getReduceMotion()) : getReduceMotionFromConfig();
if (!reduceMotionInExiting) {
updateLayoutAnimations(this.getComponentViewTag(), LayoutAnimationType.EXITING, maybeBuild(exiting, this.props?.style, AnimatedComponent.displayName));
}
}
const wrapper = this._viewInfo?.shadowNodeWrapper;
if (!SHOULD_BE_USE_WEB && isFabric() && wrapper) {
// Mark node as removable on the native (C++) side, but only actually remove it
// when it no longer exists in the Shadow Tree. This ensures proper cleanup of
// animations/transitions/props while handling cases where the node might be
// remounted (e.g., when frozen) after componentWillUnmount is called.
markNodeAsRemovable(wrapper);
}
this._willUnmount = true;
}
getComponentViewTag() {
return this._getViewInfo().viewTag;
}
_detachStyles() {
const viewTag = this.getComponentViewTag();
if (viewTag !== -1 && this._styles !== null) {
for (const style of this._styles) {
style.viewDescriptors.remove(viewTag);
}
if (this.props.animatedProps?.viewDescriptors) {
this.props.animatedProps.viewDescriptors.remove(viewTag);
}
}
}
_updateFromNative(props) {
if (options?.setNativeProps) {
options.setNativeProps(this._componentRef, props);
} else {
this._componentRef?.setNativeProps?.(props);
}
}
_getViewInfo() {
if (this._viewInfo !== undefined) {
return this._viewInfo;
}
let viewTag;
let viewName;
let shadowNodeWrapper = null;
let viewConfig;
let DOMElement = null;
if (SHOULD_BE_USE_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._componentRef;
DOMElement = this._componentDOMRef;
viewName = null;
shadowNodeWrapper = null;
viewConfig = null;
} else {
const hostInstance = findHostInstance(this);
if (!hostInstance) {
/*
findHostInstance can return 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
*/
throw new ReanimatedError('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, hostInstance) : null;
}
this._viewInfo = {
viewTag,
viewName,
shadowNodeWrapper,
viewConfig
};
if (DOMElement) {
this._viewInfo.DOMElement = DOMElement;
}
return this._viewInfo;
}
_attachAnimatedStyles() {
const styles = this.props.style ? onlyAnimatedStyles(flattenArray(this.props.style)) : [];
const animatedProps = this.props.animatedProps;
const prevStyles = this._styles;
this._styles = styles;
const prevAnimatedProps = this._animatedProps;
this._animatedProps = 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);
}
// 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);
}
}
}
}
if (animatedProps && IS_JEST) {
this.jestAnimatedProps.value = {
...this.jestAnimatedProps.value,
...animatedProps?.initial?.value
};
if (animatedProps?.jestAnimatedValues) {
animatedProps.jestAnimatedValues.current = this.jestAnimatedProps;
}
}
styles.forEach(style => {
style.viewDescriptors.add({
tag: viewTag,
name: viewName,
shadowNodeWrapper
});
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);
*/
this.jestAnimatedStyle.value = {
...this.jestAnimatedStyle.value,
...style.initial.value
};
style.jestAnimatedValues.current = this.jestAnimatedStyle;
}
});
// detach old animatedProps
if (prevAnimatedProps && prevAnimatedProps !== this.props.animatedProps) {
prevAnimatedProps.viewDescriptors.remove(viewTag);
}
// attach animatedProps property
if (this.props.animatedProps?.viewDescriptors) {
this.props.animatedProps.viewDescriptors.add({
tag: viewTag,
name: viewName,
shadowNodeWrapper: shadowNodeWrapper
});
}
}
componentDidUpdate(prevProps, _prevState,
// This type comes straight from React
// eslint-disable-next-line @typescript-eslint/no-explicit-any
snapshot) {
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._NativeEventsManager?.updateEvents(prevProps);
this._attachAnimatedStyles();
this._InlinePropManager.attachInlineProps(this, this._getViewInfo());
if (IS_WEB && this.props.exiting && this._componentDOMRef) {
saveSnapshot(this._componentDOMRef);
}
// Snapshot won't be undefined because it comes from getSnapshotBeforeUpdate method
if (IS_WEB && snapshot !== null && this.props.layout && !getReducedMotionFromConfig(this.props.layout)) {
tryActivateLayoutTransition(this.props, this._componentDOMRef, snapshot);
}
}
_configureLayoutTransition() {
if (IS_WEB) {
return;
}
const layout = this.props.layout;
if (layout && getReducedMotionFromConfig(layout)) {
return;
}
updateLayoutAnimations(this.getComponentViewTag(), LayoutAnimationType.LAYOUT, layout && maybeBuild(layout, undefined /* We don't have to warn user if style has common properties with animation for LAYOUT */, AnimatedComponent.displayName));
}
_configureSharedTransition(isUnmounting = false) {
if (IS_WEB) {
return;
}
const {
sharedTransitionTag
} = this.props;
if (!sharedTransitionTag) {
this._sharedElementTransition?.unregisterTransition(this.getComponentViewTag(), isUnmounting);
this._sharedElementTransition = null;
return;
}
const sharedElementTransition = this.props.sharedTransitionStyle ?? this._sharedElementTransition ?? new SharedTransition();
sharedElementTransition.registerTransition(this.getComponentViewTag(), sharedTransitionTag, isUnmounting);
this._sharedElementTransition = sharedElementTransition;
}
_resolveComponentRef = ref => {
const componentRef = ref;
// Component can specify ref which should be animated when animated version of the component is created.
// Otherwise, we animate the component itself.
if (componentRef && componentRef.getAnimatableRef) {
this._hasAnimatedRef = true;
return componentRef.getAnimatableRef();
}
// Case for SVG components on Web
if (SHOULD_BE_USE_WEB) {
if (componentRef && componentRef.elementRef) {
this._componentDOMRef = componentRef.elementRef.current;
} else {
this._componentDOMRef = ref;
}
}
return componentRef;
};
_setComponentRef = setAndForwardRef({
getForwardedRef: () => this.props.forwardedRef,
setLocalRef: ref => {
if (!ref) {
// component has been unmounted
return;
}
if (ref !== this._componentRef) {
this._componentRef = this._resolveComponentRef(ref);
// if ref is changed, reset viewInfo
this._viewInfo = undefined;
}
const tag = this.getComponentViewTag();
const {
layout,
entering,
exiting,
sharedTransitionTag
} = this.props;
if (layout || entering || exiting || sharedTransitionTag) {
if (!SHOULD_BE_USE_WEB) {
enableLayoutAnimations(true, false);
}
if (sharedTransitionTag) {
this._configureSharedTransition();
}
if (exiting && isFabric()) {
const reduceMotionInExiting = 'getReduceMotion' in exiting && typeof exiting.getReduceMotion === 'function' ? getReduceMotionFromConfig(exiting.getReduceMotion()) : getReduceMotionFromConfig();
if (!reduceMotionInExiting) {
updateLayoutAnimations(tag, LayoutAnimationType.EXITING, maybeBuild(exiting, this.props?.style, AnimatedComponent.displayName));
}
}
const skipEntering = this.context?.current;
if (entering && !isFabric() && !skipEntering && !IS_WEB) {
updateLayoutAnimations(tag, LayoutAnimationType.ENTERING, maybeBuild(entering, this.props?.style, AnimatedComponent.displayName));
}
}
}
});
// 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._componentDOMRef?.getBoundingClientRect !== undefined) {
return this._componentDOMRef.getBoundingClientRect();
}
return null;
}
render() {
const filteredProps = this._PropsFilter.filterNonAnimatedProps(this);
if (IS_JEST) {
filteredProps.jestAnimatedStyle = this.jestAnimatedStyle;
filteredProps.jestAnimatedProps = this.jestAnimatedProps;
}
// 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)) {
filteredProps.style = Array.isArray(filteredProps.style) ? filteredProps.style.concat([{
visibility: 'hidden'
}]) : {
...(filteredProps.style ?? {}),
visibility: 'hidden' // Hide component until `componentDidMount` triggers
};
}
const platformProps = Platform.select({
web: {},
default: {
collapsable: false
}
});
const skipEntering = this.context?.current;
const nativeID = skipEntering || !isFabric() ? undefined : `${this.reanimatedID}`;
const jestProps = IS_JEST ? {
jestInlineStyle: this.props.style,
jestAnimatedStyle: this.jestAnimatedStyle,
jestAnimatedProps: this.jestAnimatedProps
} : {};
return <Component nativeID={nativeID} {...filteredProps} {...jestProps}
// Casting is used here, because ref can be null - in that case it cannot be assigned to HTMLElement.
// After spending some time trying to figure out what to do with this problem, we decided to leave it this way
ref={this._setComponentRef} {...platformProps} />;
}
}
AnimatedComponent.displayName = `AnimatedComponent(${Component.displayName || Component.name || 'Component'})`;
const animatedComponent = componentWithRef((props, ref) => <AnimatedComponent {...props} {...ref === null ? null : {
forwardedRef: ref
}} />);
animatedComponent.displayName = Component.displayName || Component.name || 'Component';
return animatedComponent;
}
//# sourceMappingURL=createAnimatedComponent.js.map
;