react-native-reanimated
Version:
More powerful alternative to Animated library for React Native.
542 lines (534 loc) • 23.4 kB
JavaScript
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
import React from 'react';
import { findNodeHandle, Platform, StyleSheet } from 'react-native';
import WorkletEventHandler from './reanimated2/WorkletEventHandler';
import setAndForwardRef from './setAndForwardRef';
import './reanimated2/layoutReanimation/animationsManager';
import invariant from 'invariant';
import { adaptViewConfig } from './ConfigHelper';
import { RNRenderer } from './reanimated2/platform-specific/RNRenderer';
import { configureLayoutAnimations, enableLayoutAnimations, startMapper, stopMapper } from './reanimated2/core';
import { isJest, isChromeDebugger, shouldBeUseWeb, isWeb } from './reanimated2/PlatformChecker';
import { initialUpdaterRun } from './reanimated2/animation';
import { SharedTransition, LayoutAnimationType } from './reanimated2/layoutReanimation';
import { makeViewDescriptorsSet } from './reanimated2/ViewDescriptorsSet';
import { getShadowNodeWrapperFromRef } from './reanimated2/fabricUtils';
import updateProps from './reanimated2/UpdateProps';
import NativeReanimatedModule from './reanimated2/NativeReanimated';
import { isSharedValue } from './reanimated2';
import { removeFromPropsRegistry } from './reanimated2/PropsRegistry';
function dummyListener() {
// empty listener we use to assign to listener properties for which animated
// event is used.
}
function maybeBuild(layoutAnimationOrBuilder) {
const isAnimationBuilder = value => 'build' in layoutAnimationOrBuilder && typeof layoutAnimationOrBuilder.build === 'function';
if (isAnimationBuilder(layoutAnimationOrBuilder)) {
return layoutAnimationOrBuilder.build();
} else {
return layoutAnimationOrBuilder;
}
}
function flattenArray(array) {
if (!Array.isArray(array)) {
return [array];
}
const resultArr = [];
const _flattenArray = arr => {
arr.forEach(item => {
if (Array.isArray(item)) {
_flattenArray(item);
} else {
resultArr.push(item);
}
});
};
_flattenArray(array);
return resultArr;
}
function onlyAnimatedStyles(styles) {
return styles.filter(style => style === null || style === void 0 ? void 0 : style.viewDescriptors);
}
function isSameAnimatedStyle(style1, style2) {
// We cannot use equality check to compare useAnimatedStyle outputs directly.
// Instead, we can compare its viewsRefs.
return (style1 === null || style1 === void 0 ? void 0 : style1.viewsRef) === (style2 === null || style2 === void 0 ? void 0 : style2.viewsRef);
}
const isSameAnimatedProps = isSameAnimatedStyle;
const has = (key, x) => {
if (typeof x === 'function' || typeof x === 'object') {
if (x === null || x === undefined) {
return false;
} else {
return key in x;
}
}
return false;
};
function isInlineStyleTransform(transform) {
if (!transform) {
return false;
}
return transform.some(t => hasInlineStyles(t));
}
function hasInlineStyles(style) {
if (!style) {
return false;
}
return Object.keys(style).some(key => {
const styleValue = style[key];
return isSharedValue(styleValue) || key === 'transform' && isInlineStyleTransform(styleValue);
});
}
function extractSharedValuesMapFromProps(props) {
const inlineProps = {};
for (const key in props) {
const value = props[key];
if (key === 'style') {
const styles = flattenArray(props.style ?? []);
styles.forEach(style => {
if (!style) {
return;
}
for (const [key, styleValue] of Object.entries(style)) {
if (isSharedValue(styleValue)) {
inlineProps[key] = styleValue;
} else if (key === 'transform' && isInlineStyleTransform(styleValue)) {
inlineProps[key] = styleValue;
}
}
});
} else if (isSharedValue(value)) {
inlineProps[key] = value;
}
}
return inlineProps;
}
function inlinePropsHasChanged(styles1, styles2) {
if (Object.keys(styles1).length !== Object.keys(styles2).length) {
return true;
}
for (const key of Object.keys(styles1)) {
if (styles1[key] !== styles2[key]) return true;
}
return false;
}
function getInlinePropsUpdate(inlineProps) {
'worklet';
const update = {};
for (const [key, styleValue] of Object.entries(inlineProps)) {
if (key === 'transform') {
update[key] = styleValue.map(transform => {
return getInlinePropsUpdate(transform);
});
} else if (isSharedValue(styleValue)) {
update[key] = styleValue.value;
} else {
update[key] = styleValue;
}
}
return update;
}
export default function createAnimatedComponent(Component, options) {
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 {
constructor(props) {
super(props);
_defineProperty(this, "_styles", null);
_defineProperty(this, "_animatedProps", void 0);
_defineProperty(this, "_viewTag", -1);
_defineProperty(this, "_isFirstRender", true);
_defineProperty(this, "animatedStyle", {
value: {}
});
_defineProperty(this, "initialStyle", {});
_defineProperty(this, "_component", null);
_defineProperty(this, "_inlinePropsViewDescriptors", null);
_defineProperty(this, "_inlinePropsMapperId", null);
_defineProperty(this, "_inlineProps", {});
_defineProperty(this, "_sharedElementTransition", null);
_defineProperty(this, "_setComponentRef", setAndForwardRef({
getForwardedRef: () => this.props.forwardedRef,
setLocalRef: ref => {
// TODO update config
const tag = findNodeHandle(ref);
const {
layout,
entering,
exiting,
sharedTransitionTag
} = this.props;
if ((layout || entering || exiting || sharedTransitionTag) && tag != null) {
if (!shouldBeUseWeb()) {
enableLayoutAnimations(true, false);
}
if (layout) {
configureLayoutAnimations(tag, LayoutAnimationType.LAYOUT, maybeBuild(layout));
}
if (entering) {
configureLayoutAnimations(tag, LayoutAnimationType.ENTERING, maybeBuild(entering));
}
if (exiting) {
configureLayoutAnimations(tag, LayoutAnimationType.EXITING, maybeBuild(exiting));
}
if (sharedTransitionTag) {
const sharedElementTransition = this.props.sharedTransitionStyle ?? new SharedTransition();
sharedElementTransition.registerTransition(tag, sharedTransitionTag);
this._sharedElementTransition = sharedElementTransition;
}
}
if (ref !== this._component) {
this._component = ref;
}
}
}));
if (isJest()) {
this.animatedStyle = {
value: {}
};
}
}
componentWillUnmount() {
var _this$_sharedElementT;
this._detachNativeEvents();
this._detachStyles();
this._detachInlineProps();
(_this$_sharedElementT = this._sharedElementTransition) === null || _this$_sharedElementT === void 0 ? void 0 : _this$_sharedElementT.unregisterTransition(this._viewTag);
}
componentDidMount() {
this._attachNativeEvents();
this._attachAnimatedStyles();
this._attachInlineProps();
}
_getEventViewRef() {
var _this$_component;
// Make sure to get the scrollable node for components that implement
// `ScrollResponder.Mixin`.
return (_this$_component = this._component) !== null && _this$_component !== void 0 && _this$_component.getScrollableNode ? this._component.getScrollableNode() : this._component;
}
_attachNativeEvents() {
const node = this._getEventViewRef();
let viewTag = null; // We set it only if needed
for (const key in this.props) {
const prop = this.props[key];
if (has('current', prop) && prop.current instanceof WorkletEventHandler) {
if (viewTag === null) {
viewTag = findNodeHandle(options !== null && options !== void 0 && options.setNativeProps ? this : node);
}
prop.current.registerForEvents(viewTag, key);
}
}
}
_detachNativeEvents() {
for (const key in this.props) {
const prop = this.props[key];
if (has('current', prop) && prop.current instanceof WorkletEventHandler) {
prop.current.unregisterFromEvents();
}
}
}
_detachStyles() {
if (isWeb() && this._styles !== null) {
for (const style of this._styles) {
if (style !== null && style !== void 0 && style.viewsRef) {
style.viewsRef.remove(this);
}
}
} else if (this._viewTag !== -1 && this._styles !== null) {
var _this$props$animatedP;
for (const style of this._styles) {
style.viewDescriptors.remove(this._viewTag);
}
if ((_this$props$animatedP = this.props.animatedProps) !== null && _this$props$animatedP !== void 0 && _this$props$animatedP.viewDescriptors) {
this.props.animatedProps.viewDescriptors.remove(this._viewTag);
}
if (global._IS_FABRIC) {
removeFromPropsRegistry(this._viewTag);
}
}
}
_reattachNativeEvents(prevProps) {
for (const key in prevProps) {
const prop = this.props[key];
if (has('current', prop) && prop.current instanceof WorkletEventHandler && prop.current.reattachNeeded) {
prop.current.unregisterFromEvents();
}
}
let viewTag = null;
for (const key in this.props) {
const prop = this.props[key];
if (has('current', prop) && prop.current instanceof WorkletEventHandler && prop.current.reattachNeeded) {
if (viewTag === null) {
const node = this._getEventViewRef();
viewTag = findNodeHandle(options !== null && options !== void 0 && options.setNativeProps ? this : node);
}
prop.current.registerForEvents(viewTag, key);
prop.current.reattachNeeded = false;
}
}
}
_updateFromNative(props) {
if (options !== null && options !== void 0 && options.setNativeProps) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
options.setNativeProps(this._component, props);
} else {
var _this$_component2, _this$_component2$set;
// eslint-disable-next-line no-unused-expressions
(_this$_component2 = this._component) === null || _this$_component2 === void 0 ? void 0 : (_this$_component2$set = _this$_component2.setNativeProps) === null || _this$_component2$set === void 0 ? void 0 : _this$_component2$set.call(_this$_component2, props);
}
}
_getViewInfo() {
var _this$_component3;
let viewTag;
let viewName;
let shadowNodeWrapper = 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$_component3 = this._component) !== null && _this$_component3 !== void 0 && _this$_component3.getAnimatableRef ? this._component.getAnimatableRef() : this;
if (isWeb()) {
viewTag = findNodeHandle(component);
viewName = null;
shadowNodeWrapper = null;
viewConfig = null;
} else {
var _hostInstance$viewCon;
// 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('Cannot find host instance for this component. Maybe it renders nothing?');
}
// we can access view tag in the same way it's accessed here https://github.com/facebook/react/blob/e3f4eb7272d4ca0ee49f27577156b57eeb07cf73/packages/react-native-renderer/src/ReactFabric.js#L146
viewTag = hostInstance === null || hostInstance === void 0 ? void 0 : hostInstance._nativeTag;
/**
* RN uses viewConfig for components for storing different properties of the component(example: https://github.com/facebook/react-native/blob/master/Libraries/Components/ScrollView/ScrollViewViewConfig.js#L16).
* The name we're looking for is in the field named uiViewClassName.
*/
viewName = hostInstance === null || hostInstance === void 0 ? void 0 : (_hostInstance$viewCon = hostInstance.viewConfig) === null || _hostInstance$viewCon === void 0 ? void 0 : _hostInstance$viewCon.uiViewClassName;
viewConfig = hostInstance === null || hostInstance === void 0 ? void 0 : hostInstance.viewConfig;
if (global._IS_FABRIC) {
shadowNodeWrapper = getShadowNodeWrapperFromRef(this);
}
}
return {
viewTag,
viewName,
shadowNodeWrapper,
viewConfig
};
}
_attachAnimatedStyles() {
var _this$props$animatedP2, _this$props$animatedP3;
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$animatedP2 = this.props.animatedProps) === null || _this$props$animatedP2 === void 0 ? void 0 : _this$props$animatedP2.viewDescriptors) || styles.length;
if (hasReanimated2Props && viewConfig) {
adaptViewConfig(viewConfig);
}
this._viewTag = viewTag;
// 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 && isSameAnimatedStyle(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 => isSameAnimatedStyle(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.animatedStyle.value = {
...this.animatedStyle.value,
...style.initial.value
};
style.animatedStyle.current = this.animatedStyle;
}
});
// detach old animatedProps
if (prevAnimatedProps && !isSameAnimatedProps(prevAnimatedProps, this.props.animatedProps)) {
prevAnimatedProps.viewDescriptors.remove(viewTag);
}
// attach animatedProps property
if ((_this$props$animatedP3 = this.props.animatedProps) !== null && _this$props$animatedP3 !== void 0 && _this$props$animatedP3.viewDescriptors) {
this.props.animatedProps.viewDescriptors.add({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
tag: viewTag,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
name: viewName,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
shadowNodeWrapper: shadowNodeWrapper
});
}
}
_attachInlineProps() {
const newInlineProps = extractSharedValuesMapFromProps(this.props);
const hasChanged = inlinePropsHasChanged(newInlineProps, this._inlineProps);
if (hasChanged) {
if (!this._inlinePropsViewDescriptors) {
this._inlinePropsViewDescriptors = makeViewDescriptorsSet();
const {
viewTag,
viewName,
shadowNodeWrapper,
viewConfig
} = this._getViewInfo();
if (Object.keys(newInlineProps).length && viewConfig) {
adaptViewConfig(viewConfig);
}
this._inlinePropsViewDescriptors.add({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
tag: viewTag,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
name: viewName,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
shadowNodeWrapper: shadowNodeWrapper
});
}
const sharableViewDescriptors = this._inlinePropsViewDescriptors.sharableViewDescriptors;
const maybeViewRef = NativeReanimatedModule.native ? undefined : {
items: new Set([this])
}; // see makeViewsRefSet
const updaterFunction = () => {
'worklet';
const update = getInlinePropsUpdate(newInlineProps);
updateProps(sharableViewDescriptors, update, maybeViewRef);
};
this._inlineProps = newInlineProps;
if (this._inlinePropsMapperId) {
stopMapper(this._inlinePropsMapperId);
}
this._inlinePropsMapperId = null;
if (Object.keys(newInlineProps).length) {
this._inlinePropsMapperId = startMapper(updaterFunction, Object.values(newInlineProps));
}
}
}
_detachInlineProps() {
if (this._inlinePropsMapperId) {
stopMapper(this._inlinePropsMapperId);
}
}
componentDidUpdate(prevProps) {
this._reattachNativeEvents(prevProps);
this._attachAnimatedStyles();
this._attachInlineProps();
}
_filterNonAnimatedProps(inputProps) {
const props = {};
for (const key in inputProps) {
const value = inputProps[key];
if (key === 'style') {
const styleProp = inputProps.style;
const styles = flattenArray(styleProp ?? []);
const processedStyle = styles.map(style => {
if (style && style.viewDescriptors) {
// this is how we recognize styles returned by useAnimatedStyle
style.viewsRef.add(this);
if (this._isFirstRender) {
this.initialStyle = {
...style.initial.value,
...this.initialStyle,
...initialUpdaterRun(style.initial.updater)
};
}
return this.initialStyle;
} else if (hasInlineStyles(style)) {
if (this._isFirstRender) {
return getInlinePropsUpdate(style);
}
const newStyle = {};
for (const [key, styleValue] of Object.entries(style)) {
if (!isSharedValue(styleValue) && !(key === 'transform' && isInlineStyleTransform(styleValue))) {
newStyle[key] = styleValue;
}
}
return newStyle;
} else {
return style;
}
});
props[key] = StyleSheet.flatten(processedStyle);
} else if (key === 'animatedProps') {
const animatedProp = inputProps.animatedProps;
if (animatedProp.initial !== undefined) {
Object.keys(animatedProp.initial.value).forEach(key => {
var _animatedProp$initial, _animatedProp$viewsRe;
props[key] = (_animatedProp$initial = animatedProp.initial) === null || _animatedProp$initial === void 0 ? void 0 : _animatedProp$initial.value[key];
(_animatedProp$viewsRe = animatedProp.viewsRef) === null || _animatedProp$viewsRe === void 0 ? void 0 : _animatedProp$viewsRe.add(this);
});
}
} else if (has('current', value) && value.current instanceof WorkletEventHandler) {
if (value.current.eventNames.length > 0) {
value.current.eventNames.forEach(eventName => {
props[eventName] = has('listeners', value.current) ? value.current.listeners[eventName] : dummyListener;
});
} else {
props[key] = dummyListener;
}
} else if (isSharedValue(value)) {
if (this._isFirstRender) {
props[key] = value.value;
}
} else if (key !== 'onGestureHandlerStateChange' || !isChromeDebugger()) {
props[key] = value;
}
}
return props;
}
render() {
const props = this._filterNonAnimatedProps(this.props);
if (isJest()) {
props.animatedStyle = this.animatedStyle;
}
if (this._isFirstRender) {
this._isFirstRender = false;
}
const platformProps = Platform.select({
web: {},
default: {
collapsable: false
}
});
return /*#__PURE__*/React.createElement(Component, _extends({}, props, {
ref: this._setComponentRef
}, platformProps));
}
}
_defineProperty(AnimatedComponent, "displayName", void 0);
AnimatedComponent.displayName = `AnimatedComponent(${Component.displayName || Component.name || 'Component'})`;
return /*#__PURE__*/React.forwardRef((props, ref) => {
return /*#__PURE__*/React.createElement(AnimatedComponent, _extends({}, props, ref === null ? null : {
forwardedRef: ref
}));
});
}
//# sourceMappingURL=createAnimatedComponent.js.map