UNPKG

22.4 kBTypeScriptView Raw
1'use strict';
2import type {
3 Component,
4 ComponentClass,
5 ComponentType,
6 FunctionComponent,
7 MutableRefObject,
8} from 'react';
9import React from 'react';
10import { findNodeHandle, Platform } from 'react-native';
11import { WorkletEventHandler } from '../reanimated2/WorkletEventHandler';
12import '../reanimated2/layoutReanimation/animationsManager';
13import invariant from 'invariant';
14import { adaptViewConfig } from '../ConfigHelper';
15import { RNRenderer } from '../reanimated2/platform-specific/RNRenderer';
16import { enableLayoutAnimations } from '../reanimated2/core';
17import {
18 SharedTransition,
19 LayoutAnimationType,
20} from '../reanimated2/layoutReanimation';
21import type { StyleProps, ShadowNodeWrapper } from '../reanimated2/commonTypes';
22import { getShadowNodeWrapperFromRef } from '../reanimated2/fabricUtils';
23import { removeFromPropsRegistry } from '../reanimated2/PropsRegistry';
24import { getReduceMotionFromConfig } from '../reanimated2/animation/util';
25import { maybeBuild } from '../animationBuilder';
26import { SkipEnteringContext } from '../reanimated2/component/LayoutAnimationConfig';
27import type { AnimateProps } from '../reanimated2';
28import JSPropsUpdater from './JSPropsUpdater';
29import type {
30 AnimatedComponentProps,
31 AnimatedProps,
32 InitialComponentProps,
33 AnimatedComponentRef,
34 IAnimatedComponentInternal,
35 ViewInfo,
36} from './commonTypes';
37import { has, flattenArray } from './utils';
38import setAndForwardRef from './setAndForwardRef';
39import {
40 isFabric,
41 isJest,
42 isWeb,
43 shouldBeUseWeb,
44} from '../reanimated2/PlatformChecker';
45import { InlinePropManager } from './InlinePropManager';
46import { PropsFilter } from './PropsFilter';
47import {
48 startWebLayoutAnimation,
49 tryActivateLayoutTransition,
50 configureWebLayoutAnimations,
51 getReducedMotionFromConfig,
52 saveSnapshot,
53} from '../reanimated2/layoutReanimation/web';
54import { updateLayoutAnimations } from '../reanimated2/UpdateLayoutAnimations';
55import type { CustomConfig } from '../reanimated2/layoutReanimation/web/config';
56import type { FlatList, FlatListProps } from 'react-native';
57import { addHTMLMutationObserver } from '../reanimated2/layoutReanimation/web/domUtils';
58import { getViewInfo } from './getViewInfo';
59
60const IS_WEB = isWeb();
61
62if (IS_WEB) {
63 configureWebLayoutAnimations();
64}
65
66function onlyAnimatedStyles(styles: StyleProps[]): StyleProps[] {
67 return styles.filter((style) => style?.viewDescriptors);
68}
69
70type Options<P> = {
71 setNativeProps: (ref: AnimatedComponentRef, props: P) => void;
72};
73
74/**
75 * Lets you create an Animated version of any React Native component.
76 *
77 * @param component - The component you want to make animatable.
78 * @returns A component that Reanimated is capable of animating.
79 * @see https://docs.swmansion.com/react-native-reanimated/docs/core/createAnimatedComponent
80 */
81
82// Don't change the order of overloads, since such a change breaks current behavior
83export function createAnimatedComponent<P extends object>(
84 component: FunctionComponent<P>,
85 options?: Options<P>
86): FunctionComponent<AnimateProps<P>>;
87
88export function createAnimatedComponent<P extends object>(
89 component: ComponentClass<P>,
90 options?: Options<P>
91): ComponentClass<AnimateProps<P>>;
92
93export function createAnimatedComponent<P extends object>(
94 // Actually ComponentType<P = {}> = ComponentClass<P> | FunctionComponent<P> but we need this overload too
95 // since some external components (like FastImage) are typed just as ComponentType
96 component: ComponentType<P>,
97 options?: Options<P>
98): FunctionComponent<AnimateProps<P>> | ComponentClass<AnimateProps<P>>;
99
100/**
101 * @deprecated Please use `Animated.FlatList` component instead of calling `Animated.createAnimatedComponent(FlatList)` manually.
102 */
103// @ts-ignore This is required to create this overload, since type of createAnimatedComponent is incorrect and doesn't include typeof FlatList
104export function createAnimatedComponent(
105 component: typeof FlatList<unknown>,
106 options?: Options<any>
107): ComponentClass<AnimateProps<FlatListProps<unknown>>>;
108
109export function createAnimatedComponent(
110 Component: ComponentType<InitialComponentProps>,
111 options?: Options<InitialComponentProps>
112): any {
113 invariant(
114 typeof Component !== 'function' ||
115 (Component.prototype && Component.prototype.isReactComponent),
116 `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.`
117 );
118
119 class AnimatedComponent
120 extends React.Component<AnimatedComponentProps<InitialComponentProps>>
121 implements IAnimatedComponentInternal
122 {
123 _styles: StyleProps[] | null = null;
124 _animatedProps?: Partial<AnimatedComponentProps<AnimatedProps>>;
125 _componentViewTag = -1;
126 _eventViewTag = -1;
127 _isFirstRender = true;
128 jestAnimatedStyle: { value: StyleProps } = { value: {} };
129 _component: AnimatedComponentRef | HTMLElement | null = null;
130 _sharedElementTransition: SharedTransition | null = null;
131 _jsPropsUpdater = new JSPropsUpdater();
132 _InlinePropManager = new InlinePropManager();
133 _PropsFilter = new PropsFilter();
134 _viewInfo?: ViewInfo;
135 static displayName: string;
136 static contextType = SkipEnteringContext;
137 context!: React.ContextType<typeof SkipEnteringContext>;
138
139 constructor(props: AnimatedComponentProps<InitialComponentProps>) {
140 super(props);
141 if (isJest()) {
142 this.jestAnimatedStyle = { value: {} };
143 }
144 }
145
146 componentDidMount() {
147 this._setComponentViewTag();
148 this._setEventViewTag();
149 this._attachNativeEvents();
150 this._jsPropsUpdater.addOnJSPropsChangeListener(this);
151 this._attachAnimatedStyles();
152 this._InlinePropManager.attachInlineProps(this, this._getViewInfo());
153
154 const layout = this.props.layout;
155 if (layout) {
156 this._configureLayoutTransition();
157 }
158
159 if (IS_WEB) {
160 if (this.props.exiting) {
161 saveSnapshot(this._component as HTMLElement);
162 }
163
164 if (
165 !this.props.entering ||
166 getReducedMotionFromConfig(this.props.entering as CustomConfig)
167 ) {
168 this._isFirstRender = false;
169 return;
170 }
171
172 startWebLayoutAnimation(
173 this.props,
174 this._component as HTMLElement,
175 LayoutAnimationType.ENTERING
176 );
177 }
178
179 this._isFirstRender = false;
180 }
181
182 componentWillUnmount() {
183 this._detachNativeEvents();
184 this._jsPropsUpdater.removeOnJSPropsChangeListener(this);
185 this._detachStyles();
186 this._InlinePropManager.detachInlineProps();
187 if (this.props.sharedTransitionTag) {
188 this._configureSharedTransition(true);
189 }
190 this._sharedElementTransition?.unregisterTransition(
191 this._componentViewTag,
192 true
193 );
194
195 const exiting = this.props.exiting;
196 if (
197 IS_WEB &&
198 this._component &&
199 this.props.exiting &&
200 !getReducedMotionFromConfig(this.props.exiting as CustomConfig)
201 ) {
202 addHTMLMutationObserver();
203
204 startWebLayoutAnimation(
205 this.props,
206 this._component as HTMLElement,
207 LayoutAnimationType.EXITING
208 );
209 } else if (exiting) {
210 const reduceMotionInExiting =
211 'getReduceMotion' in exiting &&
212 typeof exiting.getReduceMotion === 'function'
213 ? getReduceMotionFromConfig(exiting.getReduceMotion())
214 : getReduceMotionFromConfig();
215 if (!reduceMotionInExiting) {
216 updateLayoutAnimations(
217 this._componentViewTag,
218 LayoutAnimationType.EXITING,
219 maybeBuild(
220 exiting,
221 this.props?.style,
222 AnimatedComponent.displayName
223 )
224 );
225 }
226 }
227 }
228
229 _setComponentViewTag() {
230 this._componentViewTag = this._getViewInfo().viewTag as number;
231 }
232
233 _setEventViewTag() {
234 // Setting the tag for registering events - since the event emitting view can be nested inside the main component
235 const componentAnimatedRef = this._component as AnimatedComponentRef;
236 if (componentAnimatedRef.getScrollableNode) {
237 const scrollableNode = componentAnimatedRef.getScrollableNode();
238 this._eventViewTag = findNodeHandle(scrollableNode) ?? -1;
239 } else {
240 this._eventViewTag =
241 findNodeHandle(
242 options?.setNativeProps ? this : componentAnimatedRef
243 ) ?? -1;
244 }
245 }
246
247 _attachNativeEvents() {
248 for (const key in this.props) {
249 const prop = this.props[key];
250 if (
251 has('workletEventHandler', prop) &&
252 prop.workletEventHandler instanceof WorkletEventHandler
253 ) {
254 prop.workletEventHandler.registerForEvents(this._eventViewTag, key);
255 }
256 }
257 }
258
259 _detachNativeEvents() {
260 for (const key in this.props) {
261 const prop = this.props[key];
262 if (
263 has('workletEventHandler', prop) &&
264 prop.workletEventHandler instanceof WorkletEventHandler
265 ) {
266 prop.workletEventHandler.unregisterFromEvents(this._eventViewTag);
267 }
268 }
269 }
270
271 _detachStyles() {
272 if (IS_WEB && this._styles !== null) {
273 for (const style of this._styles) {
274 style.viewsRef.remove(this);
275 }
276 } else if (this._componentViewTag !== -1 && this._styles !== null) {
277 for (const style of this._styles) {
278 style.viewDescriptors.remove(this._componentViewTag);
279 }
280 if (this.props.animatedProps?.viewDescriptors) {
281 this.props.animatedProps.viewDescriptors.remove(
282 this._componentViewTag
283 );
284 }
285 if (isFabric()) {
286 removeFromPropsRegistry(this._componentViewTag);
287 }
288 }
289 }
290
291 _updateNativeEvents(
292 prevProps: AnimatedComponentProps<InitialComponentProps>
293 ) {
294 for (const key in prevProps) {
295 const prevProp = prevProps[key];
296 if (
297 has('workletEventHandler', prevProp) &&
298 prevProp.workletEventHandler instanceof WorkletEventHandler
299 ) {
300 const newProp = this.props[key];
301 if (!newProp) {
302 // Prop got deleted
303 prevProp.workletEventHandler.unregisterFromEvents(
304 this._eventViewTag
305 );
306 } else if (
307 has('workletEventHandler', newProp) &&
308 newProp.workletEventHandler instanceof WorkletEventHandler &&
309 newProp.workletEventHandler !== prevProp.workletEventHandler
310 ) {
311 // Prop got changed
312 prevProp.workletEventHandler.unregisterFromEvents(
313 this._eventViewTag
314 );
315 newProp.workletEventHandler.registerForEvents(this._eventViewTag);
316 }
317 }
318 }
319
320 for (const key in this.props) {
321 const newProp = this.props[key];
322 if (
323 has('workletEventHandler', newProp) &&
324 newProp.workletEventHandler instanceof WorkletEventHandler &&
325 !prevProps[key]
326 ) {
327 // Prop got added
328 newProp.workletEventHandler.registerForEvents(this._eventViewTag);
329 }
330 }
331 }
332
333 _updateFromNative(props: StyleProps) {
334 if (options?.setNativeProps) {
335 options.setNativeProps(this._component as AnimatedComponentRef, props);
336 } else {
337 (this._component as AnimatedComponentRef)?.setNativeProps?.(props);
338 }
339 }
340
341 _getViewInfo(): ViewInfo {
342 if (this._viewInfo !== undefined) {
343 return this._viewInfo;
344 }
345
346 let viewTag: number | HTMLElement | null;
347 let viewName: string | null;
348 let shadowNodeWrapper: ShadowNodeWrapper | null = null;
349 let viewConfig;
350 // Component can specify ref which should be animated when animated version of the component is created.
351 // Otherwise, we animate the component itself.
352 const component = (this._component as AnimatedComponentRef)
353 ?.getAnimatableRef
354 ? (this._component as AnimatedComponentRef).getAnimatableRef?.()
355 : this;
356
357 if (IS_WEB) {
358 // At this point I assume that `_setComponentRef` was already called and `_component` is set.
359 // `this._component` on web represents HTMLElement of our component, that's why we use casting
360 viewTag = this._component as HTMLElement;
361 viewName = null;
362 shadowNodeWrapper = null;
363 viewConfig = null;
364 } else {
365 // 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
366 const hostInstance = RNRenderer.findHostInstance_DEPRECATED(component);
367 if (!hostInstance) {
368 throw new Error(
369 '[Reanimated] Cannot find host instance for this component. Maybe it renders nothing?'
370 );
371 }
372
373 const viewInfo = getViewInfo(hostInstance);
374 viewTag = viewInfo.viewTag;
375 viewName = viewInfo.viewName;
376 viewConfig = viewInfo.viewConfig;
377 shadowNodeWrapper = isFabric()
378 ? getShadowNodeWrapperFromRef(this)
379 : null;
380 }
381 this._viewInfo = { viewTag, viewName, shadowNodeWrapper, viewConfig };
382 return this._viewInfo;
383 }
384
385 _attachAnimatedStyles() {
386 const styles = this.props.style
387 ? onlyAnimatedStyles(flattenArray<StyleProps>(this.props.style))
388 : [];
389 const prevStyles = this._styles;
390 this._styles = styles;
391
392 const prevAnimatedProps = this._animatedProps;
393 this._animatedProps = this.props.animatedProps;
394
395 const { viewTag, viewName, shadowNodeWrapper, viewConfig } =
396 this._getViewInfo();
397
398 // update UI props whitelist for this view
399 const hasReanimated2Props =
400 this.props.animatedProps?.viewDescriptors || styles.length;
401 if (hasReanimated2Props && viewConfig) {
402 adaptViewConfig(viewConfig);
403 }
404
405 this._componentViewTag = viewTag as number;
406
407 // remove old styles
408 if (prevStyles) {
409 // in most of the cases, views have only a single animated style and it remains unchanged
410 const hasOneSameStyle =
411 styles.length === 1 &&
412 prevStyles.length === 1 &&
413 styles[0] === prevStyles[0];
414
415 if (!hasOneSameStyle) {
416 // otherwise, remove each style that is not present in new styles
417 for (const prevStyle of prevStyles) {
418 const isPresent = styles.some((style) => style === prevStyle);
419 if (!isPresent) {
420 prevStyle.viewDescriptors.remove(viewTag);
421 }
422 }
423 }
424 }
425
426 styles.forEach((style) => {
427 style.viewDescriptors.add({
428 tag: viewTag,
429 name: viewName,
430 shadowNodeWrapper,
431 });
432 if (isJest()) {
433 /**
434 * We need to connect Jest's TestObject instance whose contains just props object
435 * with the updateProps() function where we update the properties of the component.
436 * We can't update props object directly because TestObject contains a copy of props - look at render function:
437 * const props = this._filterNonAnimatedProps(this.props);
438 */
439 this.jestAnimatedStyle.value = {
440 ...this.jestAnimatedStyle.value,
441 ...style.initial.value,
442 };
443 style.jestAnimatedStyle.current = this.jestAnimatedStyle;
444 }
445 });
446
447 // detach old animatedProps
448 if (prevAnimatedProps && prevAnimatedProps !== this.props.animatedProps) {
449 prevAnimatedProps.viewDescriptors!.remove(viewTag as number);
450 }
451
452 // attach animatedProps property
453 if (this.props.animatedProps?.viewDescriptors) {
454 this.props.animatedProps.viewDescriptors.add({
455 tag: viewTag as number,
456 name: viewName!,
457 shadowNodeWrapper: shadowNodeWrapper!,
458 });
459 }
460 }
461
462 componentDidUpdate(
463 prevProps: AnimatedComponentProps<InitialComponentProps>,
464 _prevState: Readonly<unknown>,
465 // This type comes straight from React
466 // eslint-disable-next-line @typescript-eslint/no-explicit-any
467 snapshot: DOMRect | null
468 ) {
469 const layout = this.props.layout;
470 const oldLayout = prevProps.layout;
471 if (layout !== oldLayout) {
472 this._configureLayoutTransition();
473 }
474 if (
475 this.props.sharedTransitionTag !== undefined ||
476 prevProps.sharedTransitionTag !== undefined
477 ) {
478 this._configureSharedTransition();
479 }
480 this._updateNativeEvents(prevProps);
481 this._attachAnimatedStyles();
482 this._InlinePropManager.attachInlineProps(this, this._getViewInfo());
483
484 if (IS_WEB && this.props.exiting) {
485 saveSnapshot(this._component as HTMLElement);
486 }
487
488 // Snapshot won't be undefined because it comes from getSnapshotBeforeUpdate method
489 if (
490 IS_WEB &&
491 snapshot !== null &&
492 this.props.layout &&
493 !getReducedMotionFromConfig(this.props.layout as CustomConfig)
494 ) {
495 tryActivateLayoutTransition(
496 this.props,
497 this._component as HTMLElement,
498 snapshot
499 );
500 }
501 }
502
503 _configureLayoutTransition() {
504 const layout = this.props.layout
505 ? maybeBuild(
506 this.props.layout,
507 undefined /* We don't have to warn user if style has common properties with animation for LAYOUT */,
508 AnimatedComponent.displayName
509 )
510 : undefined;
511 updateLayoutAnimations(
512 this._componentViewTag,
513 LayoutAnimationType.LAYOUT,
514 layout
515 );
516 }
517
518 _configureSharedTransition(isUnmounting = false) {
519 if (IS_WEB) {
520 return;
521 }
522 const { sharedTransitionTag } = this.props;
523 if (!sharedTransitionTag) {
524 this._sharedElementTransition?.unregisterTransition(
525 this._componentViewTag,
526 isUnmounting
527 );
528 this._sharedElementTransition = null;
529 return;
530 }
531 const sharedElementTransition =
532 this.props.sharedTransitionStyle ??
533 this._sharedElementTransition ??
534 new SharedTransition();
535 sharedElementTransition.registerTransition(
536 this._componentViewTag,
537 sharedTransitionTag,
538 isUnmounting
539 );
540 this._sharedElementTransition = sharedElementTransition;
541 }
542
543 _setComponentRef = setAndForwardRef<Component | HTMLElement>({
544 getForwardedRef: () =>
545 this.props.forwardedRef as MutableRefObject<
546 Component<Record<string, unknown>, Record<string, unknown>, unknown>
547 >,
548 setLocalRef: (ref) => {
549 // TODO update config
550
551 const tag = IS_WEB
552 ? (ref as HTMLElement)
553 : findNodeHandle(ref as Component);
554
555 this._componentViewTag = tag as number;
556
557 const { layout, entering, exiting, sharedTransitionTag } = this.props;
558 if (
559 (layout || entering || exiting || sharedTransitionTag) &&
560 tag != null
561 ) {
562 if (!shouldBeUseWeb()) {
563 enableLayoutAnimations(true, false);
564 }
565
566 if (sharedTransitionTag) {
567 this._configureSharedTransition();
568 }
569
570 const skipEntering = this.context?.current;
571 if (entering && !skipEntering) {
572 updateLayoutAnimations(
573 tag as number,
574 LayoutAnimationType.ENTERING,
575 maybeBuild(
576 entering,
577 this.props?.style,
578 AnimatedComponent.displayName
579 )
580 );
581 }
582 }
583
584 if (ref !== this._component) {
585 this._component = ref;
586 }
587 },
588 });
589
590 // This is a component lifecycle method from React, therefore we are not calling it directly.
591 // It is called before the component gets rerendered. This way we can access components' position before it changed
592 // and later on, in componentDidUpdate, calculate translation for layout transition.
593 getSnapshotBeforeUpdate() {
594 if (
595 IS_WEB &&
596 (this._component as HTMLElement)?.getBoundingClientRect !== undefined
597 ) {
598 return (this._component as HTMLElement).getBoundingClientRect();
599 }
600
601 return null;
602 }
603
604 render() {
605 const filteredProps = this._PropsFilter.filterNonAnimatedProps(this);
606
607 if (isJest()) {
608 filteredProps.jestAnimatedStyle = this.jestAnimatedStyle;
609 }
610
611 // Layout animations on web are set inside `componentDidMount` method, which is called after first render.
612 // Because of that we can encounter a situation in which component is visible for a short amount of time, and later on animation triggers.
613 // 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,
614 // I've decided to hide component at first render. Its visibility is reset in `componentDidMount`.
615 if (
616 this._isFirstRender &&
617 IS_WEB &&
618 filteredProps.entering &&
619 !getReducedMotionFromConfig(filteredProps.entering as CustomConfig)
620 ) {
621 filteredProps.style = {
622 ...(filteredProps.style ?? {}),
623 visibility: 'hidden', // Hide component until `componentDidMount` triggers
624 };
625 }
626
627 const platformProps = Platform.select({
628 web: {},
629 default: { collapsable: false },
630 });
631
632 return (
633 <Component
634 {...filteredProps}
635 // Casting is used here, because ref can be null - in that case it cannot be assigned to HTMLElement.
636 // After spending some time trying to figure out what to do with this problem, we decided to leave it this way
637 ref={this._setComponentRef as (ref: Component) => void}
638 {...platformProps}
639 />
640 );
641 }
642 }
643
644 AnimatedComponent.displayName = `AnimatedComponent(${
645 Component.displayName || Component.name || 'Component'
646 })`;
647
648 return React.forwardRef<Component>((props, ref) => {
649 return (
650 <AnimatedComponent
651 {...props}
652 {...(ref === null ? null : { forwardedRef: ref })}
653 />
654 );
655 });
656}
657
\No newline at end of file