create-expo-cljs-app
Version:
Create a react native application with Expo and Shadow-CLJS!
516 lines (515 loc) • 25.1 kB
JavaScript
import React from 'react';
import { findNodeHandle, Platform, StyleSheet } from 'react-native';
import ReanimatedEventEmitter from './ReanimatedEventEmitter';
// @ts-ignore JS file
import AnimatedEvent from './reanimated1/core/AnimatedEvent';
// @ts-ignore JS file
import AnimatedNode from './reanimated1/core/AnimatedNode';
// @ts-ignore JS file
import AnimatedValue from './reanimated1/core/AnimatedValue';
// @ts-ignore JS file
import { createOrReusePropsNode } from './reanimated1/core/AnimatedProps';
import WorkletEventHandler from './reanimated2/WorkletEventHandler';
import setAndForwardRef from './setAndForwardRef';
import './reanimated2/layoutReanimation/LayoutAnimationRepository';
import invariant from 'invariant';
import { adaptViewConfig } from './ConfigHelper';
import { RNRenderer } from './reanimated2/platform-specific/RNRenderer';
import { makeMutable, runOnUI, enableLayoutAnimations, } from './reanimated2/core';
import { DefaultEntering, DefaultExiting, DefaultLayout, } from './reanimated2/layoutReanimation/defaultAnimations/Default';
import { isJest, isChromeDebugger, shouldBeUseWeb, } from './reanimated2/PlatformChecker';
import { initialUpdaterRun } from './reanimated2/animation';
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;
}
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;
};
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 {
constructor(props) {
super(props);
this._invokeAnimatedPropsCallbackOnMount = false;
this._styles = null;
this._viewTag = -1;
this._isFirstRender = true;
this.animatedStyle = { value: {} };
this._component = null;
// 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.
this._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 {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this._component.setNativeProps(this._propsAnimated.__getValue());
}
};
this._setComponentRef = setAndForwardRef({
getForwardedRef: () => this.props.forwardedRef,
setLocalRef: (ref) => {
// TODO update config
const tag = findNodeHandle(ref);
if ((this.props.layout || this.props.entering || this.props.exiting) &&
tag != null) {
if (!shouldBeUseWeb()) {
enableLayoutAnimations(true, false);
}
let layout = this.props.layout ? this.props.layout : DefaultLayout;
let entering = this.props.entering
? this.props.entering
: DefaultEntering;
let exiting = this.props.exiting
? this.props.exiting
: DefaultExiting;
if (has('build', layout)) {
layout = layout.build();
}
if (has('build', entering)) {
entering = entering.build();
}
if (has('build', exiting)) {
exiting = exiting.build();
}
const config = {
layout,
entering,
exiting,
sv: this.sv,
};
runOnUI(() => {
'worklet';
global.LayoutAnimationRepository.registerConfig(tag, config);
})();
}
if (ref !== this._component) {
this._component = ref;
}
},
});
this._attachProps(this.props);
if (isJest()) {
this.animatedStyle = { value: {} };
}
this.sv = makeMutable({});
}
componentWillUnmount() {
this._detachPropUpdater();
this._propsAnimated && this._propsAnimated.__detach();
this._detachNativeEvents();
this._detachStyles();
this.sv = null;
}
componentDidMount() {
if (this._invokeAnimatedPropsCallbackOnMount) {
this._invokeAnimatedPropsCallbackOnMount = false;
this._animatedPropsCallback();
}
this._propsAnimated &&
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this._propsAnimated.setNativeView(this._component);
this._attachNativeEvents();
this._attachPropUpdater();
this._attachAnimatedStyles();
}
_getEventViewRef() {
var _a;
// Make sure to get the scrollable node for components that implement
// `ScrollResponder.Mixin`.
return ((_a = this._component) === null || _a === void 0 ? void 0 : _a.getScrollableNode) ? this._component.getScrollableNode()
: this._component;
}
_attachNativeEvents() {
const node = this._getEventViewRef();
const viewTag = findNodeHandle((options === null || options === void 0 ? void 0 : 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 (has('current', prop) &&
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 (has('current', prop) &&
prop.current instanceof WorkletEventHandler) {
prop.current.unregisterFromEvents();
}
}
}
_detachStyles() {
var _a;
if (Platform.OS === 'web' && this._styles !== null) {
for (const style of this._styles) {
if (style === null || style === void 0 ? void 0 : style.viewsRef) {
style.viewsRef.remove(this);
}
}
}
else if (this._viewTag !== -1 && this._styles !== null) {
for (const style of this._styles) {
style.viewDescriptors.remove(this._viewTag);
}
if ((_a = this.props.animatedProps) === null || _a === void 0 ? void 0 : _a.viewDescriptors) {
this.props.animatedProps.viewDescriptors.remove(this._viewTag);
}
}
}
_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 (has('current', prop) &&
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 (has('current', prop) &&
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 (has('current', prop) &&
prop.current instanceof WorkletEventHandler &&
prop.current.reattachNeeded) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
prop.current.registerForEvents(viewTag, key);
prop.current.reattachNeeded = false;
}
}
}
_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) {
var _a, _b;
if (options === null || options === void 0 ? void 0 : options.setNativeProps) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
options.setNativeProps(this._component, props);
}
else {
// eslint-disable-next-line no-unused-expressions
(_b = (_a = this._component) === null || _a === void 0 ? void 0 : _a.setNativeProps) === null || _b === void 0 ? void 0 : _b.call(_a, props);
}
}
_attachPropUpdater() {
const viewTag = findNodeHandle(this);
NODE_MAPPING.set(viewTag, this);
if (NODE_MAPPING.size === 1) {
ReanimatedEventEmitter.addListener('onReanimatedPropsChange', listener);
}
}
_attachAnimatedStyles() {
var _a, _b, _c;
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;
let viewTag;
let 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 === 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 = (_a = hostInstance === null || hostInstance === void 0 ? void 0 : hostInstance.viewConfig) === null || _a === void 0 ? void 0 : _a.uiViewClassName;
// update UI props whitelist for this view
const hasReanimated2Props = ((_b = this.props.animatedProps) === null || _b === void 0 ? void 0 : _b.viewDescriptors) || styles.length;
if (hasReanimated2Props && (hostInstance === null || hostInstance === void 0 ? void 0 : hostInstance.viewConfig)) {
adaptViewConfig(hostInstance.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 });
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 = Object.assign(Object.assign({}, 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 ((_c = this.props.animatedProps) === null || _c === void 0 ? void 0 : _c.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,
});
}
}
_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 &&
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this._propsAnimated.setNativeView(this._component);
this._attachAnimatedStyles();
}
_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 styleProp = inputProps.style;
const styles = flattenArray(styleProp !== null && styleProp !== void 0 ? 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) {
return Object.assign(Object.assign({}, style.initial.value), initialUpdaterRun(style.initial.updater));
}
else {
return style.initial.value;
}
}
else {
return style;
}
});
props[key] = this._filterNonAnimatedStyle(StyleSheet.flatten(processedStyle));
}
else if (key === 'animatedProps') {
const animatedProp = inputProps.animatedProps;
if (animatedProp.initial !== undefined) {
Object.keys(animatedProp.initial.value).forEach((key) => {
var _a, _b;
props[key] = (_a = animatedProp.initial) === null || _a === void 0 ? void 0 : _a.value[key];
(_b = animatedProp.viewsRef) === null || _b === void 0 ? void 0 : _b.add(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 (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 (!(value instanceof AnimatedNode)) {
if (key !== 'onGestureHandlerStateChange' || !isChromeDebugger()) {
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 (isJest()) {
props.animatedStyle = this.animatedStyle;
}
if (this._isFirstRender) {
this._isFirstRender = false;
}
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((props, ref) => {
return (<AnimatedComponent {...props} {...(ref === null ? null : { forwardedRef: ref })}/>);
});
}