465 lines
16 KiB
JavaScript
465 lines
16 KiB
JavaScript
import React from 'react';
|
|
import { findNodeHandle, Platform, StyleSheet } from 'react-native';
|
|
import ReanimatedEventEmitter from './ReanimatedEventEmitter';
|
|
|
|
import AnimatedEvent from './reanimated1/core/AnimatedEvent';
|
|
import AnimatedNode from './reanimated1/core/AnimatedNode';
|
|
import AnimatedValue from './reanimated1/core/AnimatedValue';
|
|
import { createOrReusePropsNode } from './reanimated1/core/AnimatedProps';
|
|
import WorkletEventHandler from './reanimated2/WorkletEventHandler';
|
|
import setAndForwardRef from './setAndForwardRef';
|
|
|
|
import invariant from 'fbjs/lib/invariant';
|
|
import { adaptViewConfig } from './ConfigHelper';
|
|
import { RNRenderer } from './reanimated2/platform-specific/RNRenderer';
|
|
|
|
const NODE_MAPPING = new Map();
|
|
|
|
function listener(data) {
|
|
const component = NODE_MAPPING.get(data.viewTag);
|
|
component && component._updateFromNative(data.props);
|
|
}
|
|
|
|
function dummyListener() {
|
|
// empty listener we use to assign to listener properties for which animated
|
|
// event is used.
|
|
}
|
|
|
|
function hasAnimatedNodes(value) {
|
|
if (value instanceof AnimatedNode) {
|
|
return true;
|
|
}
|
|
if (Array.isArray(value)) {
|
|
return value.some((item) => hasAnimatedNodes(item));
|
|
}
|
|
if (value && typeof value === 'object') {
|
|
return Object.keys(value).some((key) => hasAnimatedNodes(value[key]));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
export default function createAnimatedComponent(Component, options = {}) {
|
|
invariant(
|
|
typeof Component !== 'function' ||
|
|
(Component.prototype && Component.prototype.isReactComponent),
|
|
'`createAnimatedComponent` does not support stateless functional components; ' +
|
|
'use a class component instead.'
|
|
);
|
|
|
|
class AnimatedComponent extends React.Component {
|
|
_invokeAnimatedPropsCallbackOnMount = false;
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
this._attachProps(this.props);
|
|
if (process.env.JEST_WORKER_ID) {
|
|
this.animatedStyle = { value: {} };
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
this._detachPropUpdater();
|
|
this._propsAnimated && this._propsAnimated.__detach();
|
|
this._detachNativeEvents();
|
|
}
|
|
|
|
componentDidMount() {
|
|
if (this._invokeAnimatedPropsCallbackOnMount) {
|
|
this._invokeAnimatedPropsCallbackOnMount = false;
|
|
this._animatedPropsCallback();
|
|
}
|
|
|
|
this._propsAnimated && this._propsAnimated.setNativeView(this._component);
|
|
this._attachNativeEvents();
|
|
this._attachPropUpdater();
|
|
this._attachAnimatedStyles();
|
|
}
|
|
|
|
_getEventViewRef() {
|
|
// Make sure to get the scrollable node for components that implement
|
|
// `ScrollResponder.Mixin`.
|
|
return this._component.getScrollableNode
|
|
? this._component.getScrollableNode()
|
|
: this._component;
|
|
}
|
|
|
|
_attachNativeEvents() {
|
|
const node = this._getEventViewRef();
|
|
const viewTag = findNodeHandle(options.setNativeProps ? this : node);
|
|
|
|
for (const key in this.props) {
|
|
const prop = this.props[key];
|
|
if (prop instanceof AnimatedEvent) {
|
|
prop.attachEvent(node, key);
|
|
} else if (
|
|
prop?.current &&
|
|
prop.current instanceof WorkletEventHandler
|
|
) {
|
|
prop.current.registerForEvents(viewTag, key);
|
|
}
|
|
}
|
|
}
|
|
|
|
_detachNativeEvents() {
|
|
const node = this._getEventViewRef();
|
|
|
|
for (const key in this.props) {
|
|
const prop = this.props[key];
|
|
if (prop instanceof AnimatedEvent) {
|
|
prop.detachEvent(node, key);
|
|
} else if (
|
|
prop?.current &&
|
|
prop.current instanceof WorkletEventHandler
|
|
) {
|
|
prop.current.unregisterFromEvents();
|
|
}
|
|
}
|
|
}
|
|
|
|
_reattachNativeEvents(prevProps) {
|
|
const node = this._getEventViewRef();
|
|
const attached = new Set();
|
|
const nextEvts = new Set();
|
|
let viewTag;
|
|
|
|
for (const key in this.props) {
|
|
const prop = this.props[key];
|
|
if (prop instanceof AnimatedEvent) {
|
|
nextEvts.add(prop.__nodeID);
|
|
} else if (
|
|
prop?.current &&
|
|
prop.current instanceof WorkletEventHandler
|
|
) {
|
|
if (viewTag === undefined) {
|
|
viewTag = prop.current.viewTag;
|
|
}
|
|
}
|
|
}
|
|
for (const key in prevProps) {
|
|
const prop = this.props[key];
|
|
if (prop instanceof AnimatedEvent) {
|
|
if (!nextEvts.has(prop.__nodeID)) {
|
|
// event was in prev props but not in current props, we detach
|
|
prop.detachEvent(node, key);
|
|
} else {
|
|
// event was in prev and is still in current props
|
|
attached.add(prop.__nodeID);
|
|
}
|
|
} else if (
|
|
prop?.current &&
|
|
prop.current instanceof WorkletEventHandler &&
|
|
prop.current.reattachNeeded
|
|
) {
|
|
prop.current.unregisterFromEvents();
|
|
}
|
|
}
|
|
|
|
for (const key in this.props) {
|
|
const prop = this.props[key];
|
|
if (prop instanceof AnimatedEvent && !attached.has(prop.__nodeID)) {
|
|
// not yet attached
|
|
prop.attachEvent(node, key);
|
|
} else if (
|
|
prop?.current &&
|
|
prop.current instanceof WorkletEventHandler &&
|
|
prop.current.reattachNeeded
|
|
) {
|
|
prop.current.registerForEvents(viewTag, key);
|
|
prop.current.reattachNeeded = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// The system is best designed when setNativeProps is implemented. It is
|
|
// able to avoid re-rendering and directly set the attributes that changed.
|
|
// However, setNativeProps can only be implemented on native components
|
|
// If you want to animate a composite component, you need to re-render it.
|
|
// In this case, we have a fallback that uses forceUpdate.
|
|
_animatedPropsCallback = () => {
|
|
if (this._component == null) {
|
|
// AnimatedProps is created in will-mount because it's used in render.
|
|
// But this callback may be invoked before mount in async mode,
|
|
// In which case we should defer the setNativeProps() call.
|
|
// React may throw away uncommitted work in async mode,
|
|
// So a deferred call won't always be invoked.
|
|
this._invokeAnimatedPropsCallbackOnMount = true;
|
|
} else if (typeof this._component.setNativeProps !== 'function') {
|
|
this.forceUpdate();
|
|
} else {
|
|
this._component.setNativeProps(this._propsAnimated.__getValue());
|
|
}
|
|
};
|
|
|
|
_attachProps(nextProps) {
|
|
const oldPropsAnimated = this._propsAnimated;
|
|
|
|
this._propsAnimated = createOrReusePropsNode(
|
|
nextProps,
|
|
this._animatedPropsCallback,
|
|
oldPropsAnimated
|
|
);
|
|
// If prop node has been reused we don't need to call into "__detach"
|
|
if (oldPropsAnimated !== this._propsAnimated) {
|
|
// When you call detach, it removes the element from the parent list
|
|
// of children. If it goes to 0, then the parent also detaches itself
|
|
// and so on.
|
|
// An optimization is to attach the new elements and THEN detach the old
|
|
// ones instead of detaching and THEN attaching.
|
|
// This way the intermediate state isn't to go to 0 and trigger
|
|
// this expensive recursive detaching to then re-attach everything on
|
|
// the very next operation.
|
|
oldPropsAnimated && oldPropsAnimated.__detach();
|
|
}
|
|
}
|
|
|
|
_updateFromNative(props) {
|
|
if (options.setNativeProps) {
|
|
options.setNativeProps(this._component, props);
|
|
} else {
|
|
// eslint-disable-next-line no-unused-expressions
|
|
this._component.setNativeProps?.(props);
|
|
}
|
|
}
|
|
|
|
_attachPropUpdater() {
|
|
const viewTag = findNodeHandle(this);
|
|
NODE_MAPPING.set(viewTag, this);
|
|
if (NODE_MAPPING.size === 1) {
|
|
ReanimatedEventEmitter.addListener('onReanimatedPropsChange', listener);
|
|
}
|
|
}
|
|
|
|
_attachAnimatedStyles() {
|
|
let styles = Array.isArray(this.props.style)
|
|
? this.props.style
|
|
: [this.props.style];
|
|
styles = flattenArray(styles);
|
|
let viewTag, viewName;
|
|
if (Platform.OS === 'web') {
|
|
viewTag = findNodeHandle(this);
|
|
viewName = null;
|
|
} else {
|
|
// 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(this);
|
|
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?._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?.viewConfig?.uiViewClassName;
|
|
// update UI props whitelist for this view
|
|
if (
|
|
hostInstance &&
|
|
this._hasReanimated2Props(styles) &&
|
|
hostInstance.viewConfig
|
|
) {
|
|
adaptViewConfig(hostInstance.viewConfig);
|
|
}
|
|
}
|
|
|
|
styles.forEach((style) => {
|
|
if (style?.viewDescriptor) {
|
|
style.viewDescriptor.value = { tag: viewTag, name: viewName };
|
|
if (process.env.JEST_WORKER_ID) {
|
|
/**
|
|
* 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,
|
|
};
|
|
style.animatedStyle.current = this.animatedStyle;
|
|
}
|
|
}
|
|
});
|
|
// attach animatedProps property
|
|
if (this.props.animatedProps?.viewDescriptor) {
|
|
this.props.animatedProps.viewDescriptor.value = {
|
|
tag: viewTag,
|
|
name: viewName,
|
|
};
|
|
}
|
|
}
|
|
|
|
_hasReanimated2Props(flattenStyles) {
|
|
if (this.props.animatedProps?.viewDescriptor) {
|
|
return true;
|
|
}
|
|
if (this.props.style) {
|
|
for (const style of flattenStyles) {
|
|
// eslint-disable-next-line no-prototype-builtins
|
|
if (style?.hasOwnProperty('viewDescriptor')) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
_detachPropUpdater() {
|
|
const viewTag = findNodeHandle(this);
|
|
NODE_MAPPING.delete(viewTag);
|
|
if (NODE_MAPPING.size === 0) {
|
|
ReanimatedEventEmitter.removeAllListeners('onReanimatedPropsChange');
|
|
}
|
|
}
|
|
|
|
componentDidUpdate(prevProps) {
|
|
this._attachProps(this.props);
|
|
this._reattachNativeEvents(prevProps);
|
|
|
|
this._propsAnimated && this._propsAnimated.setNativeView(this._component);
|
|
}
|
|
|
|
_setComponentRef = setAndForwardRef({
|
|
getForwardedRef: () => this.props.forwardedRef,
|
|
setLocalRef: (ref) => {
|
|
if (ref !== this._component) {
|
|
this._component = ref;
|
|
}
|
|
|
|
// TODO: Delete this after React Native also deletes this deprecation helper.
|
|
if (ref != null && ref.getNode == null) {
|
|
ref.getNode = () => {
|
|
console.warn(
|
|
'%s: Calling %s on the ref of an Animated component ' +
|
|
'is no longer necessary. You can now directly use the ref ' +
|
|
'instead. This method will be removed in a future release.',
|
|
ref.constructor.name ?? '<<anonymous>>',
|
|
'getNode()'
|
|
);
|
|
return ref;
|
|
};
|
|
}
|
|
},
|
|
});
|
|
|
|
_filterNonAnimatedStyle(inputStyle) {
|
|
const style = {};
|
|
for (const key in inputStyle) {
|
|
const value = inputStyle[key];
|
|
if (!hasAnimatedNodes(value)) {
|
|
style[key] = value;
|
|
} else if (value instanceof AnimatedValue) {
|
|
// if any style in animated component is set directly to the `Value` we set those styles to the first value of `Value` node in order
|
|
// to avoid flash of default styles when `Value` is being asynchrounously sent via bridge and initialized in the native side.
|
|
style[key] = value._startingValue;
|
|
}
|
|
}
|
|
return style;
|
|
}
|
|
|
|
_filterNonAnimatedProps(inputProps) {
|
|
const props = {};
|
|
for (const key in inputProps) {
|
|
const value = inputProps[key];
|
|
if (key === 'style') {
|
|
const styles = Array.isArray(value) ? value : [value];
|
|
const processedStyle = styles.map((style) => {
|
|
if (style && style.viewDescriptor) {
|
|
// this is how we recognize styles returned by useAnimatedStyle
|
|
if (style.viewRef.current === null) {
|
|
style.viewRef.current = this;
|
|
}
|
|
return style.initial;
|
|
} else {
|
|
return style;
|
|
}
|
|
});
|
|
props[key] = this._filterNonAnimatedStyle(
|
|
StyleSheet.flatten(processedStyle)
|
|
);
|
|
} else if (key === 'animatedProps') {
|
|
Object.keys(value.initial).forEach((key) => {
|
|
props[key] = value.initial[key];
|
|
if (value.viewRef.current === null) {
|
|
value.viewRef.current = this;
|
|
}
|
|
});
|
|
} else if (value instanceof AnimatedEvent) {
|
|
// we cannot filter out event listeners completely as some components
|
|
// rely on having a callback registered in order to generate events
|
|
// alltogether. Therefore we provide a dummy callback here to allow
|
|
// native event dispatcher to hijack events.
|
|
props[key] = dummyListener;
|
|
} else if (
|
|
value?.current &&
|
|
value.current instanceof WorkletEventHandler
|
|
) {
|
|
if (value.current.eventNames.length > 0) {
|
|
value.current.eventNames.forEach((eventName) => {
|
|
props[eventName] = value.current.listeners
|
|
? value.current.listeners[eventName]
|
|
: dummyListener;
|
|
});
|
|
} else {
|
|
props[key] = dummyListener;
|
|
}
|
|
} else if (!(value instanceof AnimatedNode)) {
|
|
props[key] = value;
|
|
} else if (value instanceof AnimatedValue) {
|
|
// if any prop in animated component is set directly to the `Value` we set those props to the first value of `Value` node in order
|
|
// to avoid default values for a short moment when `Value` is being asynchrounously sent via bridge and initialized in the native side.
|
|
props[key] = value._startingValue;
|
|
}
|
|
}
|
|
return props;
|
|
}
|
|
|
|
render() {
|
|
const props = this._filterNonAnimatedProps(this.props);
|
|
if (process.env.JEST_WORKER_ID) {
|
|
props.animatedStyle = this.animatedStyle;
|
|
}
|
|
|
|
const platformProps = Platform.select({
|
|
web: {},
|
|
default: { collapsable: false },
|
|
});
|
|
return (
|
|
<Component {...props} ref={this._setComponentRef} {...platformProps} />
|
|
);
|
|
}
|
|
}
|
|
|
|
AnimatedComponent.displayName = `AnimatedComponent(${
|
|
Component.displayName || Component.name || 'Component'
|
|
})`;
|
|
|
|
return React.forwardRef(function AnimatedComponentWrapper(props, ref) {
|
|
return (
|
|
<AnimatedComponent
|
|
{...props}
|
|
{...(ref == null ? null : { forwardedRef: ref })}
|
|
/>
|
|
);
|
|
});
|
|
}
|