chore: add mobile app

This commit is contained in:
2026-02-07 22:33:02 +01:00
parent db34640acc
commit e89384ad4e
29327 changed files with 3886944 additions and 12 deletions
@@ -0,0 +1,460 @@
'use strict';
import '../layoutReanimation/animationsManager';
import type React from 'react';
import { maybeBuild } from '../animationBuilder';
import { IS_JEST, IS_WEB, logger } from '../common';
import type { StyleProps } from '../commonTypes';
import { LayoutAnimationType } from '../commonTypes';
import { SkipEnteringContext } from '../component/LayoutAnimationConfig';
import ReanimatedAnimatedComponent from '../css/component/AnimatedComponent';
import type { AnimatedStyleHandle } from '../hook/commonTypes';
import {
configureWebLayoutAnimations,
getReducedMotionFromConfig,
saveSnapshot,
startWebLayoutAnimation,
tryActivateLayoutTransition,
} from '../layoutReanimation/web';
import type { CustomConfig } from '../layoutReanimation/web/config';
import { addHTMLMutationObserver } from '../layoutReanimation/web/domUtils';
import type { ReanimatedHTMLElement } from '../ReanimatedModule/js-reanimated';
import { updateLayoutAnimations } from '../UpdateLayoutAnimations';
import type {
AnimatedComponentProps,
AnimatedComponentRef,
AnimatedProps,
AnyComponent,
IAnimatedComponentInternal,
INativeEventsManager,
InitialComponentProps,
LayoutAnimationOrBuilder,
NestedArray,
} from './commonTypes';
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 type Options<P> = {
setNativeProps?: (ref: AnimatedComponentRef, props: P) => void;
jsProps?: string[];
};
export default class AnimatedComponent
extends ReanimatedAnimatedComponent<
AnimatedComponentProps<InitialComponentProps>
>
implements IAnimatedComponentInternal
{
_options?: Options<InitialComponentProps>;
_displayName: string;
_animatedStyles: StyleProps[] = [];
_prevAnimatedStyles: StyleProps[] = [];
_animatedProps: Partial<AnimatedComponentProps<AnimatedProps>>[] = [];
_prevAnimatedProps: Partial<AnimatedComponentProps<AnimatedProps>>[] = [];
_isFirstRender = true;
jestInlineStyle: NestedArray<StyleProps> | undefined;
jestAnimatedStyle: { value: StyleProps } = { value: {} };
jestAnimatedProps: { value: AnimatedProps } = { value: {} };
_InlinePropManager = new InlinePropManager();
_PropsFilter = new PropsFilter();
_NativeEventsManager?: INativeEventsManager;
static contextType = SkipEnteringContext;
context!: React.ContextType<typeof SkipEnteringContext>;
reanimatedID = id++;
constructor(
ChildComponent: AnyComponent,
props: AnimatedComponentProps<InitialComponentProps>,
displayName: string,
options?: Options<InitialComponentProps>
) {
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 as ReanimatedHTMLElement;
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 as CustomConfig)
) {
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 as CustomConfig)
) {
addHTMLMutationObserver();
startWebLayoutAnimation(
this.props,
this._componentDOMRef as ReanimatedHTMLElement,
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: StyleProps) {
if (this._options?.setNativeProps) {
this._options.setNativeProps(
this._componentRef as AnimatedComponentRef,
props
);
} else {
(this._componentRef as AnimatedComponentRef)?.setNativeProps?.(props);
}
}
_handleAnimatedStylesUpdate(
prevStyles: StyleProps[],
currentStyles: StyleProps[],
jestAnimatedStyleOrProps: { value: StyleProps }
) {
const { viewTag, shadowNodeWrapper } = this._getViewInfo();
const newStyles = new Set<StyleProps>(currentStyles);
const isStyleAttached = (style: StyleProps) =>
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: AnimatedComponentProps<InitialComponentProps>,
_prevState: Readonly<unknown>,
snapshot: DOMRect | null
) {
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 as CustomConfig)
) {
tryActivateLayoutTransition(
this.props,
this._componentDOMRef as ReanimatedHTMLElement,
snapshot
);
}
}
_updateStyles(props: AnimatedComponentProps<InitialComponentProps>): void {
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: LayoutAnimationType,
currentConfig: LayoutAnimationOrBuilder | undefined,
previousConfig?: LayoutAnimationOrBuilder
) {
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 as CustomConfig)
) {
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: NestedArray<StyleProps | AnimatedStyleHandle | null | undefined>
): NestedArray<StyleProps | null | undefined> {
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;
});
}
@@ -0,0 +1,176 @@
'use strict';
import type { StyleProps } from '../commonTypes';
import { isSharedValue } from '../isSharedValue';
import { startMapper, stopMapper } from '../mappers';
import { updateProps } from '../updateProps';
import type { ViewDescriptorsSet } from '../ViewDescriptorsSet';
import { makeViewDescriptorsSet } from '../ViewDescriptorsSet';
import type {
AnimatedComponentProps,
AnimatedComponentType,
IInlinePropManager,
ViewInfo,
} from './commonTypes';
import { flattenArray } from './utils';
function isInlineStyleTransform(transform: unknown): boolean {
if (!Array.isArray(transform)) {
return false;
}
return transform.some((t: Record<string, unknown>) => hasInlineStyles(t));
}
function inlinePropsHasChanged(
styles1: StyleProps,
styles2: StyleProps
): boolean {
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(styleValue: StyleProps): unknown {
'worklet';
if (isSharedValue(styleValue)) {
return styleValue.value;
}
if (Array.isArray(styleValue)) {
return styleValue.map(getInlinePropsUpdate);
}
if (styleValue && typeof styleValue === 'object') {
const update: Record<string, unknown> = {};
for (const [key, value] of Object.entries(styleValue)) {
update[key] = getInlinePropsUpdate(value);
}
return update;
}
return styleValue;
}
function extractSharedValuesMapFromProps(
props: AnimatedComponentProps<
Record<string, unknown> /* Initial component props */
>
): Record<string, unknown> {
const inlineProps: Record<string, unknown> = {};
for (const key in props) {
const value = props[key];
if (key === 'style') {
const styles = flattenArray<StyleProps>(props.style ?? []);
styles.forEach((style) => {
if (!style) {
return;
}
for (const [styleKey, styleValue] of Object.entries(style)) {
if (isSharedValue(styleValue)) {
inlineProps[styleKey] = styleValue;
} else if (
styleKey === 'transform' &&
isInlineStyleTransform(styleValue)
) {
inlineProps[styleKey] = styleValue;
}
}
});
} else if (isSharedValue(value)) {
inlineProps[key] = value;
}
}
return inlineProps;
}
export function hasInlineStyles(style: StyleProps): boolean {
if (!style) {
return false;
}
return Object.keys(style).some((key) => {
const styleValue = style[key];
return (
isSharedValue(styleValue) ||
(key === 'transform' && isInlineStyleTransform(styleValue))
);
});
}
export function getInlineStyle(
style: Record<string, unknown>,
isFirstRender: boolean
) {
if (isFirstRender) {
return getInlinePropsUpdate(style) as Record<string, unknown>;
}
const newStyle: StyleProps = {};
for (const [key, styleValue] of Object.entries(style)) {
if (
!isSharedValue(styleValue) &&
!(key === 'transform' && isInlineStyleTransform(styleValue))
) {
newStyle[key] = styleValue;
}
}
return newStyle;
}
export class InlinePropManager implements IInlinePropManager {
_inlinePropsViewDescriptors: ViewDescriptorsSet | null = null;
_inlinePropsMapperId: number | null = null;
_inlineProps: StyleProps = {};
public attachInlineProps(
animatedComponent: AnimatedComponentType,
viewInfo: ViewInfo
) {
const newInlineProps: Record<string, unknown> =
extractSharedValuesMapFromProps(animatedComponent.props);
const hasChanged = inlinePropsHasChanged(newInlineProps, this._inlineProps);
if (hasChanged) {
if (!this._inlinePropsViewDescriptors) {
this._inlinePropsViewDescriptors = makeViewDescriptorsSet();
const { viewTag, shadowNodeWrapper } = viewInfo;
this._inlinePropsViewDescriptors.add({
tag: viewTag as number,
shadowNodeWrapper: shadowNodeWrapper!,
});
}
const shareableViewDescriptors =
this._inlinePropsViewDescriptors.shareableViewDescriptors;
const updaterFunction = () => {
'worklet';
const update = getInlinePropsUpdate(newInlineProps);
updateProps(shareableViewDescriptors, update);
};
this._inlineProps = newInlineProps;
if (this._inlinePropsMapperId) {
stopMapper(this._inlinePropsMapperId);
}
this._inlinePropsMapperId = null;
if (Object.keys(newInlineProps).length) {
this._inlinePropsMapperId = startMapper(
updaterFunction,
Object.values(newInlineProps)
);
}
}
}
public detachInlineProps() {
if (this._inlinePropsMapperId) {
stopMapper(this._inlinePropsMapperId);
}
}
}
@@ -0,0 +1,88 @@
'use strict';
import { runOnUI } from 'react-native-worklets';
import { SHOULD_BE_USE_WEB } from '../common';
import type {
AnimatedComponentProps,
AnimatedComponentType,
IAnimatedComponentInternal,
IJSPropsUpdater,
InitialComponentProps,
JSPropsOperation,
} from './commonTypes';
class JSPropsUpdaterNative implements IJSPropsUpdater {
private static _tagToComponentMapping = new Map<
number,
AnimatedComponentType
>();
public registerComponent(
animatedComponent: AnimatedComponentType,
jsProps: string[]
) {
const viewTag = animatedComponent.getComponentViewTag();
JSPropsUpdaterNative._tagToComponentMapping.set(viewTag, animatedComponent);
runOnUI(() => {
global._tagToJSPropNamesMapping[viewTag] = Object.fromEntries(
jsProps.map((propName) => [propName, true])
);
})();
}
public unregisterComponent(animatedComponent: AnimatedComponentType) {
const viewTag = animatedComponent.getComponentViewTag();
JSPropsUpdaterNative._tagToComponentMapping.delete(viewTag);
runOnUI(() => {
delete global._tagToJSPropNamesMapping[viewTag];
})();
}
public updateProps(operations: JSPropsOperation[]) {
operations.forEach(({ tag, updates }) => {
const component = JSPropsUpdaterNative._tagToComponentMapping.get(tag);
component?.setNativeProps(updates);
});
}
}
class JSPropsUpdaterWeb implements IJSPropsUpdater {
public registerComponent(
_animatedComponent: React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
IAnimatedComponentInternal
) {
// noop
}
public unregisterComponent(
_animatedComponent: React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
IAnimatedComponentInternal
) {
// noop
}
public updateProps(_operations: JSPropsOperation[]) {
// noop
}
}
type JSPropsUpdaterOptions =
| typeof JSPropsUpdaterWeb
| typeof JSPropsUpdaterNative;
let JSPropsUpdater: JSPropsUpdaterOptions;
if (SHOULD_BE_USE_WEB) {
JSPropsUpdater = JSPropsUpdaterWeb;
} else {
JSPropsUpdater = JSPropsUpdaterNative;
}
const jsPropsUpdater = new JSPropsUpdater();
export default jsPropsUpdater;
@@ -0,0 +1,150 @@
'use strict';
import { findNodeHandle } from '../platformFunctions/findNodeHandle';
import { WorkletEventHandler } from '../WorkletEventHandler';
import type {
AnimatedComponentProps,
AnimatedComponentRef,
INativeEventsManager,
InitialComponentProps,
ManagedAnimatedComponent,
} from './commonTypes';
import { has } from './utils';
export class NativeEventsManager implements INativeEventsManager {
readonly #managedComponent: ManagedAnimatedComponent;
readonly #componentOptions?: ComponentOptions;
#eventViewTag = -1;
constructor(component: ManagedAnimatedComponent, options?: ComponentOptions) {
this.#managedComponent = component;
this.#componentOptions = options;
this.#eventViewTag = this.getEventViewTag();
}
public attachEvents() {
executeForEachEventHandler(this.#managedComponent.props, (key, handler) => {
handler.registerForEvents(this.#eventViewTag, key);
});
}
public detachEvents() {
executeForEachEventHandler(
this.#managedComponent.props,
(_key, handler) => {
handler.unregisterFromEvents(this.#eventViewTag);
}
);
}
public updateEvents(
prevProps: AnimatedComponentProps<InitialComponentProps>
) {
const computedEventTag = this.getEventViewTag(true);
// If the event view tag changes, we need to completely re-mount all events
if (this.#eventViewTag !== computedEventTag) {
// Remove all bindings from previous props that ran on the old viewTag
executeForEachEventHandler(prevProps, (_key, handler) => {
handler.unregisterFromEvents(this.#eventViewTag);
});
// We don't need to unregister from current (new) props, because their events weren't registered yet
// Replace the view tag
this.#eventViewTag = computedEventTag;
// Attach the events with a new viewTag
this.attachEvents();
return;
}
executeForEachEventHandler(prevProps, (key, prevHandler) => {
const newProp = this.#managedComponent.props[key];
if (!newProp) {
// Prop got deleted
prevHandler.unregisterFromEvents(this.#eventViewTag);
} else if (
isWorkletEventHandler(newProp) &&
newProp.workletEventHandler !== prevHandler
) {
// Prop got changed
prevHandler.unregisterFromEvents(this.#eventViewTag);
newProp.workletEventHandler.registerForEvents(this.#eventViewTag);
}
});
executeForEachEventHandler(this.#managedComponent.props, (key, handler) => {
if (!prevProps[key]) {
// Prop got added
handler.registerForEvents(this.#eventViewTag);
}
});
}
private getEventViewTag(componentUpdate: boolean = false) {
// Get the tag for registering events - since the event emitting view can be nested inside the main component
const componentAnimatedRef = this.#managedComponent
._componentRef as AnimatedComponentRef & { __nativeTag?: number };
if (componentAnimatedRef?.getScrollableNode) {
/*
In most cases, getScrollableNode() returns a view tag, and findNodeHandle is not required.
However, to cover more exotic list cases, we will continue to use findNodeHandle
for consistency. For numerical values, findNodeHandle should return the value immediately,
as documented here: https://github.com/facebook/react/blob/91061073d57783c061889ac6720ef1ab7f0c2149/packages/react-native-renderer/src/ReactNativePublicCompat.js#L113
*/
const scrollableNode = componentAnimatedRef.getScrollableNode();
if (typeof scrollableNode === 'number') {
return scrollableNode;
}
return findNodeHandle(scrollableNode) ?? -1;
}
if (this.#componentOptions?.setNativeProps) {
// This case ensures backward compatibility with components that
// have their own setNativeProps method passed as an option.
return findNodeHandle(this.#managedComponent) ?? -1;
}
if (!componentUpdate) {
// On the first render of a component, we may already receive a resolved view tag.
return this.#managedComponent.getComponentViewTag();
}
if (componentAnimatedRef?.__nativeTag) {
return componentAnimatedRef.__nativeTag ?? -1;
}
/*
When a component is updated, a child could potentially change and have a different
view tag. This can occur with a GestureDetector component.
*/
return findNodeHandle(componentAnimatedRef) ?? -1;
}
}
function isWorkletEventHandler(
prop: unknown
): prop is WorkletEventHandlerHolder {
return (
has('workletEventHandler', prop) &&
prop.workletEventHandler instanceof WorkletEventHandler
);
}
function executeForEachEventHandler(
props: AnimatedComponentProps<InitialComponentProps>,
callback: (
key: string,
handler: InstanceType<typeof WorkletEventHandler>
) => void
) {
for (const key in props) {
const prop = props[key];
if (isWorkletEventHandler(prop)) {
callback(key, prop.workletEventHandler);
}
}
}
type ComponentOptions = {
setNativeProps?: (
ref: AnimatedComponentRef,
props: InitialComponentProps
) => void;
};
type WorkletEventHandlerHolder = {
workletEventHandler: InstanceType<typeof WorkletEventHandler>;
};
@@ -0,0 +1,101 @@
'use strict';
import { initialUpdaterRun } from '../animation';
import type { StyleProps } from '../commonTypes';
import type { AnimatedStyleHandle } from '../hook/commonTypes';
import { isSharedValue } from '../isSharedValue';
import { WorkletEventHandler } from '../WorkletEventHandler';
import type {
AnimatedComponentProps,
AnimatedComponentType,
AnimatedProps,
InitialComponentProps,
IPropsFilter,
} from './commonTypes';
import { getInlineStyle, hasInlineStyles } from './InlinePropManager';
import { flattenArray, has } from './utils';
function dummyListener() {
// empty listener we use to assign to listener properties for which animated
// event is used.
}
export class PropsFilter implements IPropsFilter {
private _initialPropsMap = new Map<AnimatedStyleHandle, StyleProps>();
public filterNonAnimatedProps(
component: AnimatedComponentType
): Record<string, unknown> {
const inputProps =
component.props as AnimatedComponentProps<InitialComponentProps>;
const props: Record<string, unknown> = {};
for (const key in inputProps) {
const value = inputProps[key];
if (key === 'style') {
const styleProp = inputProps.style;
const styles = flattenArray<StyleProps>(styleProp ?? []);
const processedStyle: StyleProps[] = styles.map((style) => {
if (style?.viewDescriptors) {
const handle = style as AnimatedStyleHandle;
if (component._isFirstRender) {
this._initialPropsMap.set(handle, {
...handle.initial.value,
...initialUpdaterRun(handle.initial.updater),
} as StyleProps);
}
return this._initialPropsMap.get(handle) ?? {};
} else if (hasInlineStyles(style)) {
return getInlineStyle(style, component._isFirstRender);
} else {
return style;
}
});
// keep styles as they were passed by the user
// it will help other libs to interpret styles correctly
props[key] = processedStyle;
} else if (key === 'animatedProps') {
const animatedPropsProp = inputProps.animatedProps;
const animatedPropsArray = flattenArray<
Partial<AnimatedComponentProps<AnimatedProps>>
>(animatedPropsProp ?? []);
animatedPropsArray.forEach((animatedProps) => {
if (animatedProps?.viewDescriptors && animatedProps.initial) {
Object.keys(animatedProps.initial.value).forEach(
(initialValueKey) => {
props[initialValueKey] =
animatedProps.initial?.value[initialValueKey];
}
);
}
});
} else if (
has('workletEventHandler', value) &&
value.workletEventHandler instanceof WorkletEventHandler
) {
if (value.workletEventHandler.eventNames.length > 0) {
value.workletEventHandler.eventNames.forEach((eventName) => {
props[eventName] = has('listeners', value.workletEventHandler)
? (
value.workletEventHandler.listeners as Record<string, unknown>
)[eventName]
: dummyListener;
});
} else {
props[key] = dummyListener;
}
} else if (isSharedValue(value)) {
if (component._isFirstRender) {
props[key] = value.value;
}
} else {
props[key] = value;
}
}
return props;
}
}
@@ -0,0 +1,170 @@
'use strict';
import type { Component, Ref, RefObject } from 'react';
import type {
AnimatedStyle,
EntryExitAnimationFunction,
ILayoutAnimationBuilder,
ShadowNodeWrapper,
SharedValue,
StyleProps,
StyleUpdaterContainer,
} from '../commonTypes';
import type { SkipEnteringContext } from '../component/LayoutAnimationConfig';
import type { BaseAnimationBuilder } from '../layoutReanimation';
import type { ViewDescriptorsSet } from '../ViewDescriptorsSet';
export interface AnimatedProps extends Record<string, unknown> {
viewDescriptors?: ViewDescriptorsSet;
initial?: SharedValue<StyleProps>;
styleUpdaterContainer?: StyleUpdaterContainer;
}
export interface ViewInfo {
viewTag: number | AnimatedComponentRef | HTMLElement | null;
shadowNodeWrapper: ShadowNodeWrapper | null;
// This is a React host instance view name which might differ from the
// Fabric component name. For clarity, we use the viewName property
// here and componentName in C++ after converting react viewName to
// Fabric component name.
// (see react/renderer/componentregistry/componentNameByReactViewName.cpp)
viewName?: string;
DOMElement?: HTMLElement | null;
}
export interface IInlinePropManager {
attachInlineProps(
animatedComponent: React.Component<unknown, unknown>,
viewInfo: ViewInfo
): void;
detachInlineProps(): void;
}
export type AnimatedComponentType = React.Component<unknown, unknown> &
IAnimatedComponentInternal;
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
export type PropUpdates = StyleProps | AnimatedStyle<any>;
export interface IPropsFilter {
filterNonAnimatedProps: (
component: AnimatedComponentType
) => Record<string, unknown>;
}
export type JSPropsOperation = {
tag: number;
updates: StyleProps;
};
export interface IJSPropsUpdater {
registerComponent(
animatedComponent: AnimatedComponentType,
jsProps: string[]
): void;
unregisterComponent(animatedComponent: AnimatedComponentType): void;
updateProps(operations: JSPropsOperation[]): void;
}
export interface INativeEventsManager {
attachEvents(): void;
detachEvents(): void;
updateEvents(prevProps: AnimatedComponentProps<InitialComponentProps>): void;
}
export type LayoutAnimationStaticContext = {
presetName: string;
};
export type AnimatedComponentProps<
P extends Record<string, unknown> = Record<string, unknown>,
> = P & {
ref?: Ref<Component>;
style?: NestedArray<StyleProps>;
animatedProps?: Partial<AnimatedComponentProps<AnimatedProps>>;
jestAnimatedValues?: RefObject<AnimatedProps>;
animatedStyle?: StyleProps;
layout?: (
| BaseAnimationBuilder
| ILayoutAnimationBuilder
| typeof BaseAnimationBuilder
) &
LayoutAnimationStaticContext;
entering?: (
| BaseAnimationBuilder
| typeof BaseAnimationBuilder
| EntryExitAnimationFunction
| Keyframe
) &
LayoutAnimationStaticContext;
exiting?: (
| BaseAnimationBuilder
| typeof BaseAnimationBuilder
| EntryExitAnimationFunction
| Keyframe
) &
LayoutAnimationStaticContext;
};
export type LayoutAnimationOrBuilder = (
| BaseAnimationBuilder
| typeof BaseAnimationBuilder
| EntryExitAnimationFunction
| Keyframe
| ILayoutAnimationBuilder
) &
LayoutAnimationStaticContext;
export interface AnimatedComponentRef extends Component {
setNativeProps?: (props: Record<string, unknown>) => void;
getScrollableNode?: () => AnimatedComponentRef;
getAnimatableRef?: () => AnimatedComponentRef;
// Case for SVG components on Web
elementRef?: React.RefObject<HTMLElement>;
}
export interface IAnimatedComponentInternalBase {
ChildComponent: AnyComponent;
_componentRef: AnimatedComponentRef | HTMLElement | null;
_hasAnimatedRef: boolean;
_viewInfo?: ViewInfo;
/**
* Used for Layout Animations and Animated Styles. It is not related to event
* handling.
*/
getComponentViewTag: () => number;
}
export interface IAnimatedComponentInternal
extends IAnimatedComponentInternalBase {
_animatedStyles: StyleProps[];
_prevAnimatedStyles: StyleProps[];
_animatedProps: Partial<AnimatedComponentProps<AnimatedProps>>[];
_prevAnimatedProps: Partial<AnimatedComponentProps<AnimatedProps>>[];
_isFirstRender: boolean;
jestInlineStyle: NestedArray<StyleProps> | undefined;
jestAnimatedStyle: { value: StyleProps };
jestAnimatedProps: { value: AnimatedProps };
_InlinePropManager: IInlinePropManager;
_PropsFilter: IPropsFilter;
/** Doesn't exist on web. */
_NativeEventsManager?: INativeEventsManager;
context: React.ContextType<typeof SkipEnteringContext>;
setNativeProps: (props: StyleProps) => void;
}
export type NestedArray<T> = T | NestedArray<T>[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyComponent = React.ComponentType<any>;
export interface InitialComponentProps extends Record<string, unknown> {
ref?: Ref<Component>;
collapsable?: boolean;
}
export type ManagedAnimatedComponent = React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
IAnimatedComponentInternal;
@@ -0,0 +1,98 @@
'use strict';
import type {
ComponentClass,
ComponentType,
FunctionComponent,
Ref,
} from 'react';
import React from 'react';
import type { FlatList, FlatListProps } from 'react-native';
import type { AnimatedProps } from '../helperTypes';
import type { Options } from './AnimatedComponent';
import AnimatedComponentImpl from './AnimatedComponent';
import type {
AnimatedComponentProps,
InitialComponentProps,
} from './commonTypes';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnimatableComponent<C extends ComponentType<any>> = C & {
jsProps?: string[];
};
/**
* Lets you create an Animated version of any React Native component.
*
* @param component - The component you want to make animatable.
* @returns A component that Reanimated is capable of animating.
* @see https://docs.swmansion.com/react-native-reanimated/docs/core/createAnimatedComponent
*/
// Don't change the order of overloads, since such a change breaks current behavior
export function createAnimatedComponent<P extends object>(
component: AnimatableComponent<FunctionComponent<P>>,
options?: Options<P>
): FunctionComponent<AnimatedProps<P>>;
export function createAnimatedComponent<P extends object>(
component: AnimatableComponent<ComponentClass<P>>,
options?: Options<P>
): ComponentClass<AnimatedProps<P>>;
export function createAnimatedComponent<P extends object>(
// Actually ComponentType<P = {}> = ComponentClass<P> | FunctionComponent<P> but we need this overload too
// since some external components (like FastImage) are typed just as ComponentType
component: AnimatableComponent<ComponentType<P>>,
options?: Options<P>
): FunctionComponent<AnimatedProps<P>> | ComponentClass<AnimatedProps<P>>;
/**
* @deprecated Please use `Animated.FlatList` component instead of calling
* `Animated.createAnimatedComponent(FlatList)` manually.
*/
// @ts-ignore This is required to create this overload, since type of createAnimatedComponent is incorrect and doesn't include typeof FlatList
export function createAnimatedComponent(
component: AnimatableComponent<typeof FlatList<unknown>>,
options?: Options<typeof FlatList<unknown>>
): ComponentClass<AnimatedProps<FlatListProps<unknown>>>;
export function createAnimatedComponent(
Component: AnimatableComponent<ComponentType<InitialComponentProps>>,
options?: Options<InitialComponentProps>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): any {
class AnimatedComponent extends AnimatedComponentImpl {
static displayName = `AnimatedComponent(${
Component.displayName || Component.name || 'Component'
})`;
constructor(props: AnimatedComponentProps<InitialComponentProps>) {
// User can override component-defined jsProps via options
const jsProps = options?.jsProps ?? Component.jsProps;
const modifiedOptions = jsProps?.length
? { ...options, jsProps }
: options;
super(Component, props, AnimatedComponent.displayName, modifiedOptions);
}
}
const animatedComponent = (
props: AnimatedComponentProps & { ref: Ref<AnimatedComponent> }
) => {
return (
<AnimatedComponent
{...props}
// Needed to prevent react from signing AnimatedComponent to the ref
// (we want to handle the ref assignment in the AnimatedComponent)
ref={null}
{...(props.ref === null ? null : { forwardedRef: props.ref })}
/>
);
};
animatedComponent.displayName =
Component.displayName || Component.name || 'Component';
return animatedComponent;
}
@@ -0,0 +1,15 @@
'use strict';
import type { HostInstance } from '../platform-specific/findHostInstance';
export function getViewInfo(element: HostInstance): {
viewName?: string;
viewTag?: number;
} {
return {
viewName: (element?._viewConfig?.uiViewClassName ??
element?.__internalInstanceHandle?.type ??
element?.__internalInstanceHandle?.elementType) as string,
viewTag: element?.__nativeTag,
};
}
@@ -0,0 +1,2 @@
'use strict';
export { createAnimatedComponent } from './createAnimatedComponent';
@@ -0,0 +1,60 @@
'use strict';
import type { StyleProps } from '../commonTypes';
import type { CSSStyle } from '../css';
import type { NestedArray } from './commonTypes';
export function flattenArray<T>(array: NestedArray<T>): T[] {
if (!Array.isArray(array)) {
return [array];
}
const resultArr: T[] = [];
const _flattenArray = (arr: NestedArray<T>[]): void => {
arr.forEach((item) => {
if (Array.isArray(item)) {
_flattenArray(item);
} else {
resultArr.push(item);
}
});
};
_flattenArray(array);
return resultArr;
}
export const has = <K extends string>(
key: K,
x: unknown
): x is { [key in K]: unknown } => {
if (typeof x === 'function' || typeof x === 'object') {
if (x === null || x === undefined) {
return false;
} else {
return key in x;
}
}
return false;
};
type FilteredStyles = {
cssStyle: CSSStyle | null;
animatedStyles: StyleProps[];
};
export function filterStyles(styles: StyleProps[] | undefined): FilteredStyles {
if (!styles) {
return { animatedStyles: [], cssStyle: null };
}
return styles.reduce<FilteredStyles>(
({ animatedStyles, cssStyle }, style) => {
if (style?.viewDescriptors) {
animatedStyles.push(style);
} else {
cssStyle = { ...cssStyle, ...style } as CSSStyle;
}
return { animatedStyles, cssStyle };
},
{ animatedStyles: [], cssStyle: null }
);
}