Files
Schnappix/node_modules/react-native-reanimated/lib/module/createAnimatedComponent/AnimatedComponent.js
T

282 lines
12 KiB
JavaScript

'use strict';
import '../layoutReanimation/animationsManager';
import { maybeBuild } from '../animationBuilder';
import { IS_JEST, IS_WEB, logger } from '../common';
import { LayoutAnimationType } from '../commonTypes';
import { SkipEnteringContext } from '../component/LayoutAnimationConfig';
import ReanimatedAnimatedComponent from '../css/component/AnimatedComponent';
import { configureWebLayoutAnimations, getReducedMotionFromConfig, saveSnapshot, startWebLayoutAnimation, tryActivateLayoutTransition } from '../layoutReanimation/web';
import { addHTMLMutationObserver } from '../layoutReanimation/web/domUtils';
import { updateLayoutAnimations } from '../UpdateLayoutAnimations';
import { InlinePropManager } from './InlinePropManager';
import jsPropsUpdater from './JSPropsUpdater';
import { NativeEventsManager } from './NativeEventsManager';
import { PropsFilter } from './PropsFilter';
import { filterStyles, flattenArray } from './utils';
let id = 0;
if (IS_WEB) {
configureWebLayoutAnimations();
}
export default class AnimatedComponent extends ReanimatedAnimatedComponent {
_animatedStyles = [];
_prevAnimatedStyles = [];
_animatedProps = [];
_prevAnimatedProps = [];
_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._updateAnimatedStylesAndProps();
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 && this._componentDOMRef) {
const element = this._componentDOMRef;
const dummyClone = element.dummyClone;
// If the element was cloned (because of the exiting animation), we need bring it
// back to the DOM
while (dummyClone?.firstChild) {
element.appendChild(dummyClone.firstChild);
}
delete element.dummyClone;
if (this.props.exiting) {
saveSnapshot(element);
}
if (!this.props.entering || getReducedMotionFromConfig(this.props.entering)) {
this._isFirstRender = false;
return;
}
const skipEntering = this.context?.current;
if (!skipEntering) {
startWebLayoutAnimation(this.props, element, LayoutAnimationType.ENTERING);
} else if (element.style) {
element.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);
}
}
_handleAnimatedStylesUpdate(prevStyles, currentStyles, jestAnimatedStyleOrProps) {
const {
viewTag,
shadowNodeWrapper
} = this._getViewInfo();
const newStyles = new Set(currentStyles);
const isStyleAttached = style => 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, _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._updateAnimatedStylesAndProps();
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 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, currentConfig, previousConfig) {
if (IS_WEB || currentConfig === previousConfig) {
return;
}
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));
}
// 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