react-native-reanimated
Version:
More powerful alternative to Animated library for React Native.
275 lines (268 loc) • 11.3 kB
JavaScript
'use strict';
import "../layoutReanimation/animationsManager.js";
import { getReduceMotionFromConfig } from "../animation/util.js";
import { maybeBuild } from "../animationBuilder.js";
import { IS_JEST, IS_WEB } from "../common/index.js";
import { LayoutAnimationType } from "../commonTypes.js";
import { SkipEnteringContext } from "../component/LayoutAnimationConfig.js";
import ReanimatedAnimatedComponent from "../css/component/AnimatedComponent.js";
import { configureWebLayoutAnimations, getReducedMotionFromConfig, saveSnapshot, startWebLayoutAnimation, tryActivateLayoutTransition } from "../layoutReanimation/web/index.js";
import { addHTMLMutationObserver } from "../layoutReanimation/web/domUtils.js";
import { updateLayoutAnimations } from "../UpdateLayoutAnimations.js";
import { InlinePropManager } from "./InlinePropManager.js";
import jsPropsUpdater from "./JSPropsUpdater.js";
import { NativeEventsManager } from "./NativeEventsManager.js";
import { PropsFilter } from "./PropsFilter.js";
import { filterStyles, flattenArray } from "./utils.js";
let id = 0;
if (IS_WEB) {
configureWebLayoutAnimations();
}
export default class AnimatedComponent extends ReanimatedAnimatedComponent {
_animatedStyles = [];
_prevAnimatedStyles = [];
_isFirstRender = true;
jestAnimatedStyle = {
value: {}
};
jestAnimatedProps = {
value: {}
};
_InlinePropManager = new InlinePropManager();
_PropsFilter = new PropsFilter();
static contextType = SkipEnteringContext;
reanimatedID = id++;
constructor(ChildComponent, props, displayName, options) {
super(ChildComponent, props);
this._options = options;
this._displayName = displayName;
if (IS_JEST) {
this.jestAnimatedStyle = {
value: {}
};
this.jestAnimatedProps = {
value: {}
};
}
const skipEntering = this.context?.current;
if (!skipEntering) {
this._configureLayoutAnimation(LayoutAnimationType.ENTERING, this.props.entering);
}
}
componentDidMount() {
super.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, this._options);
}
this._NativeEventsManager?.attachEvents();
this._attachAnimatedStyles();
this._InlinePropManager.attachInlineProps(this, this._getViewInfo());
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);
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';
}
}
this._isFirstRender = false;
}
componentWillUnmount() {
super.componentWillUnmount();
this._NativeEventsManager?.detachEvents();
this._detachStyles();
this._InlinePropManager.detachInlineProps();
if (this._options?.jsProps?.length) {
jsPropsUpdater.unregisterComponent(this);
}
const exiting = this.props.exiting;
if (IS_WEB && this._componentDOMRef && exiting && !getReducedMotionFromConfig(exiting)) {
addHTMLMutationObserver();
startWebLayoutAnimation(this.props, this._componentDOMRef, LayoutAnimationType.EXITING);
}
}
_detachStyles() {
const viewTag = this.getComponentViewTag();
if (viewTag !== -1) {
for (const style of this._animatedStyles) {
style.viewDescriptors.remove(viewTag);
}
if (this.props.animatedProps?.viewDescriptors) {
this.props.animatedProps.viewDescriptors.remove(viewTag);
}
}
}
setNativeProps(props) {
if (this._options?.setNativeProps) {
this._options.setNativeProps(this._componentRef, props);
} else {
this._componentRef?.setNativeProps?.(props);
}
}
_attachAnimatedStyles() {
const animatedProps = this.props.animatedProps;
const prevAnimatedProps = this._animatedProps;
this._animatedProps = animatedProps;
const {
viewTag,
shadowNodeWrapper
} = this._getViewInfo();
// remove old styles
if (this._prevAnimatedStyles) {
// in most of the cases, views have only a single animated style and it remains unchanged
const hasOneSameStyle = this._animatedStyles.length === 1 && this._prevAnimatedStyles.length === 1 && this._animatedStyles[0] === this._prevAnimatedStyles[0];
if (!hasOneSameStyle) {
// otherwise, remove each style that is not present in new styles
for (const prevStyle of this._prevAnimatedStyles) {
const isPresent = this._animatedStyles.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;
}
}
this._animatedStyles.forEach(style => {
style.viewDescriptors.add({
tag: viewTag,
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,
shadowNodeWrapper: shadowNodeWrapper
});
}
}
componentDidUpdate(prevProps, _prevState, snapshot) {
this._configureLayoutAnimation(LayoutAnimationType.LAYOUT, this.props.layout, prevProps.layout);
this._configureLayoutAnimation(LayoutAnimationType.EXITING, this.props.exiting, prevProps.exiting);
this._NativeEventsManager?.updateEvents(prevProps);
this._attachAnimatedStyles();
this._InlinePropManager.attachInlineProps(this, this._getViewInfo());
if (IS_WEB && this.props.exiting && this._componentDOMRef) {
saveSnapshot(this._componentDOMRef);
}
if (IS_WEB && snapshot && this.props.layout && !getReducedMotionFromConfig(this.props.layout)) {
tryActivateLayoutTransition(this.props, this._componentDOMRef, snapshot);
}
}
_updateStyles(props) {
const filtered = filterStyles(flattenArray(props.style ?? []));
this._prevAnimatedStyles = this._animatedStyles;
this._animatedStyles = filtered.animatedStyles;
this._cssStyle = filtered.cssStyle;
}
_configureLayoutAnimation(type, currentConfig, previousConfig) {
if (IS_WEB || currentConfig === previousConfig) {
return;
}
if (this._isReducedMotion(currentConfig)) {
if (!previousConfig) {
return;
}
currentConfig = undefined;
}
updateLayoutAnimations(type === LayoutAnimationType.ENTERING ? this.reanimatedID : this.getComponentViewTag(), type, currentConfig && maybeBuild(currentConfig, type === LayoutAnimationType.LAYOUT ? undefined /* We don't have to warn user if style has common properties with animation for LAYOUT */ : this.props?.style, this._displayName));
}
_isReducedMotion(config) {
return config && 'getReduceMotion' in config && typeof config.getReduceMotion === 'function' ? getReduceMotionFromConfig(config.getReduceMotion()) : getReduceMotionFromConfig();
}
// 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.props.layout && this._componentDOMRef?.getBoundingClientRect) {
return this._componentDOMRef.getBoundingClientRect();
}
// `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;
}
// 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 skipEntering = this.context?.current;
const nativeID = skipEntering ? undefined : `${this.reanimatedID}`;
const jestProps = IS_JEST ? {
jestInlineStyle: this.props.style && filterOutAnimatedStyles(this.props.style),
jestAnimatedStyle: this.jestAnimatedStyle,
jestAnimatedProps: this.jestAnimatedProps
} : {};
return super.render({
nativeID,
...filteredProps,
...jestProps
});
}
}
function filterOutAnimatedStyles(style) {
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;
});
}
//# sourceMappingURL=AnimatedComponent.js.map