feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,81 @@
'use strict';
import type { Extrapolate as _Extrapolate } from './reanimated2/interpolateColor';
import type { SharedValue as _SharedValue } from './reanimated2/commonTypes';
import type { DerivedValue as _DerivedValue } from './reanimated2/hook/useDerivedValue';
import type {
TransformStyleTypes as _TransformStyleTypes,
Adaptable as _Adaptable,
AdaptTransforms as _AdaptTransforms,
AnimatedTransform as _AnimatedTransform,
AnimateStyle as _AnimateStyle,
StylesOrDefault as _StylesOrDefault,
AnimateProps as _AnimateProps,
} from './reanimated2/helperTypes';
import type { EasingFunction as _EasingFunction } from './reanimated2/Easing';
import type { AnimatedScrollViewProps as _AnimatedScrollViewProps } from './reanimated2/component/ScrollView';
import type { FlatListPropsWithLayout as _FlatListPropsWithLayout } from './reanimated2/component/FlatList';
export { createAnimatedComponent } from './createAnimatedComponent';
export { AnimatedText as Text } from './reanimated2/component/Text';
export { AnimatedView as View } from './reanimated2/component/View';
export { AnimatedScrollView as ScrollView } from './reanimated2/component/ScrollView';
export { AnimatedImage as Image } from './reanimated2/component/Image';
export { ReanimatedFlatList as FlatList } from './reanimated2/component/FlatList';
export {
addWhitelistedNativeProps,
addWhitelistedUIProps,
} from './ConfigHelper';
/**
* @deprecated Please import `Extrapolate` directly from `react-native-reanimated` instead of `Animated` namespace.
*/
export type Extrapolate = typeof _Extrapolate;
/**
* @deprecated Please import `SharedValue` directly from `react-native-reanimated` instead of `Animated` namespace.
*/
export type SharedValue<T> = _SharedValue<T>;
/**
* @deprecated Please import `DerivedValue` directly from `react-native-reanimated` instead of `Animated` namespace.
*/
export type DerivedValue<T> = _DerivedValue<T>;
/**
* @deprecated Please import `Adaptable` directly from `react-native-reanimated` instead of `Animated` namespace.
*/
export type Adaptable<T> = _Adaptable<T>;
/**
* @deprecated Please import `TransformStyleTypes` directly from `react-native-reanimated` instead of `Animated` namespace.
* */
export type TransformStyleTypes = _TransformStyleTypes;
/**
* @deprecated Please import `AdaptTransforms` directly from `react-native-reanimated` instead of `Animated` namespace.
* */
export type AdaptTransforms<T> = _AdaptTransforms<T>;
/**
* @deprecated Please import `AnimatedTransform` directly from `react-native-reanimated` instead of `Animated` namespace.
*/
export type AnimatedTransform = _AnimatedTransform;
/**
* @deprecated Please import `AnimateStyle` directly from `react-native-reanimated` instead of `Animated` namespace.
* */
export type AnimateStyle<S> = _AnimateStyle<S>;
/**
* @deprecated Please import `StylesOrDefault` directly from `react-native-reanimated` instead of `Animated` namespace.
* */
export type StylesOrDefault<S> = _StylesOrDefault<S>;
/**
* @deprecated Please import `AnimateProps` directly from `react-native-reanimated` instead of `Animated` namespace.
* */
export type AnimateProps<P extends object> = _AnimateProps<P>;
/**
* @deprecated Please import `EasingFunction` directly from `react-native-reanimated` instead of `Animated` namespace.
* */
export type EasingFunction = _EasingFunction;
/**
* @deprecated Please import `AnimatedScrollViewProps` directly from `react-native-reanimated` instead of `Animated` namespace.
* */
export type AnimatedScrollViewProps = _AnimatedScrollViewProps;
/**
* @deprecated Please import `FlatListPropsWithLayout` directly from `react-native-reanimated` instead of `Animated` namespace.
* */
export type FlatListPropsWithLayout<T> = _FlatListPropsWithLayout<T>;

View File

@@ -0,0 +1,87 @@
'use strict';
import { PropsAllowlists } from './propsAllowlists';
import { jsiConfigureProps } from './reanimated2/core';
function assertNoOverlapInLists() {
for (const key in PropsAllowlists.NATIVE_THREAD_PROPS_WHITELIST) {
if (key in PropsAllowlists.UI_THREAD_PROPS_WHITELIST) {
throw new Error(
`[Reanimated] Property \`${key}\` was whitelisted both as UI and native prop. Please remove it from one of the lists.`
);
}
}
}
export function configureProps(): void {
assertNoOverlapInLists();
jsiConfigureProps(
Object.keys(PropsAllowlists.UI_THREAD_PROPS_WHITELIST),
Object.keys(PropsAllowlists.NATIVE_THREAD_PROPS_WHITELIST)
);
}
export function addWhitelistedNativeProps(
props: Record<string, boolean>
): void {
const oldSize = Object.keys(
PropsAllowlists.NATIVE_THREAD_PROPS_WHITELIST
).length;
PropsAllowlists.NATIVE_THREAD_PROPS_WHITELIST = {
...PropsAllowlists.NATIVE_THREAD_PROPS_WHITELIST,
...props,
};
if (
oldSize !==
Object.keys(PropsAllowlists.NATIVE_THREAD_PROPS_WHITELIST).length
) {
configureProps();
}
}
export function addWhitelistedUIProps(props: Record<string, boolean>): void {
const oldSize = Object.keys(PropsAllowlists.UI_THREAD_PROPS_WHITELIST).length;
PropsAllowlists.UI_THREAD_PROPS_WHITELIST = {
...PropsAllowlists.UI_THREAD_PROPS_WHITELIST,
...props,
};
if (
oldSize !== Object.keys(PropsAllowlists.UI_THREAD_PROPS_WHITELIST).length
) {
configureProps();
}
}
const PROCESSED_VIEW_NAMES = new Set();
export interface ViewConfig {
uiViewClassName: string;
validAttributes: Record<string, unknown>;
}
/**
* updates UI props whitelist for given view host instance
* this will work just once for every view name
*/
export function adaptViewConfig(viewConfig: ViewConfig): void {
const viewName = viewConfig.uiViewClassName;
const props = viewConfig.validAttributes;
// update whitelist of UI props for this view name only once
if (!PROCESSED_VIEW_NAMES.has(viewName)) {
const propsToAdd: Record<string, boolean> = {};
Object.keys(props).forEach((key) => {
// we don't want to add native props as they affect layout
// we also skip props which repeat here
if (
!(key in PropsAllowlists.NATIVE_THREAD_PROPS_WHITELIST) &&
!(key in PropsAllowlists.UI_THREAD_PROPS_WHITELIST)
) {
propsToAdd[key] = true;
}
});
addWhitelistedUIProps(propsToAdd);
PROCESSED_VIEW_NAMES.add(viewName);
}
}
configureProps();

View File

@@ -0,0 +1,104 @@
'use strict';
import type {
ILayoutAnimationBuilder,
LayoutAnimationFunction,
LayoutAnimationsValues,
} from './reanimated2/layoutReanimation';
import type { StyleProps } from './reanimated2/commonTypes';
import type { NestedArray } from './createAnimatedComponent/commonTypes';
const mockTargetValues: LayoutAnimationsValues = {
targetOriginX: 0,
targetOriginY: 0,
targetWidth: 0,
targetHeight: 0,
targetGlobalOriginX: 0,
targetGlobalOriginY: 0,
targetBorderRadius: 0,
windowWidth: 0,
windowHeight: 0,
currentOriginX: 0,
currentOriginY: 0,
currentWidth: 0,
currentHeight: 0,
currentGlobalOriginX: 0,
currentGlobalOriginY: 0,
currentBorderRadius: 0,
};
function getCommonProperties(
layoutStyle: StyleProps,
componentStyle: StyleProps | Array<StyleProps>
) {
let componentStyleFlat = Array.isArray(componentStyle)
? componentStyle.flat()
: [componentStyle];
componentStyleFlat = componentStyleFlat.filter(Boolean);
componentStyleFlat = componentStyleFlat.map((style) =>
'initial' in style
? style.initial.value // Include properties of animated style
: style
);
const componentStylesKeys = componentStyleFlat.flatMap((style) =>
Object.keys(style)
);
const commonKeys = Object.keys(layoutStyle).filter((key) =>
componentStylesKeys.includes(key)
);
return commonKeys;
}
function maybeReportOverwrittenProperties(
layoutAnimationStyle: StyleProps,
style: NestedArray<StyleProps>,
displayName: string
) {
const commonProperties = getCommonProperties(layoutAnimationStyle, style);
if (commonProperties.length > 0) {
console.warn(
`[Reanimated] ${
commonProperties.length === 1 ? 'Property' : 'Properties'
} "${commonProperties.join(
', '
)}" of ${displayName} may be overwritten by a layout animation. Please wrap your component with an animated view and apply the layout animation on the wrapper.`
);
}
}
export function maybeBuild(
layoutAnimationOrBuilder:
| ILayoutAnimationBuilder
| LayoutAnimationFunction
| Keyframe,
style: NestedArray<StyleProps> | undefined,
displayName: string
): LayoutAnimationFunction | Keyframe {
const isAnimationBuilder = (
value: ILayoutAnimationBuilder | LayoutAnimationFunction | Keyframe
): value is ILayoutAnimationBuilder =>
'build' in layoutAnimationOrBuilder &&
typeof layoutAnimationOrBuilder.build === 'function';
if (isAnimationBuilder(layoutAnimationOrBuilder)) {
const animationFactory = layoutAnimationOrBuilder.build();
if (__DEV__ && style) {
const layoutAnimation = animationFactory(mockTargetValues);
maybeReportOverwrittenProperties(
layoutAnimation.animations,
style,
displayName
);
}
return animationFactory;
} else {
return layoutAnimationOrBuilder;
}
}

View File

@@ -0,0 +1,191 @@
'use strict';
import type { StyleProps } from '../reanimated2';
import type {
IAnimatedComponentInternal,
AnimatedComponentProps,
IInlinePropManager,
ViewInfo,
} from './commonTypes';
import { flattenArray } from './utils';
import { makeViewDescriptorsSet } from '../reanimated2/ViewDescriptorsSet';
import type {
ViewDescriptorsSet,
ViewRefSet,
} from '../reanimated2/ViewDescriptorsSet';
import { adaptViewConfig } from '../ConfigHelper';
import updateProps from '../reanimated2/UpdateProps';
import { stopMapper, startMapper } from '../reanimated2/mappers';
import { isSharedValue } from '../reanimated2/isSharedValue';
import { shouldBeUseWeb } from '../reanimated2/PlatformChecker';
const SHOULD_BE_USE_WEB = shouldBeUseWeb();
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(inlineProps: Record<string, unknown>) {
'worklet';
const update: Record<string, unknown> = {};
for (const [key, styleValue] of Object.entries(inlineProps)) {
if (isSharedValue(styleValue)) {
update[key] = styleValue.value;
} else if (Array.isArray(styleValue)) {
update[key] = styleValue.map((item) => {
return getInlinePropsUpdate(item);
});
} else if (typeof styleValue === 'object') {
update[key] = getInlinePropsUpdate(styleValue as Record<string, unknown>);
} else {
update[key] = styleValue;
}
}
return update;
}
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>,
shouldGetInitialStyle: boolean
) {
if (shouldGetInitialStyle) {
return getInlinePropsUpdate(style);
}
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: React.Component<unknown, unknown> &
IAnimatedComponentInternal,
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, viewName, shadowNodeWrapper, viewConfig } = viewInfo;
if (Object.keys(newInlineProps).length && viewConfig) {
adaptViewConfig(viewConfig);
}
this._inlinePropsViewDescriptors.add({
tag: viewTag as number,
name: viewName!,
shadowNodeWrapper: shadowNodeWrapper!,
});
}
const shareableViewDescriptors =
this._inlinePropsViewDescriptors.shareableViewDescriptors;
const maybeViewRef = SHOULD_BE_USE_WEB
? ({ items: new Set([animatedComponent]) } as ViewRefSet<unknown>) // see makeViewsRefSet
: undefined;
const updaterFunction = () => {
'worklet';
const update = getInlinePropsUpdate(newInlineProps);
updateProps(shareableViewDescriptors, 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)
);
}
}
}
public detachInlineProps() {
if (this._inlinePropsMapperId) {
stopMapper(this._inlinePropsMapperId);
}
}
}

View File

@@ -0,0 +1,155 @@
'use strict';
import { NativeEventEmitter, Platform, findNodeHandle } from 'react-native';
import type { NativeModule } from 'react-native';
import { shouldBeUseWeb } from '../reanimated2/PlatformChecker';
import type { StyleProps } from '../reanimated2';
import { runOnJS, runOnUIImmediately } from '../reanimated2/threads';
import type {
AnimatedComponentProps,
IAnimatedComponentInternal,
IJSPropsUpdater,
InitialComponentProps,
} from './commonTypes';
import NativeReanimatedModule from '../specs/NativeReanimatedModule';
interface ListenerData {
viewTag: number;
props: StyleProps;
}
const SHOULD_BE_USE_WEB = shouldBeUseWeb();
class JSPropsUpdaterPaper implements IJSPropsUpdater {
private static _tagToComponentMapping = new Map();
private _reanimatedEventEmitter: NativeEventEmitter;
constructor() {
this._reanimatedEventEmitter = new NativeEventEmitter(
// NativeEventEmitter only uses this parameter on iOS.
Platform.OS === 'ios'
? (NativeReanimatedModule as unknown as NativeModule)
: undefined
);
}
public addOnJSPropsChangeListener(
animatedComponent: React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
IAnimatedComponentInternal
) {
const viewTag = findNodeHandle(animatedComponent);
JSPropsUpdaterPaper._tagToComponentMapping.set(viewTag, animatedComponent);
if (JSPropsUpdaterPaper._tagToComponentMapping.size === 1) {
const listener = (data: ListenerData) => {
const component = JSPropsUpdaterPaper._tagToComponentMapping.get(
data.viewTag
);
component?._updateFromNative(data.props);
};
this._reanimatedEventEmitter.addListener(
'onReanimatedPropsChange',
listener
);
}
}
public removeOnJSPropsChangeListener(
animatedComponent: React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
IAnimatedComponentInternal
) {
const viewTag = findNodeHandle(animatedComponent);
JSPropsUpdaterPaper._tagToComponentMapping.delete(viewTag);
if (JSPropsUpdaterPaper._tagToComponentMapping.size === 0) {
this._reanimatedEventEmitter.removeAllListeners(
'onReanimatedPropsChange'
);
}
}
}
class JSPropsUpdaterFabric implements IJSPropsUpdater {
private static _tagToComponentMapping = new Map();
private static isInitialized = false;
constructor() {
if (!JSPropsUpdaterFabric.isInitialized) {
const updater = (viewTag: number, props: unknown) => {
const component =
JSPropsUpdaterFabric._tagToComponentMapping.get(viewTag);
component?._updateFromNative(props);
};
runOnUIImmediately(() => {
'worklet';
global.updateJSProps = (viewTag: number, props: unknown) => {
runOnJS(updater)(viewTag, props);
};
})();
JSPropsUpdaterFabric.isInitialized = true;
}
}
public addOnJSPropsChangeListener(
animatedComponent: React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
IAnimatedComponentInternal
) {
if (!JSPropsUpdaterFabric.isInitialized) {
return;
}
const viewTag = findNodeHandle(animatedComponent);
JSPropsUpdaterFabric._tagToComponentMapping.set(viewTag, animatedComponent);
}
public removeOnJSPropsChangeListener(
animatedComponent: React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
IAnimatedComponentInternal
) {
if (!JSPropsUpdaterFabric.isInitialized) {
return;
}
const viewTag = findNodeHandle(animatedComponent);
JSPropsUpdaterFabric._tagToComponentMapping.delete(viewTag);
}
}
class JSPropsUpdaterWeb implements IJSPropsUpdater {
public addOnJSPropsChangeListener(
_animatedComponent: React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
IAnimatedComponentInternal
) {
// noop
}
public removeOnJSPropsChangeListener(
_animatedComponent: React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
IAnimatedComponentInternal
) {
// noop
}
}
type JSPropsUpdaterOptions =
| typeof JSPropsUpdaterWeb
| typeof JSPropsUpdaterFabric
| typeof JSPropsUpdaterPaper;
let JSPropsUpdater: JSPropsUpdaterOptions;
if (SHOULD_BE_USE_WEB) {
JSPropsUpdater = JSPropsUpdaterWeb;
} else if (global._IS_FABRIC) {
JSPropsUpdater = JSPropsUpdaterFabric;
} else {
JSPropsUpdater = JSPropsUpdaterPaper;
}
export default JSPropsUpdater;

View File

@@ -0,0 +1,26 @@
'use strict';
import type {
AnimatedComponentProps,
IAnimatedComponentInternal,
InitialComponentProps,
} from './commonTypes';
export default class JSPropsUpdaterWeb {
public addOnJSPropsChangeListener(
_animatedComponent: React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
IAnimatedComponentInternal
) {
// noop
}
public removeOnJSPropsChangeListener(
_animatedComponent: React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
IAnimatedComponentInternal
) {
// noop
}
}

View File

@@ -0,0 +1,117 @@
'use strict';
import { shallowEqual } from '../reanimated2/hook/utils';
import type { StyleProps } from '../reanimated2/commonTypes';
import { isSharedValue } from '../reanimated2/isSharedValue';
import { isChromeDebugger } from '../reanimated2/PlatformChecker';
import { WorkletEventHandler } from '../reanimated2/WorkletEventHandler';
import { initialUpdaterRun } from '../reanimated2/animation';
import { hasInlineStyles, getInlineStyle } from './InlinePropManager';
import type {
AnimatedComponentProps,
AnimatedProps,
InitialComponentProps,
IAnimatedComponentInternal,
IPropsFilter,
} from './commonTypes';
import { flattenArray, has } from './utils';
import { StyleSheet } from 'react-native';
function dummyListener() {
// empty listener we use to assign to listener properties for which animated
// event is used.
}
export class PropsFilter implements IPropsFilter {
private _initialStyle = {};
private _previousProps: React.Component['props'] | null = null;
private _requiresNewInitials = true;
public filterNonAnimatedProps(
component: React.Component<unknown, unknown> & IAnimatedComponentInternal
): Record<string, unknown> {
const inputProps =
component.props as AnimatedComponentProps<InitialComponentProps>;
this._maybePrepareForNewInitials(inputProps);
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 ?? []);
if (this._requiresNewInitials) {
this._initialStyle = {};
}
const processedStyle: StyleProps = styles.map((style) => {
if (style && style.viewDescriptors) {
// this is how we recognize styles returned by useAnimatedStyle
// TODO - refactor, since `viewsRef` is only present on Web
style.viewsRef?.add(component);
if (this._requiresNewInitials) {
this._initialStyle = {
...style.initial.value,
...this._initialStyle,
...initialUpdaterRun<StyleProps>(style.initial.updater),
};
}
return this._initialStyle;
} else if (hasInlineStyles(style)) {
return getInlineStyle(style, this._requiresNewInitials);
} else {
return style;
}
});
props[key] = StyleSheet.flatten(processedStyle);
} else if (key === 'animatedProps') {
const animatedProp = inputProps.animatedProps as Partial<
AnimatedComponentProps<AnimatedProps>
>;
if (animatedProp.initial !== undefined) {
Object.keys(animatedProp.initial.value).forEach((initialValueKey) => {
props[initialValueKey] =
animatedProp.initial?.value[initialValueKey];
// TODO - refacotr, since `viewsRef` is only present on Web
animatedProp.viewsRef?.add(component);
});
}
} 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 (this._requiresNewInitials) {
props[key] = value.value;
}
} else if (key !== 'onGestureHandlerStateChange' || !isChromeDebugger()) {
props[key] = value;
}
}
this._requiresNewInitials = false;
return props;
}
private _maybePrepareForNewInitials(
inputProps: AnimatedComponentProps<InitialComponentProps>
) {
if (this._previousProps && inputProps.style) {
this._requiresNewInitials = !shallowEqual(
this._previousProps,
inputProps
);
}
this._previousProps = inputProps;
}
}

View File

@@ -0,0 +1,116 @@
'use strict';
import type { Ref, Component } from 'react';
import type {
StyleProps,
BaseAnimationBuilder,
ILayoutAnimationBuilder,
EntryExitAnimationFunction,
SharedTransition,
SharedValue,
} from '../reanimated2';
import type {
ViewDescriptorsSet,
ViewRefSet,
} from '../reanimated2/ViewDescriptorsSet';
import type { SkipEnteringContext } from '../reanimated2/component/LayoutAnimationConfig';
import type { ShadowNodeWrapper } from '../reanimated2/commonTypes';
import type { ViewConfig } from '../ConfigHelper';
export interface AnimatedProps extends Record<string, unknown> {
viewDescriptors?: ViewDescriptorsSet;
viewsRef?: ViewRefSet<unknown>;
initial?: SharedValue<StyleProps>;
}
export interface ViewInfo {
viewTag: number | HTMLElement | null;
viewName: string | null;
shadowNodeWrapper: ShadowNodeWrapper | null;
viewConfig: ViewConfig;
}
export interface IInlinePropManager {
attachInlineProps(
animatedComponent: React.Component<unknown, unknown>,
viewInfo: ViewInfo
): void;
detachInlineProps(): void;
}
export interface IPropsFilter {
filterNonAnimatedProps: (
component: React.Component<unknown, unknown> & IAnimatedComponentInternal
) => Record<string, unknown>;
}
export interface IJSPropsUpdater {
addOnJSPropsChangeListener(
animatedComponent: React.Component<unknown, unknown> &
IAnimatedComponentInternal
): void;
removeOnJSPropsChangeListener(
animatedComponent: React.Component<unknown, unknown> &
IAnimatedComponentInternal
): void;
}
export type LayoutAnimationStaticContext = {
presetName: string;
};
export type AnimatedComponentProps<P extends Record<string, unknown>> = P & {
forwardedRef?: Ref<Component>;
style?: NestedArray<StyleProps>;
animatedProps?: Partial<AnimatedComponentProps<AnimatedProps>>;
animatedStyle?: StyleProps;
layout?: (
| BaseAnimationBuilder
| ILayoutAnimationBuilder
| typeof BaseAnimationBuilder
) &
LayoutAnimationStaticContext;
entering?: (
| BaseAnimationBuilder
| typeof BaseAnimationBuilder
| EntryExitAnimationFunction
| Keyframe
) &
LayoutAnimationStaticContext;
exiting?: (
| BaseAnimationBuilder
| typeof BaseAnimationBuilder
| EntryExitAnimationFunction
| Keyframe
) &
LayoutAnimationStaticContext;
sharedTransitionTag?: string;
sharedTransitionStyle?: SharedTransition;
};
export interface AnimatedComponentRef extends Component {
setNativeProps?: (props: Record<string, unknown>) => void;
getScrollableNode?: () => AnimatedComponentRef;
getAnimatableRef?: () => AnimatedComponentRef;
}
export interface IAnimatedComponentInternal {
_styles: StyleProps[] | null;
_animatedProps?: Partial<AnimatedComponentProps<AnimatedProps>>;
_viewTag: number;
_isFirstRender: boolean;
jestAnimatedStyle: { value: StyleProps };
_component: AnimatedComponentRef | HTMLElement | null;
_sharedElementTransition: SharedTransition | null;
_jsPropsUpdater: IJSPropsUpdater;
_InlinePropManager: IInlinePropManager;
_PropsFilter: IPropsFilter;
_viewInfo?: ViewInfo;
context: React.ContextType<typeof SkipEnteringContext>;
}
export type NestedArray<T> = T | NestedArray<T>[];
export interface InitialComponentProps extends Record<string, unknown> {
ref?: Ref<Component>;
collapsable?: boolean;
}

View File

@@ -0,0 +1,631 @@
'use strict';
import type {
Component,
ComponentClass,
ComponentType,
FunctionComponent,
MutableRefObject,
} from 'react';
import React from 'react';
import { findNodeHandle, Platform } from 'react-native';
import { WorkletEventHandler } from '../reanimated2/WorkletEventHandler';
import '../reanimated2/layoutReanimation/animationsManager';
import invariant from 'invariant';
import { adaptViewConfig } from '../ConfigHelper';
import { RNRenderer } from '../reanimated2/platform-specific/RNRenderer';
import { enableLayoutAnimations } from '../reanimated2/core';
import {
SharedTransition,
LayoutAnimationType,
} from '../reanimated2/layoutReanimation';
import type { StyleProps, ShadowNodeWrapper } from '../reanimated2/commonTypes';
import { getShadowNodeWrapperFromRef } from '../reanimated2/fabricUtils';
import { removeFromPropsRegistry } from '../reanimated2/PropsRegistry';
import { getReduceMotionFromConfig } from '../reanimated2/animation/util';
import { maybeBuild } from '../animationBuilder';
import { SkipEnteringContext } from '../reanimated2/component/LayoutAnimationConfig';
import type { AnimateProps } from '../reanimated2';
import JSPropsUpdater from './JSPropsUpdater';
import type {
AnimatedComponentProps,
AnimatedProps,
InitialComponentProps,
AnimatedComponentRef,
IAnimatedComponentInternal,
ViewInfo,
} from './commonTypes';
import { has, flattenArray } from './utils';
import setAndForwardRef from './setAndForwardRef';
import {
isFabric,
isJest,
isWeb,
shouldBeUseWeb,
} from '../reanimated2/PlatformChecker';
import { InlinePropManager } from './InlinePropManager';
import { PropsFilter } from './PropsFilter';
import {
startWebLayoutAnimation,
tryActivateLayoutTransition,
configureWebLayoutAnimations,
getReducedMotionFromConfig,
saveSnapshot,
} from '../reanimated2/layoutReanimation/web';
import { updateLayoutAnimations } from '../reanimated2/UpdateLayoutAnimations';
import type { CustomConfig } from '../reanimated2/layoutReanimation/web/config';
import type { FlatList, FlatListProps } from 'react-native';
import { addHTMLMutationObserver } from '../reanimated2/layoutReanimation/web/domUtils';
import { getViewInfo } from './getViewInfo';
const IS_WEB = isWeb();
if (IS_WEB) {
configureWebLayoutAnimations();
}
function onlyAnimatedStyles(styles: StyleProps[]): StyleProps[] {
return styles.filter((style) => style?.viewDescriptors);
}
type Options<P> = {
setNativeProps: (ref: AnimatedComponentRef, props: P) => void;
};
/**
* 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: FunctionComponent<P>,
options?: Options<P>
): FunctionComponent<AnimateProps<P>>;
export function createAnimatedComponent<P extends object>(
component: ComponentClass<P>,
options?: Options<P>
): ComponentClass<AnimateProps<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: ComponentType<P>,
options?: Options<P>
): FunctionComponent<AnimateProps<P>> | ComponentClass<AnimateProps<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: typeof FlatList<unknown>,
options?: Options<any>
): ComponentClass<AnimateProps<FlatListProps<unknown>>>;
export function createAnimatedComponent(
Component: ComponentType<InitialComponentProps>,
options?: Options<InitialComponentProps>
): any {
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<AnimatedComponentProps<InitialComponentProps>>
implements IAnimatedComponentInternal
{
_styles: StyleProps[] | null = null;
_animatedProps?: Partial<AnimatedComponentProps<AnimatedProps>>;
_viewTag = -1;
_isFirstRender = true;
jestAnimatedStyle: { value: StyleProps } = { value: {} };
_component: AnimatedComponentRef | HTMLElement | null = null;
_sharedElementTransition: SharedTransition | null = null;
_jsPropsUpdater = new JSPropsUpdater();
_InlinePropManager = new InlinePropManager();
_PropsFilter = new PropsFilter();
_viewInfo?: ViewInfo;
static displayName: string;
static contextType = SkipEnteringContext;
context!: React.ContextType<typeof SkipEnteringContext>;
constructor(props: AnimatedComponentProps<InitialComponentProps>) {
super(props);
if (isJest()) {
this.jestAnimatedStyle = { value: {} };
}
}
componentDidMount() {
this._viewTag = this._getViewInfo().viewTag as number;
this._attachNativeEvents();
this._jsPropsUpdater.addOnJSPropsChangeListener(this);
this._attachAnimatedStyles();
this._InlinePropManager.attachInlineProps(this, this._getViewInfo());
const layout = this.props.layout;
if (layout) {
this._configureLayoutTransition();
}
if (IS_WEB) {
if (this.props.exiting) {
saveSnapshot(this._component as HTMLElement);
}
if (
!this.props.entering ||
getReducedMotionFromConfig(this.props.entering as CustomConfig)
) {
this._isFirstRender = false;
return;
}
startWebLayoutAnimation(
this.props,
this._component as HTMLElement,
LayoutAnimationType.ENTERING
);
}
this._isFirstRender = false;
}
componentWillUnmount() {
this._detachNativeEvents();
this._jsPropsUpdater.removeOnJSPropsChangeListener(this);
this._detachStyles();
this._InlinePropManager.detachInlineProps();
if (this.props.sharedTransitionTag) {
this._configureSharedTransition(true);
}
this._sharedElementTransition?.unregisterTransition(this._viewTag, true);
const exiting = this.props.exiting;
if (
IS_WEB &&
this._component &&
this.props.exiting &&
!getReducedMotionFromConfig(this.props.exiting as CustomConfig)
) {
addHTMLMutationObserver();
startWebLayoutAnimation(
this.props,
this._component as HTMLElement,
LayoutAnimationType.EXITING
);
} else if (exiting) {
const reduceMotionInExiting =
'getReduceMotion' in exiting &&
typeof exiting.getReduceMotion === 'function'
? getReduceMotionFromConfig(exiting.getReduceMotion())
: getReduceMotionFromConfig();
if (!reduceMotionInExiting) {
updateLayoutAnimations(
this._viewTag,
LayoutAnimationType.EXITING,
maybeBuild(
exiting,
this.props?.style,
AnimatedComponent.displayName
)
);
}
}
}
_getEventViewRef() {
// Make sure to get the scrollable node for components that implement
// `ScrollResponder.Mixin`.
return (this._component as AnimatedComponentRef)?.getScrollableNode
? (this._component as AnimatedComponentRef).getScrollableNode?.()
: this._component;
}
_attachNativeEvents() {
for (const key in this.props) {
const prop = this.props[key];
if (
has('workletEventHandler', prop) &&
prop.workletEventHandler instanceof WorkletEventHandler
) {
prop.workletEventHandler.registerForEvents(this._viewTag, key);
}
}
}
_detachNativeEvents() {
for (const key in this.props) {
const prop = this.props[key];
if (
has('workletEventHandler', prop) &&
prop.workletEventHandler instanceof WorkletEventHandler
) {
prop.workletEventHandler.unregisterFromEvents(this._viewTag);
}
}
}
_detachStyles() {
if (IS_WEB && this._styles !== null) {
for (const style of this._styles) {
style.viewsRef.remove(this);
}
} else if (this._viewTag !== -1 && this._styles !== null) {
for (const style of this._styles) {
style.viewDescriptors.remove(this._viewTag);
}
if (this.props.animatedProps?.viewDescriptors) {
this.props.animatedProps.viewDescriptors.remove(this._viewTag);
}
if (isFabric()) {
removeFromPropsRegistry(this._viewTag);
}
}
}
_updateNativeEvents(
prevProps: AnimatedComponentProps<InitialComponentProps>
) {
for (const key in prevProps) {
const prevProp = prevProps[key];
if (
has('workletEventHandler', prevProp) &&
prevProp.workletEventHandler instanceof WorkletEventHandler
) {
const newProp = this.props[key];
if (!newProp) {
// Prop got deleted
prevProp.workletEventHandler.unregisterFromEvents(this._viewTag);
} else if (
has('workletEventHandler', newProp) &&
newProp.workletEventHandler instanceof WorkletEventHandler &&
newProp.workletEventHandler !== prevProp.workletEventHandler
) {
// Prop got changed
prevProp.workletEventHandler.unregisterFromEvents(this._viewTag);
newProp.workletEventHandler.registerForEvents(this._viewTag);
}
}
}
for (const key in this.props) {
const newProp = this.props[key];
if (
has('workletEventHandler', newProp) &&
newProp.workletEventHandler instanceof WorkletEventHandler &&
!prevProps[key]
) {
// Prop got added
newProp.workletEventHandler.registerForEvents(this._viewTag);
}
}
}
_updateFromNative(props: StyleProps) {
if (options?.setNativeProps) {
options.setNativeProps(this._component as AnimatedComponentRef, props);
} else {
(this._component as AnimatedComponentRef)?.setNativeProps?.(props);
}
}
_getViewInfo(): ViewInfo {
if (this._viewInfo !== undefined) {
return this._viewInfo;
}
let viewTag: number | HTMLElement | null;
let viewName: string | null;
let shadowNodeWrapper: ShadowNodeWrapper | null = 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._component as AnimatedComponentRef)
?.getAnimatableRef
? (this._component as AnimatedComponentRef).getAnimatableRef?.()
: this;
if (IS_WEB) {
// At this point I assume that `_setComponentRef` was already called and `_component` is set.
// `this._component` on web represents HTMLElement of our component, that's why we use casting
viewTag = this._component as HTMLElement;
viewName = null;
shadowNodeWrapper = null;
viewConfig = 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(component);
if (!hostInstance) {
throw new Error(
'[Reanimated] Cannot find host instance for this component. Maybe it renders nothing?'
);
}
const viewInfo = getViewInfo(hostInstance);
viewTag = viewInfo.viewTag;
viewName = viewInfo.viewName;
viewConfig = viewInfo.viewConfig;
shadowNodeWrapper = isFabric()
? getShadowNodeWrapperFromRef(this)
: null;
}
this._viewInfo = { viewTag, viewName, shadowNodeWrapper, viewConfig };
return this._viewInfo;
}
_attachAnimatedStyles() {
const styles = this.props.style
? onlyAnimatedStyles(flattenArray<StyleProps>(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.animatedProps?.viewDescriptors || styles.length;
if (hasReanimated2Props && viewConfig) {
adaptViewConfig(viewConfig);
}
this._viewTag = viewTag as number;
// 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 &&
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) => 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.jestAnimatedStyle.value = {
...this.jestAnimatedStyle.value,
...style.initial.value,
};
style.jestAnimatedStyle.current = this.jestAnimatedStyle;
}
});
// detach old animatedProps
if (prevAnimatedProps && prevAnimatedProps !== this.props.animatedProps) {
prevAnimatedProps.viewDescriptors!.remove(viewTag as number);
}
// attach animatedProps property
if (this.props.animatedProps?.viewDescriptors) {
this.props.animatedProps.viewDescriptors.add({
tag: viewTag as number,
name: viewName!,
shadowNodeWrapper: shadowNodeWrapper!,
});
}
}
componentDidUpdate(
prevProps: AnimatedComponentProps<InitialComponentProps>,
_prevState: Readonly<unknown>,
// This type comes straight from React
// eslint-disable-next-line @typescript-eslint/no-explicit-any
snapshot: DOMRect | null
) {
const layout = this.props.layout;
const oldLayout = prevProps.layout;
if (layout !== oldLayout) {
this._configureLayoutTransition();
}
if (
this.props.sharedTransitionTag !== undefined ||
prevProps.sharedTransitionTag !== undefined
) {
this._configureSharedTransition();
}
this._updateNativeEvents(prevProps);
this._attachAnimatedStyles();
this._InlinePropManager.attachInlineProps(this, this._getViewInfo());
if (IS_WEB && this.props.exiting) {
saveSnapshot(this._component as HTMLElement);
}
// Snapshot won't be undefined because it comes from getSnapshotBeforeUpdate method
if (
IS_WEB &&
snapshot !== null &&
this.props.layout &&
!getReducedMotionFromConfig(this.props.layout as CustomConfig)
) {
tryActivateLayoutTransition(
this.props,
this._component as HTMLElement,
snapshot
);
}
}
_configureLayoutTransition() {
const layout = this.props.layout
? maybeBuild(
this.props.layout,
undefined /* We don't have to warn user if style has common properties with animation for LAYOUT */,
AnimatedComponent.displayName
)
: undefined;
updateLayoutAnimations(this._viewTag, LayoutAnimationType.LAYOUT, layout);
}
_configureSharedTransition(isUnmounting = false) {
if (IS_WEB) {
return;
}
const { sharedTransitionTag } = this.props;
if (!sharedTransitionTag) {
this._sharedElementTransition?.unregisterTransition(
this._viewTag,
isUnmounting
);
this._sharedElementTransition = null;
return;
}
const sharedElementTransition =
this.props.sharedTransitionStyle ??
this._sharedElementTransition ??
new SharedTransition();
sharedElementTransition.registerTransition(
this._viewTag,
sharedTransitionTag,
isUnmounting
);
this._sharedElementTransition = sharedElementTransition;
}
_setComponentRef = setAndForwardRef<Component | HTMLElement>({
getForwardedRef: () =>
this.props.forwardedRef as MutableRefObject<
Component<Record<string, unknown>, Record<string, unknown>, unknown>
>,
setLocalRef: (ref) => {
// TODO update config
const tag = IS_WEB
? (ref as HTMLElement)
: findNodeHandle(ref as Component);
this._viewTag = tag as number;
const { layout, entering, exiting, sharedTransitionTag } = this.props;
if (
(layout || entering || exiting || sharedTransitionTag) &&
tag != null
) {
if (!shouldBeUseWeb()) {
enableLayoutAnimations(true, false);
}
if (sharedTransitionTag) {
this._configureSharedTransition();
}
const skipEntering = this.context?.current;
if (entering && !skipEntering) {
updateLayoutAnimations(
tag as number,
LayoutAnimationType.ENTERING,
maybeBuild(
entering,
this.props?.style,
AnimatedComponent.displayName
)
);
}
}
if (ref !== this._component) {
this._component = ref;
}
},
});
// 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._component as HTMLElement)?.getBoundingClientRect !== undefined
) {
return (this._component as HTMLElement).getBoundingClientRect();
}
return null;
}
render() {
const filteredProps = this._PropsFilter.filterNonAnimatedProps(this);
if (isJest()) {
filteredProps.jestAnimatedStyle = this.jestAnimatedStyle;
}
// 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 = {
...(filteredProps.style ?? {}),
visibility: 'hidden', // Hide component until `componentDidMount` triggers
};
}
const platformProps = Platform.select({
web: {},
default: { collapsable: false },
});
return (
<Component
{...filteredProps}
// Casting is used here, because ref can be null - in that case it cannot be assigned to HTMLElement.
// After spending some time trying to figure out what to do with this problem, we decided to leave it this way
ref={this._setComponentRef as (ref: Component) => void}
{...platformProps}
/>
);
}
}
AnimatedComponent.displayName = `AnimatedComponent(${
Component.displayName || Component.name || 'Component'
})`;
return React.forwardRef<Component>((props, ref) => {
return (
<AnimatedComponent
{...props}
{...(ref === null ? null : { forwardedRef: ref })}
/>
);
});
}

View File

@@ -0,0 +1,39 @@
'use strict';
/* eslint-disable @typescript-eslint/no-explicit-any */
// This is a makeshift solution to handle both 0.73 and 0.74 versions of React Native.
export let getViewInfo = (element: any) => {
if (element._nativeTag !== undefined && element.__nativeTag !== null) {
getViewInfo = getViewInfo73;
return getViewInfo73(element);
} else if (
element.__nativeTag !== undefined &&
element.__nativeTag !== null
) {
getViewInfo = getViewInfoLatest;
return getViewInfoLatest(element);
}
return getViewInfo73(element);
};
function getViewInfo73(element: any) {
return {
// 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
viewName: element?.viewConfig?.uiViewClassName,
/**
* RN uses viewConfig for components for storing different properties of the component(example: https://github.com/facebook/react-native/blob/main/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponent.js#L24).
* The name we're looking for is in the field named uiViewClassName.
*/
viewTag: element?._nativeTag,
viewConfig: element?.viewConfig,
};
}
function getViewInfoLatest(element: any) {
return {
viewName: element?._viewConfig?.uiViewClassName,
viewTag: element?.__nativeTag,
viewConfig: element?._viewConfig,
};
}

View File

@@ -0,0 +1,2 @@
'use strict';
export { createAnimatedComponent } from './createAnimatedComponent';

View File

@@ -0,0 +1,66 @@
'use strict';
/**
* imported from react-native
*/
import type { MutableRefObject } from 'react';
/* eslint-disable */
/**
* This is a helper function for when a component needs to be able to forward a ref
* to a child component, but still needs to have access to that component as part of
* its implementation.
*
* Its main use case is in wrappers for native components.
*
* Usage:
*
* class MyView extends React.Component {
* _nativeRef = null;
*
* _setNativeRef = setAndForwardRef({
* getForwardedRef: () => this.props.forwardedRef,
* setLocalRef: ref => {
* this._nativeRef = ref;
* },
* });
*
* render() {
* return <View ref={this._setNativeRef} />;
* }
* }
*
* const MyViewWithRef = React.forwardRef((props, ref) => (
* <MyView {...props} forwardedRef={ref} />
* ));
*
* module.exports = MyViewWithRef;
*/
/* eslint-enable */
type ForwardedRef<T> = () => MutableRefObject<T> | ((ref: T) => void);
function setAndForwardRef<T>({
getForwardedRef,
setLocalRef,
}: {
getForwardedRef: ForwardedRef<T>;
setLocalRef: (ref: T) => void;
}): (ref: T) => void {
return function forwardRef(ref: T) {
const forwardedRef = getForwardedRef();
setLocalRef(ref);
// Forward to user ref prop (if one has been specified)
if (typeof forwardedRef === 'function') {
// Handle function-based refs. String-based refs are handled as functions.
forwardedRef(ref);
} else if (typeof forwardedRef === 'object' && forwardedRef != null) {
// Handle createRef-based refs
forwardedRef.current = ref;
}
};
}
export default setAndForwardRef;

View File

@@ -0,0 +1,35 @@
'use strict';
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;
};

View File

@@ -0,0 +1,5 @@
'use strict';
import * as Animated from './Animated';
export * from './reanimated2';
export default Animated;

View File

@@ -0,0 +1,3 @@
'use strict';
export * from './reanimated2';
export * as default from './Animated'; // If this line fails, you probably forgot some installation steps. Check out the installation guide here: https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/getting-started#installation 1) Make sure reanimated's babel plugin is installed in your babel.config.js (you should have 'react-native-reanimated/plugin' listed there - also see the above link for details) 2) Make sure you reset build cache after updating the config, run: yarn start --reset-cache

View File

@@ -0,0 +1,454 @@
/* eslint-disable n/no-callback-literal */
'use strict';
import type {
WithSpringConfig,
WithTimingConfig,
WithDecayConfig,
AnimatableValue,
AnimationCallback,
} from './reanimated2';
import {
IOSReferenceFrame,
InterfaceOrientation,
KeyboardState,
ReduceMotion,
SensorType,
ColorSpace,
Extrapolation,
SharedTransitionType,
withReanimatedTimer,
advanceAnimationByTime,
advanceAnimationByFrame,
setUpTests,
getAnimatedStyle,
} from './reanimated2';
import {
View as ViewRN,
Text as TextRN,
Image as ImageRN,
Animated as AnimatedRN,
processColor as processColorRN,
} from 'react-native';
const NOOP = () => {};
const NOOP_FACTORY = () => NOOP;
const ID = <T>(t: T) => t;
const IMMEDIATE_CALLBACK_INVOCATION = <T>(callback: () => T) => callback();
const hook = {
useAnimatedProps: IMMEDIATE_CALLBACK_INVOCATION,
// useEvent: ADD ME IF NEEDED
// useHandler: ADD ME IF NEEDED
useWorkletCallback: ID,
useSharedValue: <Value>(init: Value) => ({ value: init }),
// useReducedMotion: ADD ME IF NEEDED
useAnimatedStyle: IMMEDIATE_CALLBACK_INVOCATION,
useAnimatedGestureHandler: NOOP_FACTORY,
useAnimatedReaction: NOOP,
useAnimatedRef: () => ({ current: null }),
useAnimatedScrollHandler: NOOP_FACTORY,
useDerivedValue: <Value>(processor: () => Value) => ({ value: processor() }),
useAnimatedSensor: () => ({
sensor: {
value: {
x: 0,
y: 0,
z: 0,
interfaceOrientation: 0,
qw: 0,
qx: 0,
qy: 0,
qz: 0,
yaw: 0,
pitch: 0,
roll: 0,
},
},
unregister: NOOP,
isAvailable: false,
config: {
interval: 0,
adjustToInterfaceOrientation: false,
iosReferenceFrame: 0,
},
}),
// useFrameCallback: ADD ME IF NEEDED
useAnimatedKeyboard: () => ({ height: 0, state: 0 }),
// useScrollViewOffset: ADD ME IF NEEDED
};
const animation = {
cancelAnimation: NOOP,
// defineAnimation: ADD ME IF NEEDED
// withClamp: ADD ME IF NEEDED
withDecay: (_userConfig: WithDecayConfig, callback?: AnimationCallback) => {
callback?.(true);
return 0;
},
withDelay: <T>(_delayMs: number, nextAnimation: T) => {
return nextAnimation;
},
withRepeat: ID,
withSequence: () => 0,
withSpring: (
toValue: AnimatableValue,
_userConfig?: WithSpringConfig,
callback?: AnimationCallback
) => {
callback?.(true);
return toValue;
},
withTiming: (
toValue: AnimatableValue,
_userConfig?: WithTimingConfig,
callback?: AnimationCallback
) => {
callback?.(true);
return toValue;
},
};
const interpolation = {
Extrapolation,
interpolate: NOOP,
clamp: NOOP,
};
const interpolateColor = {
Extrapolate: Extrapolation,
Extrapolation,
ColorSpace,
interpolateColor: NOOP,
// useInterpolateConfig: ADD ME IF NEEDED
};
const Easing = {
Easing: {
linear: ID,
ease: ID,
quad: ID,
cubic: ID,
poly: ID,
sin: ID,
circle: ID,
exp: ID,
elastic: ID,
back: ID,
bounce: ID,
bezier: () => ({ factory: ID }),
bezierFn: ID,
steps: ID,
in: ID,
out: ID,
inOut: ID,
},
};
const platformFunctions = {
measure: () => ({
x: 0,
y: 0,
width: 0,
height: 0,
pageX: 0,
pageY: 0,
}),
// dispatchCommand: ADD ME IF NEEDED
scrollTo: NOOP,
// setGestureState: ADD ME IF NEEDED
// setNativeProps: ADD ME IF NEEDED
// getRelativeCoords: ADD ME IF NEEDED
};
const Colors = {
// isColor: ADD ME IF NEEDED
processColor: processColorRN,
// convertToRGBA: ADD ME IF NEEDED
};
const PropAdapters = {
// createAnimatedPropAdapter: ADD ME IF NEEDED
};
class BaseAnimationMock {
duration() {
return this;
}
delay() {
return this;
}
springify() {
return this;
}
damping() {
return this;
}
stiffness() {
return this;
}
withCallback() {
return this;
}
randomDelay() {
return this;
}
withInitialValues() {
return this;
}
easing(_: (t: number) => number) {
return this;
}
rotate(_: string) {
return this;
}
mass(_: number) {
return this;
}
restDisplacementThreshold(_: number) {
return this;
}
restSpeedThreshold(_: number) {
return this;
}
overshootClamping(_: number) {
return this;
}
dampingRatio(_: number) {
return this;
}
getDelay() {
return 0;
}
getDelayFunction() {
return NOOP;
}
getDuration() {
return 300;
}
getReduceMotion() {
return ReduceMotion.System;
}
getAnimationAndConfig() {
return [NOOP, {}];
}
build() {
return () => ({ initialValues: {}, animations: {} });
}
reduceMotion() {
return this;
}
}
const core = {
runOnJS: ID,
runOnUI: ID,
createWorkletRuntime: NOOP,
runOnRuntime: NOOP,
makeMutable: ID,
makeShareableCloneRecursive: ID,
isReanimated3: () => true,
// isConfigured: ADD ME IF NEEDED
enableLayoutAnimations: NOOP,
// getViewProp: ADD ME IF NEEDED
};
const layoutReanimation = {
BaseAnimationBuilder: new BaseAnimationMock(),
ComplexAnimationBuilder: new BaseAnimationMock(),
Keyframe: new BaseAnimationMock(),
// Flip
FlipInXUp: new BaseAnimationMock(),
FlipInYLeft: new BaseAnimationMock(),
FlipInXDown: new BaseAnimationMock(),
FlipInYRight: new BaseAnimationMock(),
FlipInEasyX: new BaseAnimationMock(),
FlipInEasyY: new BaseAnimationMock(),
FlipOutXUp: new BaseAnimationMock(),
FlipOutYLeft: new BaseAnimationMock(),
FlipOutXDown: new BaseAnimationMock(),
FlipOutYRight: new BaseAnimationMock(),
FlipOutEasyX: new BaseAnimationMock(),
FlipOutEasyY: new BaseAnimationMock(),
// Stretch
StretchInX: new BaseAnimationMock(),
StretchInY: new BaseAnimationMock(),
StretchOutX: new BaseAnimationMock(),
StretchOutY: new BaseAnimationMock(),
// Fade
FadeIn: new BaseAnimationMock(),
FadeInRight: new BaseAnimationMock(),
FadeInLeft: new BaseAnimationMock(),
FadeInUp: new BaseAnimationMock(),
FadeInDown: new BaseAnimationMock(),
FadeOut: new BaseAnimationMock(),
FadeOutRight: new BaseAnimationMock(),
FadeOutLeft: new BaseAnimationMock(),
FadeOutUp: new BaseAnimationMock(),
FadeOutDown: new BaseAnimationMock(),
// Slide
SlideInRight: new BaseAnimationMock(),
SlideInLeft: new BaseAnimationMock(),
SlideOutRight: new BaseAnimationMock(),
SlideOutLeft: new BaseAnimationMock(),
SlideInUp: new BaseAnimationMock(),
SlideInDown: new BaseAnimationMock(),
SlideOutUp: new BaseAnimationMock(),
SlideOutDown: new BaseAnimationMock(),
// Zoom
ZoomIn: new BaseAnimationMock(),
ZoomInRotate: new BaseAnimationMock(),
ZoomInLeft: new BaseAnimationMock(),
ZoomInRight: new BaseAnimationMock(),
ZoomInUp: new BaseAnimationMock(),
ZoomInDown: new BaseAnimationMock(),
ZoomInEasyUp: new BaseAnimationMock(),
ZoomInEasyDown: new BaseAnimationMock(),
ZoomOut: new BaseAnimationMock(),
ZoomOutRotate: new BaseAnimationMock(),
ZoomOutLeft: new BaseAnimationMock(),
ZoomOutRight: new BaseAnimationMock(),
ZoomOutUp: new BaseAnimationMock(),
ZoomOutDown: new BaseAnimationMock(),
ZoomOutEasyUp: new BaseAnimationMock(),
ZoomOutEasyDown: new BaseAnimationMock(),
// Bounce
BounceIn: new BaseAnimationMock(),
BounceInDown: new BaseAnimationMock(),
BounceInUp: new BaseAnimationMock(),
BounceInLeft: new BaseAnimationMock(),
BounceInRight: new BaseAnimationMock(),
BounceOut: new BaseAnimationMock(),
BounceOutDown: new BaseAnimationMock(),
BounceOutUp: new BaseAnimationMock(),
BounceOutLeft: new BaseAnimationMock(),
BounceOutRight: new BaseAnimationMock(),
// Lightspeed
LightSpeedInRight: new BaseAnimationMock(),
LightSpeedInLeft: new BaseAnimationMock(),
LightSpeedOutRight: new BaseAnimationMock(),
LightSpeedOutLeft: new BaseAnimationMock(),
// Pinwheel
PinwheelIn: new BaseAnimationMock(),
PinwheelOut: new BaseAnimationMock(),
// Rotate
RotateInDownLeft: new BaseAnimationMock(),
RotateInDownRight: new BaseAnimationMock(),
RotateInUpLeft: new BaseAnimationMock(),
RotateInUpRight: new BaseAnimationMock(),
RotateOutDownLeft: new BaseAnimationMock(),
RotateOutDownRight: new BaseAnimationMock(),
RotateOutUpLeft: new BaseAnimationMock(),
RotateOutUpRight: new BaseAnimationMock(),
// Roll
RollInLeft: new BaseAnimationMock(),
RollInRight: new BaseAnimationMock(),
RollOutLeft: new BaseAnimationMock(),
RollOutRight: new BaseAnimationMock(),
// Transitions
Layout: new BaseAnimationMock(),
LinearTransition: new BaseAnimationMock(),
FadingTransition: new BaseAnimationMock(),
SequencedTransition: new BaseAnimationMock(),
JumpingTransition: new BaseAnimationMock(),
CurvedTransition: new BaseAnimationMock(),
EntryExitTransition: new BaseAnimationMock(),
// combineTransitions: ADD ME IF NEEDED
// SET
// SharedTransition: ADD ME IF NEEDED
SharedTransitionType,
};
const isSharedValue = {
// isSharedValue: ADD ME IF NEEDED
};
const commonTypes = {
SensorType,
IOSReferenceFrame,
InterfaceOrientation,
KeyboardState,
ReduceMotion,
};
const pluginUtils = {
// getUseOfValueInStyleWarning: ADD ME IF NEEDED
};
const jestUtils = {
withReanimatedTimer,
advanceAnimationByTime,
advanceAnimationByFrame,
setUpTests,
getAnimatedStyle,
};
const LayoutAnimationConfig = {
// LayoutAnimationConfig: ADD ME IF NEEDED
};
const mappers = {
// startMapper: ADD ME IF NEEDED
// stopMapper: ADD ME IF NEEDED
};
const Animated = {
View: ViewRN,
Text: TextRN,
Image: ImageRN,
ScrollView: AnimatedRN.ScrollView,
FlatList: AnimatedRN.FlatList,
Extrapolate: Extrapolation,
interpolate: NOOP,
interpolateColor: NOOP,
clamp: NOOP,
createAnimatedComponent: ID,
addWhitelistedUIProps: NOOP,
addWhitelistedNativeProps: NOOP,
};
const Reanimated = {
...core,
...hook,
...animation,
...interpolation,
...interpolateColor,
...Easing,
...platformFunctions,
...Colors,
...PropAdapters,
...layoutReanimation,
...isSharedValue,
...commonTypes,
...pluginUtils,
...jestUtils,
...LayoutAnimationConfig,
...mappers,
};
module.exports = {
__esModule: true,
...Reanimated,
default: Animated,
};

View File

@@ -0,0 +1,126 @@
'use strict';
type AllowlistsHolder = {
UI_THREAD_PROPS_WHITELIST: Record<string, boolean>;
NATIVE_THREAD_PROPS_WHITELIST: Record<string, boolean>;
};
export const PropsAllowlists: AllowlistsHolder = {
/**
* Styles allowed to be direcly updated in UI thread
*/
UI_THREAD_PROPS_WHITELIST: {
opacity: true,
transform: true,
/* colors */
backgroundColor: true,
borderRightColor: true,
borderBottomColor: true,
borderColor: true,
borderEndColor: true,
borderLeftColor: true,
borderStartColor: true,
borderTopColor: true,
/* ios styles */
shadowOpacity: true,
shadowRadius: true,
/* legacy android transform properties */
scaleX: true,
scaleY: true,
translateX: true,
translateY: true,
},
/**
* Whitelist of view props that can be updated in native thread via UIManagerModule
*/
NATIVE_THREAD_PROPS_WHITELIST: {
borderBottomWidth: true,
borderEndWidth: true,
borderLeftWidth: true,
borderRightWidth: true,
borderStartWidth: true,
borderTopWidth: true,
borderWidth: true,
bottom: true,
flex: true,
flexGrow: true,
flexShrink: true,
height: true,
left: true,
margin: true,
marginBottom: true,
marginEnd: true,
marginHorizontal: true,
marginLeft: true,
marginRight: true,
marginStart: true,
marginTop: true,
marginVertical: true,
maxHeight: true,
maxWidth: true,
minHeight: true,
minWidth: true,
padding: true,
paddingBottom: true,
paddingEnd: true,
paddingHorizontal: true,
paddingLeft: true,
paddingRight: true,
paddingStart: true,
paddingTop: true,
paddingVertical: true,
right: true,
start: true,
top: true,
width: true,
zIndex: true,
borderBottomEndRadius: true,
borderBottomLeftRadius: true,
borderBottomRightRadius: true,
borderBottomStartRadius: true,
borderRadius: true,
borderTopEndRadius: true,
borderTopLeftRadius: true,
borderTopRightRadius: true,
borderTopStartRadius: true,
elevation: true,
fontSize: true,
lineHeight: true,
textShadowRadius: true,
textShadowOffset: true,
letterSpacing: true,
aspectRatio: true,
columnGap: true, // iOS only
end: true, // number or string
flexBasis: true, // number or string
gap: true,
rowGap: true,
/* strings */
display: true,
backfaceVisibility: true,
overflow: true,
resizeMode: true,
fontStyle: true,
fontWeight: true,
textAlign: true,
textDecorationLine: true,
fontFamily: true,
textAlignVertical: true,
fontVariant: true,
textDecorationStyle: true,
textTransform: true,
writingDirection: true,
alignContent: true,
alignItems: true,
alignSelf: true,
direction: true, // iOS only
flexDirection: true,
flexWrap: true,
justifyContent: true,
position: true,
/* text color */
color: true,
tintColor: true,
shadowColor: true,
placeholderTextColor: true,
},
};

View File

@@ -0,0 +1,173 @@
'use strict';
/**
* https://github.com/gre/bezier-easing
* BezierEasing - use bezier curve for transition easing function
* by Gaëtan Renaudeau 2014 - 2015 MIT License
*/
// These values are established by empiricism with tests (tradeoff: performance VS precision)
const NEWTON_ITERATIONS = 4;
const NEWTON_MIN_SLOPE = 0.001;
const SUBDIVISION_PRECISION = 0.0000001;
const SUBDIVISION_MAX_ITERATIONS = 10;
const kSplineTableSize = 11;
const kSampleStepSize = 1.0 / (kSplineTableSize - 1.0);
function A(aA1: number, aA2: number): number {
'worklet';
return 1.0 - 3.0 * aA2 + 3.0 * aA1;
}
function B(aA1: number, aA2: number): number {
'worklet';
return 3.0 * aA2 - 6.0 * aA1;
}
function C(aA1: number) {
'worklet';
return 3.0 * aA1;
}
// Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2.
function calcBezier(aT: number, aA1: number, aA2: number): number {
'worklet';
return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT;
}
// Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2.
function getSlope(aT: number, aA1: number, aA2: number): number {
'worklet';
return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1);
}
function binarySubdivide(
aX: number,
aA: number,
aB: number,
mX1: number,
mX2: number
): number {
'worklet';
let currentX;
let currentT;
let i = 0;
do {
currentT = aA + (aB - aA) / 2.0;
currentX = calcBezier(currentT, mX1, mX2) - aX;
if (currentX > 0.0) {
aB = currentT;
} else {
aA = currentT;
}
} while (
Math.abs(currentX) > SUBDIVISION_PRECISION &&
++i < SUBDIVISION_MAX_ITERATIONS
);
return currentT;
}
function newtonRaphsonIterate(
aX: number,
aGuessT: number,
mX1: number,
mX2: number
): number {
'worklet';
for (let i = 0; i < NEWTON_ITERATIONS; ++i) {
const currentSlope = getSlope(aGuessT, mX1, mX2);
if (currentSlope === 0.0) {
return aGuessT;
}
const currentX = calcBezier(aGuessT, mX1, mX2) - aX;
aGuessT -= currentX / currentSlope;
}
return aGuessT;
}
export function Bezier(
mX1: number,
mY1: number,
mX2: number,
mY2: number
): (x: number) => number {
'worklet';
function LinearEasing(x: number): number {
'worklet';
return x;
}
if (!(mX1 >= 0 && mX1 <= 1 && mX2 >= 0 && mX2 <= 1)) {
throw new Error('[Reanimated] Bezier x values must be in [0, 1] range.');
}
if (mX1 === mY1 && mX2 === mY2) {
return LinearEasing;
}
// FIXME: Float32Array is not available in Hermes right now
//
// var float32ArraySupported = typeof Float32Array === 'function';
// const sampleValues = float32ArraySupported
// ? new Float32Array(kSplineTableSize)
// : new Array(kSplineTableSize);
// Precompute samples table
const sampleValues = new Array(kSplineTableSize);
for (let i = 0; i < kSplineTableSize; ++i) {
sampleValues[i] = calcBezier(i * kSampleStepSize, mX1, mX2);
}
function getTForX(aX: number): number {
'worklet';
let intervalStart = 0.0;
let currentSample = 1;
const lastSample = kSplineTableSize - 1;
for (
;
currentSample !== lastSample && sampleValues[currentSample] <= aX;
++currentSample
) {
intervalStart += kSampleStepSize;
}
--currentSample;
// Interpolate to provide an initial guess for t
const dist =
(aX - sampleValues[currentSample]) /
(sampleValues[currentSample + 1] - sampleValues[currentSample]);
const guessForT = intervalStart + dist * kSampleStepSize;
const initialSlope = getSlope(guessForT, mX1, mX2);
if (initialSlope >= NEWTON_MIN_SLOPE) {
return newtonRaphsonIterate(aX, guessForT, mX1, mX2);
} else if (initialSlope === 0.0) {
return guessForT;
} else {
return binarySubdivide(
aX,
intervalStart,
intervalStart + kSampleStepSize,
mX1,
mX2
);
}
}
return function BezierEasing(x) {
'worklet';
if (mX1 === mY1 && mX2 === mY2) {
return x; // linear
}
// Because JavaScript number are imprecise, we should guarantee the extremes are right.
if (x === 0) {
return 0;
}
if (x === 1) {
return 1;
}
return calcBezier(getTForX(x), mY1, mY2);
};
}

View File

@@ -0,0 +1,649 @@
'use strict';
/**
* Copied from:
* react-native/Libraries/StyleSheet/normalizeColor.js
* react-native/Libraries/StyleSheet/processColor.js
* https://github.com/wcandillon/react-native-redash/blob/master/src/Colors.ts
*/
/* eslint no-bitwise: 0 */
import type { StyleProps } from './commonTypes';
import { makeShareable } from './core';
import { isAndroid, isWeb } from './PlatformChecker';
interface RGB {
r: number;
g: number;
b: number;
}
interface HSV {
h: number;
s: number;
v: number;
}
// var INTEGER = '[-+]?\\d+';
const NUMBER = '[-+]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)';
const PERCENTAGE = NUMBER + '%';
function call(...args: unknown[]): string {
'worklet';
return '\\(\\s*(' + args.join(')\\s*,\\s*(') + ')\\s*\\)';
}
const MATCHERS = {
rgb: new RegExp('rgb' + call(NUMBER, NUMBER, NUMBER)),
rgba: new RegExp('rgba' + call(NUMBER, NUMBER, NUMBER, NUMBER)),
hsl: new RegExp('hsl' + call(NUMBER, PERCENTAGE, PERCENTAGE)),
hsla: new RegExp('hsla' + call(NUMBER, PERCENTAGE, PERCENTAGE, NUMBER)),
hex3: /^#([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
hex4: /^#([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
hex6: /^#([0-9a-fA-F]{6})$/,
hex8: /^#([0-9a-fA-F]{8})$/,
};
function hue2rgb(p: number, q: number, t: number): number {
'worklet';
if (t < 0) {
t += 1;
}
if (t > 1) {
t -= 1;
}
if (t < 1 / 6) {
return p + (q - p) * 6 * t;
}
if (t < 1 / 2) {
return q;
}
if (t < 2 / 3) {
return p + (q - p) * (2 / 3 - t) * 6;
}
return p;
}
function hslToRgb(h: number, s: number, l: number): number {
'worklet';
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
const r = hue2rgb(p, q, h + 1 / 3);
const g = hue2rgb(p, q, h);
const b = hue2rgb(p, q, h - 1 / 3);
return (
(Math.round(r * 255) << 24) |
(Math.round(g * 255) << 16) |
(Math.round(b * 255) << 8)
);
}
function parse255(str: string): number {
'worklet';
const int = Number.parseInt(str, 10);
if (int < 0) {
return 0;
}
if (int > 255) {
return 255;
}
return int;
}
function parse360(str: string): number {
'worklet';
const int = Number.parseFloat(str);
return (((int % 360) + 360) % 360) / 360;
}
function parse1(str: string): number {
'worklet';
const num = Number.parseFloat(str);
if (num < 0) {
return 0;
}
if (num > 1) {
return 255;
}
return Math.round(num * 255);
}
function parsePercentage(str: string): number {
'worklet';
// parseFloat conveniently ignores the final %
const int = Number.parseFloat(str);
if (int < 0) {
return 0;
}
if (int > 100) {
return 1;
}
return int / 100;
}
const names: Record<string, number> = makeShareable({
transparent: 0x00000000,
// http://www.w3.org/TR/css3-color/#svg-color
aliceblue: 0xf0f8ffff,
antiquewhite: 0xfaebd7ff,
aqua: 0x00ffffff,
aquamarine: 0x7fffd4ff,
azure: 0xf0ffffff,
beige: 0xf5f5dcff,
bisque: 0xffe4c4ff,
black: 0x000000ff,
blanchedalmond: 0xffebcdff,
blue: 0x0000ffff,
blueviolet: 0x8a2be2ff,
brown: 0xa52a2aff,
burlywood: 0xdeb887ff,
burntsienna: 0xea7e5dff,
cadetblue: 0x5f9ea0ff,
chartreuse: 0x7fff00ff,
chocolate: 0xd2691eff,
coral: 0xff7f50ff,
cornflowerblue: 0x6495edff,
cornsilk: 0xfff8dcff,
crimson: 0xdc143cff,
cyan: 0x00ffffff,
darkblue: 0x00008bff,
darkcyan: 0x008b8bff,
darkgoldenrod: 0xb8860bff,
darkgray: 0xa9a9a9ff,
darkgreen: 0x006400ff,
darkgrey: 0xa9a9a9ff,
darkkhaki: 0xbdb76bff,
darkmagenta: 0x8b008bff,
darkolivegreen: 0x556b2fff,
darkorange: 0xff8c00ff,
darkorchid: 0x9932ccff,
darkred: 0x8b0000ff,
darksalmon: 0xe9967aff,
darkseagreen: 0x8fbc8fff,
darkslateblue: 0x483d8bff,
darkslategray: 0x2f4f4fff,
darkslategrey: 0x2f4f4fff,
darkturquoise: 0x00ced1ff,
darkviolet: 0x9400d3ff,
deeppink: 0xff1493ff,
deepskyblue: 0x00bfffff,
dimgray: 0x696969ff,
dimgrey: 0x696969ff,
dodgerblue: 0x1e90ffff,
firebrick: 0xb22222ff,
floralwhite: 0xfffaf0ff,
forestgreen: 0x228b22ff,
fuchsia: 0xff00ffff,
gainsboro: 0xdcdcdcff,
ghostwhite: 0xf8f8ffff,
gold: 0xffd700ff,
goldenrod: 0xdaa520ff,
gray: 0x808080ff,
green: 0x008000ff,
greenyellow: 0xadff2fff,
grey: 0x808080ff,
honeydew: 0xf0fff0ff,
hotpink: 0xff69b4ff,
indianred: 0xcd5c5cff,
indigo: 0x4b0082ff,
ivory: 0xfffff0ff,
khaki: 0xf0e68cff,
lavender: 0xe6e6faff,
lavenderblush: 0xfff0f5ff,
lawngreen: 0x7cfc00ff,
lemonchiffon: 0xfffacdff,
lightblue: 0xadd8e6ff,
lightcoral: 0xf08080ff,
lightcyan: 0xe0ffffff,
lightgoldenrodyellow: 0xfafad2ff,
lightgray: 0xd3d3d3ff,
lightgreen: 0x90ee90ff,
lightgrey: 0xd3d3d3ff,
lightpink: 0xffb6c1ff,
lightsalmon: 0xffa07aff,
lightseagreen: 0x20b2aaff,
lightskyblue: 0x87cefaff,
lightslategray: 0x778899ff,
lightslategrey: 0x778899ff,
lightsteelblue: 0xb0c4deff,
lightyellow: 0xffffe0ff,
lime: 0x00ff00ff,
limegreen: 0x32cd32ff,
linen: 0xfaf0e6ff,
magenta: 0xff00ffff,
maroon: 0x800000ff,
mediumaquamarine: 0x66cdaaff,
mediumblue: 0x0000cdff,
mediumorchid: 0xba55d3ff,
mediumpurple: 0x9370dbff,
mediumseagreen: 0x3cb371ff,
mediumslateblue: 0x7b68eeff,
mediumspringgreen: 0x00fa9aff,
mediumturquoise: 0x48d1ccff,
mediumvioletred: 0xc71585ff,
midnightblue: 0x191970ff,
mintcream: 0xf5fffaff,
mistyrose: 0xffe4e1ff,
moccasin: 0xffe4b5ff,
navajowhite: 0xffdeadff,
navy: 0x000080ff,
oldlace: 0xfdf5e6ff,
olive: 0x808000ff,
olivedrab: 0x6b8e23ff,
orange: 0xffa500ff,
orangered: 0xff4500ff,
orchid: 0xda70d6ff,
palegoldenrod: 0xeee8aaff,
palegreen: 0x98fb98ff,
paleturquoise: 0xafeeeeff,
palevioletred: 0xdb7093ff,
papayawhip: 0xffefd5ff,
peachpuff: 0xffdab9ff,
peru: 0xcd853fff,
pink: 0xffc0cbff,
plum: 0xdda0ddff,
powderblue: 0xb0e0e6ff,
purple: 0x800080ff,
rebeccapurple: 0x663399ff,
red: 0xff0000ff,
rosybrown: 0xbc8f8fff,
royalblue: 0x4169e1ff,
saddlebrown: 0x8b4513ff,
salmon: 0xfa8072ff,
sandybrown: 0xf4a460ff,
seagreen: 0x2e8b57ff,
seashell: 0xfff5eeff,
sienna: 0xa0522dff,
silver: 0xc0c0c0ff,
skyblue: 0x87ceebff,
slateblue: 0x6a5acdff,
slategray: 0x708090ff,
slategrey: 0x708090ff,
snow: 0xfffafaff,
springgreen: 0x00ff7fff,
steelblue: 0x4682b4ff,
tan: 0xd2b48cff,
teal: 0x008080ff,
thistle: 0xd8bfd8ff,
tomato: 0xff6347ff,
turquoise: 0x40e0d0ff,
violet: 0xee82eeff,
wheat: 0xf5deb3ff,
white: 0xffffffff,
whitesmoke: 0xf5f5f5ff,
yellow: 0xffff00ff,
yellowgreen: 0x9acd32ff,
});
// copied from react-native/Libraries/Components/View/ReactNativeStyleAttributes
export const ColorProperties = makeShareable([
'backgroundColor',
'borderBottomColor',
'borderColor',
'borderLeftColor',
'borderRightColor',
'borderTopColor',
'borderStartColor',
'borderEndColor',
'borderBlockColor',
'borderBlockEndColor',
'borderBlockStartColor',
'color',
'shadowColor',
'textDecorationColor',
'tintColor',
'textShadowColor',
'overlayColor',
]);
function normalizeColor(color: unknown): number | null {
'worklet';
if (typeof color === 'number') {
if (color >>> 0 === color && color >= 0 && color <= 0xffffffff) {
return color;
}
return null;
}
if (typeof color !== 'string') {
return null;
}
let match: RegExpExecArray | null | undefined;
// Ordered based on occurrences on Facebook codebase
if ((match = MATCHERS.hex6.exec(color))) {
return Number.parseInt(match[1] + 'ff', 16) >>> 0;
}
if (names[color] !== undefined) {
return names[color];
}
if ((match = MATCHERS.rgb.exec(color))) {
return (
// b
((parse255(match[1]) << 24) | // r
(parse255(match[2]) << 16) | // g
(parse255(match[3]) << 8) |
0x000000ff) >>> // a
0
);
}
if ((match = MATCHERS.rgba.exec(color))) {
return (
// b
((parse255(match[1]) << 24) | // r
(parse255(match[2]) << 16) | // g
(parse255(match[3]) << 8) |
parse1(match[4])) >>> // a
0
);
}
if ((match = MATCHERS.hex3.exec(color))) {
return (
Number.parseInt(
match[1] +
match[1] + // r
match[2] +
match[2] + // g
match[3] +
match[3] + // b
'ff', // a
16
) >>> 0
);
}
// https://drafts.csswg.org/css-color-4/#hex-notation
if ((match = MATCHERS.hex8.exec(color))) {
return Number.parseInt(match[1], 16) >>> 0;
}
if ((match = MATCHERS.hex4.exec(color))) {
return (
Number.parseInt(
match[1] +
match[1] + // r
match[2] +
match[2] + // g
match[3] +
match[3] + // b
match[4] +
match[4], // a
16
) >>> 0
);
}
if ((match = MATCHERS.hsl.exec(color))) {
return (
(hslToRgb(
parse360(match[1]), // h
parsePercentage(match[2]), // s
parsePercentage(match[3]) // l
) |
0x000000ff) >>> // a
0
);
}
if ((match = MATCHERS.hsla.exec(color))) {
return (
(hslToRgb(
parse360(match[1]), // h
parsePercentage(match[2]), // s
parsePercentage(match[3]) // l
) |
parse1(match[4])) >>> // a
0
);
}
return null;
}
export const opacity = (c: number): number => {
'worklet';
return ((c >> 24) & 255) / 255;
};
export const red = (c: number): number => {
'worklet';
return (c >> 16) & 255;
};
export const green = (c: number): number => {
'worklet';
return (c >> 8) & 255;
};
export const blue = (c: number): number => {
'worklet';
return c & 255;
};
const IS_WEB = isWeb();
const IS_ANDROID = isAndroid();
export const rgbaColor = (
r: number,
g: number,
b: number,
alpha = 1
): number | string => {
'worklet';
if (IS_WEB || !_WORKLET) {
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}
const c =
Math.round(alpha * 255) * (1 << 24) +
Math.round(r) * (1 << 16) +
Math.round(g) * (1 << 8) +
Math.round(b);
if (IS_ANDROID) {
// on Android color is represented as signed 32 bit int
return c < (1 << 31) >>> 0 ? c : c - 4294967296; // 4294967296 == Math.pow(2, 32);
}
return c;
};
/**
*
* @param r - red value (0-255)
* @param g - green value (0-255)
* @param b - blue value (0-255)
* @returns \{h: hue (0-1), s: saturation (0-1), v: value (0-1)\}
*/
export function RGBtoHSV(r: number, g: number, b: number): HSV {
'worklet';
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const d = max - min;
const s = max === 0 ? 0 : d / max;
const v = max / 255;
let h = 0;
switch (max) {
case min:
break;
case r:
h = g - b + d * (g < b ? 6 : 0);
h /= 6 * d;
break;
case g:
h = b - r + d * 2;
h /= 6 * d;
break;
case b:
h = r - g + d * 4;
h /= 6 * d;
break;
}
return { h, s, v };
}
/**
*
* @param h - hue (0-1)
* @param s - saturation (0-1)
* @param v - value (0-1)
* @returns \{r: red (0-255), g: green (0-255), b: blue (0-255)\}
*/
function HSVtoRGB(h: number, s: number, v: number): RGB {
'worklet';
let r, g, b;
const i = Math.floor(h * 6);
const f = h * 6 - i;
const p = v * (1 - s);
const q = v * (1 - f * s);
const t = v * (1 - (1 - f) * s);
switch ((i % 6) as 0 | 1 | 2 | 3 | 4 | 5) {
case 0:
[r, g, b] = [v, t, p];
break;
case 1:
[r, g, b] = [q, v, p];
break;
case 2:
[r, g, b] = [p, v, t];
break;
case 3:
[r, g, b] = [p, q, v];
break;
case 4:
[r, g, b] = [t, p, v];
break;
case 5:
[r, g, b] = [v, p, q];
break;
}
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255),
};
}
export const hsvToColor = (
h: number,
s: number,
v: number,
a: number
): number | string => {
'worklet';
const { r, g, b } = HSVtoRGB(h, s, v);
return rgbaColor(r, g, b, a);
};
function processColorInitially(color: unknown): number | null | undefined {
'worklet';
if (color === null || color === undefined || typeof color === 'number') {
return color;
}
let normalizedColor = normalizeColor(color);
if (normalizedColor === null || normalizedColor === undefined) {
return undefined;
}
if (typeof normalizedColor !== 'number') {
return null;
}
normalizedColor = ((normalizedColor << 24) | (normalizedColor >>> 8)) >>> 0; // argb
return normalizedColor;
}
export function isColor(value: unknown): boolean {
'worklet';
if (typeof value !== 'string') {
return false;
}
return processColorInitially(value) != null;
}
export function processColor(color: unknown): number | null | undefined {
'worklet';
let normalizedColor = processColorInitially(color);
if (normalizedColor === null || normalizedColor === undefined) {
return undefined;
}
if (typeof normalizedColor !== 'number') {
return null;
}
if (IS_ANDROID) {
// Android use 32 bit *signed* integer to represent the color
// We utilize the fact that bitwise operations in JS also operates on
// signed 32 bit integers, so that we can use those to convert from
// *unsigned* to *signed* 32bit int that way.
normalizedColor = normalizedColor | 0x0;
}
return normalizedColor;
}
export function processColorsInProps(props: StyleProps) {
'worklet';
for (const key in props) {
if (ColorProperties.includes(key)) {
props[key] = processColor(props[key]);
}
}
}
export type ParsedColorArray = [number, number, number, number];
export function convertToRGBA(color: unknown): ParsedColorArray {
'worklet';
const processedColor = processColorInitially(color)!; // argb;
const a = (processedColor >>> 24) / 255;
const r = ((processedColor << 8) >>> 24) / 255;
const g = ((processedColor << 16) >>> 24) / 255;
const b = ((processedColor << 24) >>> 24) / 255;
return [r, g, b, a];
}
export function rgbaArrayToRGBAColor(RGBA: ParsedColorArray): string {
'worklet';
return `rgba(${Math.round(RGBA[0] * 255)}, ${Math.round(
RGBA[1] * 255
)}, ${Math.round(RGBA[2] * 255)}, ${RGBA[3]})`;
}
export function toLinearSpace(
RGBA: ParsedColorArray,
gamma = 2.2
): ParsedColorArray {
'worklet';
const res = [];
for (let i = 0; i < 3; ++i) {
res.push(Math.pow(RGBA[i], gamma));
}
res.push(RGBA[3]);
return res as ParsedColorArray;
}
export function toGammaSpace(
RGBA: ParsedColorArray,
gamma = 2.2
): ParsedColorArray {
'worklet';
const res = [];
for (let i = 0; i < 3; ++i) {
res.push(Math.pow(RGBA[i], 1 / gamma));
}
res.push(RGBA[3]);
return res as ParsedColorArray;
}

View File

@@ -0,0 +1,316 @@
'use strict';
import { Bezier } from './Bezier';
/**
* The `Easing` module implements common easing functions. This module is used
* by [Animate.timing()](docs/animate.html#timing) to convey physically
* believable motion in animations.
*
* You can find a visualization of some common easing functions at
* http://easings.net/
*
* ### Predefined animations
*
* The `Easing` module provides several predefined animations through the
* following methods:
*
* - [`back`](docs/easing.html#back) provides a simple animation where the
* object goes slightly back before moving forward
* - [`bounce`](docs/easing.html#bounce) provides a bouncing animation
* - [`ease`](docs/easing.html#ease) provides a simple inertial animation
* - [`elastic`](docs/easing.html#elastic) provides a simple spring interaction
*
* ### Standard functions
*
* Three standard easing functions are provided:
*
* - [`linear`](docs/easing.html#linear)
* - [`quad`](docs/easing.html#quad)
* - [`cubic`](docs/easing.html#cubic)
*
* The [`poly`](docs/easing.html#poly) function can be used to implement
* quartic, quintic, and other higher power functions.
*
* ### Additional functions
*
* Additional mathematical functions are provided by the following methods:
*
* - [`bezier`](docs/easing.html#bezier) provides a cubic bezier curve
* - [`circle`](docs/easing.html#circle) provides a circular function
* - [`sin`](docs/easing.html#sin) provides a sinusoidal function
* - [`exp`](docs/easing.html#exp) provides an exponential function
*
* The following helpers are used to modify other easing functions.
*
* - [`in`](docs/easing.html#in) runs an easing function forwards
* - [`inOut`](docs/easing.html#inout) makes any easing function symmetrical
* - [`out`](docs/easing.html#out) runs an easing function backwards
*/
export type EasingFunction = (t: number) => number;
/**
* @deprecated Please use {@link EasingFunction} type instead.
*/
export type EasingFn = EasingFunction;
export type EasingFunctionFactory = { factory: () => EasingFunction };
/**
* @deprecated Please use {@link EasingFunctionFactory} type instead.
*/
export type EasingFactoryFn = EasingFunctionFactory;
/**
* A linear function, `f(t) = t`. Position correlates to elapsed time one to
* one.
*
* http://cubic-bezier.com/#0,0,1,1
*/
function linear(t: number): number {
'worklet';
return t;
}
/**
* A simple inertial interaction, similar to an object slowly accelerating to
* speed.
*
* http://cubic-bezier.com/#.42,0,1,1
*/
function ease(t: number): number {
'worklet';
return Bezier(0.42, 0, 1, 1)(t);
}
/**
* A quadratic function, `f(t) = t * t`. Position equals the square of elapsed
* time.
*
* http://easings.net/#easeInQuad
*/
function quad(t: number): number {
'worklet';
return t * t;
}
/**
* A cubic function, `f(t) = t * t * t`. Position equals the cube of elapsed
* time.
*
* http://easings.net/#easeInCubic
*/
function cubic(t: number): number {
'worklet';
return t * t * t;
}
/**
* A power function. Position is equal to the Nth power of elapsed time.
*
* n = 4: http://easings.net/#easeInQuart
* n = 5: http://easings.net/#easeInQuint
*/
function poly(n: number): EasingFunction {
'worklet';
return (t) => {
'worklet';
return Math.pow(t, n);
};
}
/**
* A sinusoidal function.
*
* http://easings.net/#easeInSine
*/
function sin(t: number): number {
'worklet';
return 1 - Math.cos((t * Math.PI) / 2);
}
/**
* A circular function.
*
* http://easings.net/#easeInCirc
*/
function circle(t: number): number {
'worklet';
return 1 - Math.sqrt(1 - t * t);
}
/**
* An exponential function.
*
* http://easings.net/#easeInExpo
*/
function exp(t: number): number {
'worklet';
return Math.pow(2, 10 * (t - 1));
}
/**
* A simple elastic interaction, similar to a spring oscillating back and
* forth.
*
* Default bounciness is 1, which overshoots a little bit once. 0 bounciness
* doesn't overshoot at all, and bounciness of N \> 1 will overshoot about N
* times.
*
* http://easings.net/#easeInElastic
*/
function elastic(bounciness = 1): EasingFunction {
'worklet';
const p = bounciness * Math.PI;
return (t) => {
'worklet';
return 1 - Math.pow(Math.cos((t * Math.PI) / 2), 3) * Math.cos(t * p);
};
}
/**
* Use with `Animated.parallel()` to create a simple effect where the object
* animates back slightly as the animation starts.
*
* Wolfram Plot:
*
* - http://tiny.cc/back_default (s = 1.70158, default)
*/
function back(s = 1.70158): (t: number) => number {
'worklet';
return (t) => {
'worklet';
return t * t * ((s + 1) * t - s);
};
}
/**
* Provides a simple bouncing effect.
*
* http://easings.net/#easeInBounce
*/
function bounce(t: number): number {
'worklet';
if (t < 1 / 2.75) {
return 7.5625 * t * t;
}
if (t < 2 / 2.75) {
const t2 = t - 1.5 / 2.75;
return 7.5625 * t2 * t2 + 0.75;
}
if (t < 2.5 / 2.75) {
const t2 = t - 2.25 / 2.75;
return 7.5625 * t2 * t2 + 0.9375;
}
const t2 = t - 2.625 / 2.75;
return 7.5625 * t2 * t2 + 0.984375;
}
/**
* Provides a cubic bezier curve, equivalent to CSS Transitions'
* `transition-timing-function`.
*
* A useful tool to visualize cubic bezier curves can be found at
* http://cubic-bezier.com/
*/
function bezier(
x1: number,
y1: number,
x2: number,
y2: number
): { factory: () => (x: number) => number } {
'worklet';
return {
factory: () => {
'worklet';
return Bezier(x1, y1, x2, y2);
},
};
}
function bezierFn(
x1: number,
y1: number,
x2: number,
y2: number
): (x: number) => number {
'worklet';
return Bezier(x1, y1, x2, y2);
}
/**
* Runs an easing function forwards.
*/
function in_(easing: EasingFunction): EasingFunction {
'worklet';
return easing;
}
/**
* Runs an easing function backwards.
*/
function out(easing: EasingFunction): EasingFunction {
'worklet';
return (t) => {
'worklet';
return 1 - easing(1 - t);
};
}
/**
* Makes any easing function symmetrical. The easing function will run
* forwards for half of the duration, then backwards for the rest of the
* duration.
*/
function inOut(easing: EasingFunction): EasingFunction {
'worklet';
return (t) => {
'worklet';
if (t < 0.5) {
return easing(t * 2) / 2;
}
return 1 - easing((1 - t) * 2) / 2;
};
}
/**
* The `steps` easing function jumps between discrete values at regular intervals,
* creating a stepped animation effect. The `n` parameter determines the number of
* steps in the animation, and the `roundToNextStep` parameter determines whether the animation
* should start at the beginning or end of each step.
*/
function steps(n = 10, roundToNextStep = true): EasingFunction {
'worklet';
return (t) => {
'worklet';
const value = Math.min(Math.max(t, 0), 1) * n;
if (roundToNextStep) {
return Math.ceil(value) / n;
}
return Math.floor(value) / n;
};
}
const EasingObject = {
linear,
ease,
quad,
cubic,
poly,
sin,
circle,
exp,
elastic,
back,
bounce,
bezier,
bezierFn,
steps,
in: in_,
out,
inOut,
};
export const Easing = EasingObject;

View File

@@ -0,0 +1,226 @@
'use strict';
import type {
ShadowNodeWrapper,
Value3D,
ValueRotation,
ShareableRef,
} from '../commonTypes';
import { checkCppVersion } from '../platform-specific/checkCppVersion';
import { jsVersion } from '../platform-specific/jsVersion';
import type { WorkletRuntime } from '../runtimes';
import { getValueUnpackerCode } from '../valueUnpacker';
import { isFabric } from '../PlatformChecker';
import type React from 'react';
import { getShadowNodeWrapperFromRef } from '../fabricUtils';
import type { LayoutAnimationBatchItem } from '../layoutReanimation/animationBuilder/commonTypes';
import ReanimatedModule from '../../specs/NativeReanimatedModule';
// this is the type of `__reanimatedModuleProxy` which is injected using JSI
export interface NativeReanimatedModule {
makeShareableClone<T>(
value: T,
shouldPersistRemote: boolean,
nativeStateSource?: object
): ShareableRef<T>;
scheduleOnUI<T>(shareable: ShareableRef<T>): void;
executeOnUIRuntimeSync<T, R>(shareable: ShareableRef<T>): R;
createWorkletRuntime(
name: string,
initializer: ShareableRef<() => void>
): WorkletRuntime;
scheduleOnRuntime<T>(
workletRuntime: WorkletRuntime,
worklet: ShareableRef<T>
): void;
registerEventHandler<T>(
eventHandler: ShareableRef<T>,
eventName: string,
emitterReactTag: number
): number;
unregisterEventHandler(id: number): void;
getViewProp<T>(
viewTagOrShadowNodeWrapper: number | ShadowNodeWrapper,
propName: string,
callback?: (result: T) => void
): Promise<T>;
enableLayoutAnimations(flag: boolean): void;
registerSensor(
sensorType: number,
interval: number,
iosReferenceFrame: number,
handler: ShareableRef<(data: Value3D | ValueRotation) => void>
): number;
unregisterSensor(sensorId: number): void;
configureProps(uiProps: string[], nativeProps: string[]): void;
subscribeForKeyboardEvents(
handler: ShareableRef<number>,
isStatusBarTranslucent: boolean
): number;
unsubscribeFromKeyboardEvents(listenerId: number): void;
configureLayoutAnimationBatch(
layoutAnimationsBatch: LayoutAnimationBatchItem[]
): void;
setShouldAnimateExitingForTag(viewTag: number, shouldAnimate: boolean): void;
}
function assertSingleReanimatedInstance() {
if (
global._REANIMATED_VERSION_JS !== undefined &&
global._REANIMATED_VERSION_JS !== jsVersion
) {
throw new Error(
`[Reanimated] Another instance of Reanimated was detected.
See \`https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#another-instance-of-reanimated-was-detected\` for more details. Previous: ${global._REANIMATED_VERSION_JS}, current: ${jsVersion}.`
);
}
}
export class NativeReanimated {
private InnerNativeModule: NativeReanimatedModule;
constructor() {
// These checks have to split since version checking depend on the execution order
if (__DEV__) {
assertSingleReanimatedInstance();
}
global._REANIMATED_VERSION_JS = jsVersion;
if (global.__reanimatedModuleProxy === undefined) {
const valueUnpackerCode = getValueUnpackerCode();
ReanimatedModule?.installTurboModule(valueUnpackerCode);
}
if (global.__reanimatedModuleProxy === undefined) {
throw new Error(
`[Reanimated] Native part of Reanimated doesn't seem to be initialized.
See https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#native-part-of-reanimated-doesnt-seem-to-be-initialized for more details.`
);
}
if (__DEV__) {
checkCppVersion();
}
this.InnerNativeModule = global.__reanimatedModuleProxy;
}
makeShareableClone<T>(
value: T,
shouldPersistRemote: boolean,
nativeStateSource?: object
) {
return this.InnerNativeModule.makeShareableClone(
value,
shouldPersistRemote,
nativeStateSource
);
}
scheduleOnUI<T>(shareable: ShareableRef<T>) {
return this.InnerNativeModule.scheduleOnUI(shareable);
}
executeOnUIRuntimeSync<T, R>(shareable: ShareableRef<T>): R {
return this.InnerNativeModule.executeOnUIRuntimeSync(shareable);
}
createWorkletRuntime(name: string, initializer: ShareableRef<() => void>) {
return this.InnerNativeModule.createWorkletRuntime(name, initializer);
}
scheduleOnRuntime<T>(
workletRuntime: WorkletRuntime,
shareableWorklet: ShareableRef<T>
) {
return this.InnerNativeModule.scheduleOnRuntime(
workletRuntime,
shareableWorklet
);
}
registerSensor(
sensorType: number,
interval: number,
iosReferenceFrame: number,
handler: ShareableRef<(data: Value3D | ValueRotation) => void>
) {
return this.InnerNativeModule.registerSensor(
sensorType,
interval,
iosReferenceFrame,
handler
);
}
unregisterSensor(sensorId: number) {
return this.InnerNativeModule.unregisterSensor(sensorId);
}
registerEventHandler<T>(
eventHandler: ShareableRef<T>,
eventName: string,
emitterReactTag: number
) {
return this.InnerNativeModule.registerEventHandler(
eventHandler,
eventName,
emitterReactTag
);
}
unregisterEventHandler(id: number) {
return this.InnerNativeModule.unregisterEventHandler(id);
}
getViewProp<T>(
viewTag: number,
propName: string,
component: React.Component | undefined, // required on Fabric
callback?: (result: T) => void
) {
let shadowNodeWrapper;
if (isFabric()) {
shadowNodeWrapper = getShadowNodeWrapperFromRef(
component as React.Component
);
return this.InnerNativeModule.getViewProp(
shadowNodeWrapper,
propName,
callback
);
}
return this.InnerNativeModule.getViewProp(viewTag, propName, callback);
}
configureLayoutAnimationBatch(
layoutAnimationsBatch: LayoutAnimationBatchItem[]
) {
this.InnerNativeModule.configureLayoutAnimationBatch(layoutAnimationsBatch);
}
setShouldAnimateExitingForTag(viewTag: number, shouldAnimate: boolean) {
this.InnerNativeModule.setShouldAnimateExitingForTag(
viewTag,
shouldAnimate
);
}
enableLayoutAnimations(flag: boolean) {
this.InnerNativeModule.enableLayoutAnimations(flag);
}
configureProps(uiProps: string[], nativeProps: string[]) {
this.InnerNativeModule.configureProps(uiProps, nativeProps);
}
subscribeForKeyboardEvents(
handler: ShareableRef<number>,
isStatusBarTranslucent: boolean
) {
return this.InnerNativeModule.subscribeForKeyboardEvents(
handler,
isStatusBarTranslucent
);
}
unsubscribeFromKeyboardEvents(listenerId: number) {
this.InnerNativeModule.unsubscribeFromKeyboardEvents(listenerId);
}
}

View File

@@ -0,0 +1,6 @@
'use strict';
import reanimatedJS from '../js-reanimated';
import { shouldBeUseWeb } from '../PlatformChecker';
import { NativeReanimated } from './NativeReanimated';
export default shouldBeUseWeb() ? reanimatedJS : new NativeReanimated();

View File

@@ -0,0 +1,3 @@
'use strict';
// this file was created to prevent NativeReanimated from being included in the web bundle
export { default } from '../js-reanimated';

View File

@@ -0,0 +1,56 @@
'use strict';
import { Platform } from 'react-native';
// This type is necessary since some libraries tend to do a lib check
// and this file causes type errors on `global` access.
type localGlobal = typeof global & Record<string, unknown>;
export function isJest(): boolean {
return !!process.env.JEST_WORKER_ID;
}
// `isChromeDebugger` also returns true in Jest environment, so `isJest()` check should always be performed first
export function isChromeDebugger(): boolean {
return (
(!(global as localGlobal).nativeCallSyncHook ||
!!(global as localGlobal).__REMOTEDEV__) &&
!(global as localGlobal).RN$Bridgeless
);
}
export function isWeb(): boolean {
return Platform.OS === 'web';
}
export function isAndroid(): boolean {
return Platform.OS === 'android';
}
function isWindows(): boolean {
return Platform.OS === 'windows';
}
export function shouldBeUseWeb() {
return isJest() || isChromeDebugger() || isWeb() || isWindows();
}
export function isFabric() {
return !!(global as localGlobal)._IS_FABRIC;
}
export function isWindowAvailable() {
// the window object is unavailable when building the server portion of a site that uses SSG
// this function shouldn't be used to conditionally render components
// https://www.joshwcomeau.com/react/the-perils-of-rehydration/
// @ts-ignore Fallback if `window` is undefined.
return typeof window !== 'undefined';
}
export function isReducedMotion() {
return isWeb()
? isWindowAvailable()
? // @ts-ignore Fallback if `window` is undefined.
!window.matchMedia('(prefers-reduced-motion: no-preference)').matches
: false
: !!(global as localGlobal)._REANIMATED_IS_REDUCED_MOTION;
}

View File

@@ -0,0 +1,24 @@
'use strict';
import { addWhitelistedNativeProps } from '../ConfigHelper';
import type {
AnimatedPropsAdapterFunction,
AnimatedPropsAdapterWorklet,
} from './commonTypes';
// @ts-expect-error This overload is required by our API.
export function createAnimatedPropAdapter(
adapter: AnimatedPropsAdapterFunction,
nativeProps?: string[]
): AnimatedPropsAdapterFunction;
export function createAnimatedPropAdapter(
adapter: AnimatedPropsAdapterWorklet,
nativeProps?: string[]
): AnimatedPropsAdapterWorklet {
const nativePropsToAdd: { [key: string]: boolean } = {};
nativeProps?.forEach((prop) => {
nativePropsToAdd[prop] = true;
});
addWhitelistedNativeProps(nativePropsToAdd);
return adapter;
}

View File

@@ -0,0 +1,25 @@
'use strict';
import { isFabric } from './PlatformChecker';
import { runOnUI } from './threads';
let VIEW_TAGS: number[] = [];
export function removeFromPropsRegistry(viewTag: number) {
VIEW_TAGS.push(viewTag);
if (VIEW_TAGS.length === 1) {
queueMicrotask(flush);
}
}
function flush() {
if (__DEV__ && !isFabric()) {
throw new Error('[Reanimated] PropsRegistry is only available on Fabric.');
}
runOnUI(removeFromPropsRegistryOnUI)(VIEW_TAGS);
VIEW_TAGS = [];
}
function removeFromPropsRegistryOnUI(viewTags: number[]) {
'worklet';
global._removeFromPropsRegistry(viewTags);
}

View File

@@ -0,0 +1,82 @@
'use strict';
import NativeReanimatedModule from './NativeReanimated';
import type {
SensorConfig,
SharedValue,
Value3D,
ValueRotation,
ShareableRef,
} from './commonTypes';
import { SensorType } from './commonTypes';
import { makeMutable } from './mutables';
function initSensorData(
sensorType: SensorType
): SharedValue<Value3D | ValueRotation> {
if (sensorType === SensorType.ROTATION) {
return makeMutable<Value3D | ValueRotation>({
qw: 0,
qx: 0,
qy: 0,
qz: 0,
yaw: 0,
pitch: 0,
roll: 0,
interfaceOrientation: 0,
});
} else {
return makeMutable<Value3D | ValueRotation>({
x: 0,
y: 0,
z: 0,
interfaceOrientation: 0,
});
}
}
export default class Sensor {
public listenersNumber = 0;
private sensorId: number | null = null;
private sensorType: SensorType;
private data: SharedValue<Value3D | ValueRotation>;
private config: SensorConfig;
constructor(sensorType: SensorType, config: SensorConfig) {
this.sensorType = sensorType;
this.config = config;
this.data = initSensorData(sensorType);
}
register(
eventHandler: ShareableRef<(data: Value3D | ValueRotation) => void>
) {
const config = this.config;
const sensorType = this.sensorType;
this.sensorId = NativeReanimatedModule.registerSensor(
sensorType,
config.interval === 'auto' ? -1 : config.interval,
config.iosReferenceFrame,
eventHandler
);
return this.sensorId !== -1;
}
isRunning() {
return this.sensorId !== -1 && this.sensorId !== null;
}
isAvailable() {
return this.sensorId !== -1;
}
getSharedValue() {
return this.data;
}
unregister() {
if (this.sensorId !== null && this.sensorId !== -1) {
NativeReanimatedModule.unregisterSensor(this.sensorId);
}
this.sensorId = null;
}
}

View File

@@ -0,0 +1,72 @@
'use strict';
import type {
SensorType,
SensorConfig,
Value3D,
ValueRotation,
ShareableRef,
SharedValue,
} from './commonTypes';
import Sensor from './Sensor';
export class SensorContainer {
private nativeSensors: Map<number, Sensor> = new Map();
getSensorId(sensorType: SensorType, config: SensorConfig) {
return (
sensorType * 100 +
config.iosReferenceFrame * 10 +
Number(config.adjustToInterfaceOrientation)
);
}
initializeSensor(
sensorType: SensorType,
config: SensorConfig
): SharedValue<Value3D | ValueRotation> {
const sensorId = this.getSensorId(sensorType, config);
if (!this.nativeSensors.has(sensorId)) {
const newSensor = new Sensor(sensorType, config);
this.nativeSensors.set(sensorId, newSensor);
}
const sensor = this.nativeSensors.get(sensorId);
return sensor!.getSharedValue();
}
registerSensor(
sensorType: SensorType,
config: SensorConfig,
handler: ShareableRef<(data: Value3D | ValueRotation) => void>
): number {
const sensorId = this.getSensorId(sensorType, config);
if (!this.nativeSensors.has(sensorId)) {
return -1;
}
const sensor = this.nativeSensors.get(sensorId);
if (
sensor &&
sensor.isAvailable() &&
(sensor.isRunning() || sensor.register(handler))
) {
sensor.listenersNumber++;
return sensorId;
}
return -1;
}
unregisterSensor(sensorId: number) {
if (this.nativeSensors.has(sensorId)) {
const sensor = this.nativeSensors.get(sensorId);
if (sensor && sensor.isRunning()) {
sensor.listenersNumber--;
if (sensor.listenersNumber === 0) {
sensor.unregister();
}
}
}
}
}

View File

@@ -0,0 +1,87 @@
'use strict';
import { shouldBeUseWeb } from './PlatformChecker';
import {
configureLayoutAnimationBatch,
makeShareableCloneRecursive,
} from './core';
import type {
LayoutAnimationFunction,
LayoutAnimationType,
} from './layoutReanimation';
import type {
LayoutAnimationBatchItem,
ProgressAnimationCallback,
SharedTransitionAnimationsFunction,
} from './layoutReanimation/animationBuilder/commonTypes';
function createUpdateManager() {
const animations: LayoutAnimationBatchItem[] = [];
// When a stack is rerendered we reconfigure all the shared elements.
// To do that we want them to appear in our batch in the correct order,
// so we defer some of the updates to appear at the end of the batch.
const deferredAnimations: LayoutAnimationBatchItem[] = [];
return {
update(batchItem: LayoutAnimationBatchItem, isUnmounting?: boolean) {
if (isUnmounting) {
deferredAnimations.push(batchItem);
} else {
animations.push(batchItem);
}
if (animations.length + deferredAnimations.length === 1) {
setImmediate(this.flush);
}
},
flush(this: void) {
configureLayoutAnimationBatch(animations.concat(deferredAnimations));
animations.length = 0;
deferredAnimations.length = 0;
},
};
}
/**
* Lets you update the current configuration of the layout animation or shared element transition for a given component.
* Configurations are batched and applied at the end of the current execution block, right before sending the response back to native.
*
* @param viewTag - The tag of the component you'd like to configure.
* @param type - The type of the animation you'd like to configure - {@link LayoutAnimationType}.
* @param config - The animation configuration - {@link LayoutAnimationFunction}, {@link SharedTransitionAnimationsFunction}, {@link ProgressAnimationCallback} or {@link Keyframe}. Passing `undefined` will remove the animation.
* @param sharedTransitionTag - The tag of the shared element transition you'd like to configure. Passing `undefined` will remove the transition.
* @param isUnmounting - Determines whether the configuration should be included at the end of the batch, after all the non-deferred configurations (even those that were updated later). This is used to retain the correct ordering of shared elements. Defaults to `false`.
*/
export let updateLayoutAnimations: (
viewTag: number,
type: LayoutAnimationType,
config?:
| Keyframe
| LayoutAnimationFunction
| SharedTransitionAnimationsFunction
| ProgressAnimationCallback,
sharedTransitionTag?: string,
isUnmounting?: boolean
) => void;
if (shouldBeUseWeb()) {
updateLayoutAnimations = () => {
// no-op
};
} else {
const updateLayoutAnimationsManager = createUpdateManager();
updateLayoutAnimations = (
viewTag,
type,
config,
sharedTransitionTag,
isUnmounting
) =>
updateLayoutAnimationsManager.update(
{
viewTag,
type,
config: config ? makeShareableCloneRecursive(config) : undefined,
sharedTransitionTag,
},
isUnmounting
);
}

View File

@@ -0,0 +1,147 @@
/* eslint-disable @typescript-eslint/no-redundant-type-constituents */
'use strict';
import type { MutableRefObject } from 'react';
import { processColorsInProps } from './Colors';
import type { ShadowNodeWrapper, SharedValue, StyleProps } from './commonTypes';
import type { AnimatedStyle } from './helperTypes';
import type { Descriptor } from './hook/commonTypes';
import { _updatePropsJS } from './js-reanimated';
import { isFabric, isJest, shouldBeUseWeb } from './PlatformChecker';
import type { ViewRefSet } from './ViewDescriptorsSet';
import { runOnUIImmediately } from './threads';
let updateProps: (
viewDescriptor: SharedValue<Descriptor[]>,
updates: StyleProps | AnimatedStyle<any>,
maybeViewRef: ViewRefSet<any> | undefined,
isAnimatedProps?: boolean
) => void;
if (shouldBeUseWeb()) {
updateProps = (_, updates, maybeViewRef, isAnimatedProps) => {
'worklet';
if (maybeViewRef) {
maybeViewRef.items.forEach((item, _index) => {
_updatePropsJS(updates, item, isAnimatedProps);
});
}
};
} else {
updateProps = (viewDescriptors, updates) => {
'worklet';
processColorsInProps(updates);
global.UpdatePropsManager.update(viewDescriptors, updates);
};
}
export const updatePropsJestWrapper = (
viewDescriptors: SharedValue<Descriptor[]>,
updates: AnimatedStyle<any>,
maybeViewRef: ViewRefSet<any> | undefined,
animatedStyle: MutableRefObject<AnimatedStyle<any>>,
adapters: ((updates: AnimatedStyle<any>) => void)[]
): void => {
adapters.forEach((adapter) => {
adapter(updates);
});
animatedStyle.current.value = {
...animatedStyle.current.value,
...updates,
};
updateProps(viewDescriptors, updates, maybeViewRef);
};
export default updateProps;
const createUpdatePropsManager = isFabric()
? () => {
'worklet';
// Fabric
const operations: {
shadowNodeWrapper: ShadowNodeWrapper;
updates: StyleProps | AnimatedStyle<any>;
}[] = [];
return {
update(
viewDescriptors: SharedValue<Descriptor[]>,
updates: StyleProps | AnimatedStyle<any>
) {
viewDescriptors.value.forEach((viewDescriptor) => {
operations.push({
shadowNodeWrapper: viewDescriptor.shadowNodeWrapper,
updates,
});
if (operations.length === 1) {
queueMicrotask(this.flush);
}
});
},
flush(this: void) {
global._updatePropsFabric!(operations);
operations.length = 0;
},
};
}
: () => {
'worklet';
// Paper
const operations: {
tag: number;
name: string;
updates: StyleProps | AnimatedStyle<any>;
}[] = [];
return {
update(
viewDescriptors: SharedValue<Descriptor[]>,
updates: StyleProps | AnimatedStyle<any>
) {
viewDescriptors.value.forEach((viewDescriptor) => {
operations.push({
tag: viewDescriptor.tag,
name: viewDescriptor.name || 'RCTView',
updates,
});
if (operations.length === 1) {
queueMicrotask(this.flush);
}
});
},
flush(this: void) {
global._updatePropsPaper!(operations);
operations.length = 0;
},
};
};
if (shouldBeUseWeb()) {
const maybeThrowError = () => {
// Jest attempts to access a property of this object to check if it is a Jest mock
// so we can't throw an error in the getter.
if (!isJest()) {
throw new Error(
'[Reanimated] `UpdatePropsManager` is not available on non-native platform.'
);
}
};
global.UpdatePropsManager = new Proxy({} as UpdatePropsManager, {
get: maybeThrowError,
set: () => {
maybeThrowError();
return false;
},
});
} else {
runOnUIImmediately(() => {
'worklet';
global.UpdatePropsManager = createUpdatePropsManager();
})();
}
export interface UpdatePropsManager {
update(
viewDescriptors: SharedValue<Descriptor[]>,
updates: StyleProps | AnimatedStyle<any>
): void;
flush(): void;
}

View File

@@ -0,0 +1,85 @@
'use strict';
import { useRef } from 'react';
import { makeMutable } from './core';
import type { SharedValue } from './commonTypes';
import type { Descriptor } from './hook/commonTypes';
import { shouldBeUseWeb } from './PlatformChecker';
export interface ViewRefSet<T> {
items: Set<T>;
add: (item: T) => void;
remove: (item: T) => void;
}
export interface ViewDescriptorsSet {
shareableViewDescriptors: SharedValue<Descriptor[]>;
add: (item: Descriptor) => void;
remove: (viewTag: number) => void;
}
export function makeViewDescriptorsSet(): ViewDescriptorsSet {
const shareableViewDescriptors = makeMutable<Descriptor[]>([]);
const data: ViewDescriptorsSet = {
shareableViewDescriptors,
add: (item: Descriptor) => {
shareableViewDescriptors.modify((descriptors) => {
'worklet';
const index = descriptors.findIndex(
(descriptor) => descriptor.tag === item.tag
);
if (index !== -1) {
descriptors[index] = item;
} else {
descriptors.push(item);
}
return descriptors;
}, false);
},
remove: (viewTag: number) => {
shareableViewDescriptors.modify((descriptors) => {
'worklet';
const index = descriptors.findIndex(
(descriptor) => descriptor.tag === viewTag
);
if (index !== -1) {
descriptors.splice(index, 1);
}
return descriptors;
}, false);
},
};
return data;
}
const SHOULD_BE_USE_WEB = shouldBeUseWeb();
export const useViewRefSet = SHOULD_BE_USE_WEB
? useViewRefSetJS
: useViewRefSetNative;
function useViewRefSetNative() {
// Stub native implementation.
return undefined;
}
function useViewRefSetJS<T>(): ViewRefSet<T> {
const ref = useRef<ViewRefSet<T> | null>(null);
if (ref.current === null) {
const data: ViewRefSet<T> = {
items: new Set(),
add: (item: T) => {
if (data.items.has(item)) return;
data.items.add(item);
},
remove: (item: T) => {
data.items.delete(item);
},
};
ref.current = data;
}
return ref.current;
}

View File

@@ -0,0 +1,142 @@
'use strict';
import type { NativeSyntheticEvent } from 'react-native';
import { registerEventHandler, unregisterEventHandler } from './core';
import type {
EventPayload,
ReanimatedEvent,
IWorkletEventHandler,
} from './hook/commonTypes';
import { shouldBeUseWeb } from './PlatformChecker';
const SHOULD_BE_USE_WEB = shouldBeUseWeb();
type JSEvent<Event extends object> = NativeSyntheticEvent<EventPayload<Event>>;
// In JS implementation (e.g. for web) we don't use Reanimated's
// event emitter, therefore we have to handle here
// the event that came from React Native and convert it.
function jsListener<Event extends object>(
eventName: string,
handler: (event: ReanimatedEvent<Event>) => void
) {
return (evt: JSEvent<Event>) => {
handler({ ...evt.nativeEvent, eventName } as ReanimatedEvent<Event>);
};
}
class WorkletEventHandlerNative<Event extends object>
implements IWorkletEventHandler<Event>
{
eventNames: string[];
worklet: (event: ReanimatedEvent<Event>) => void;
#viewTags: Set<number>;
#registrations: Map<number, number[]>; // keys are viewTags, values are arrays of registration ID's for each viewTag
constructor(
worklet: (event: ReanimatedEvent<Event>) => void,
eventNames: string[]
) {
this.worklet = worklet;
this.eventNames = eventNames;
this.#viewTags = new Set<number>();
this.#registrations = new Map<number, number[]>();
}
updateEventHandler(
newWorklet: (event: ReanimatedEvent<Event>) => void,
newEvents: string[]
): void {
// Update worklet and event names
this.worklet = newWorklet;
this.eventNames = newEvents;
// Detach all events
this.#registrations.forEach((registrationIDs) => {
registrationIDs.forEach((id) => unregisterEventHandler(id));
// No need to remove registrationIDs from map, since it gets overwritten when attaching
});
// Attach new events with new worklet
Array.from(this.#viewTags).forEach((tag) => {
const newRegistrations = this.eventNames.map((eventName) =>
registerEventHandler(this.worklet, eventName, tag)
);
this.#registrations.set(tag, newRegistrations);
});
}
registerForEvents(viewTag: number, fallbackEventName?: string): void {
this.#viewTags.add(viewTag);
const newRegistrations = this.eventNames.map((eventName) =>
registerEventHandler(this.worklet, eventName, viewTag)
);
this.#registrations.set(viewTag, newRegistrations);
if (this.eventNames.length === 0 && fallbackEventName) {
const newRegistration = registerEventHandler(
this.worklet,
fallbackEventName,
viewTag
);
this.#registrations.set(viewTag, [newRegistration]);
}
}
unregisterFromEvents(viewTag: number): void {
this.#viewTags.delete(viewTag);
this.#registrations.get(viewTag)?.forEach((id) => {
unregisterEventHandler(id);
});
this.#registrations.delete(viewTag);
}
}
class WorkletEventHandlerWeb<Event extends object>
implements IWorkletEventHandler<Event>
{
eventNames: string[];
listeners:
| Record<string, (event: ReanimatedEvent<ReanimatedEvent<Event>>) => void>
| Record<string, (event: JSEvent<Event>) => void>;
worklet: (event: ReanimatedEvent<Event>) => void;
constructor(
worklet: (event: ReanimatedEvent<Event>) => void,
eventNames: string[] = []
) {
this.worklet = worklet;
this.eventNames = eventNames;
this.listeners = {};
this.setupWebListeners();
}
setupWebListeners() {
this.listeners = {};
this.eventNames.forEach((eventName) => {
this.listeners[eventName] = jsListener(eventName, this.worklet);
});
}
updateEventHandler(
newWorklet: (event: ReanimatedEvent<Event>) => void,
newEvents: string[]
): void {
// Update worklet and event names
this.worklet = newWorklet;
this.eventNames = newEvents;
this.setupWebListeners();
}
registerForEvents(_viewTag: number, _fallbackEventName?: string): void {
// noop
}
unregisterFromEvents(_viewTag: number): void {
// noop
}
}
export const WorkletEventHandler = SHOULD_BE_USE_WEB
? WorkletEventHandlerWeb
: WorkletEventHandlerNative;

View File

@@ -0,0 +1,132 @@
'use strict';
import {
defineAnimation,
getReduceMotionForAnimation,
recognizePrefixSuffix,
} from './util';
import type {
Animation,
Timestamp,
AnimatableValue,
AnimationObject,
ReduceMotion,
} from '../commonTypes';
import type { ClampAnimation } from './commonTypes';
type withClampType = <T extends number | string>(
config: {
min?: T;
max?: T;
},
clampedAnimation: T
) => T;
export const withClamp = function <T extends number | string>(
config: { min?: T; max?: T; reduceMotion?: ReduceMotion },
_animationToClamp: AnimationObject<T> | (() => AnimationObject<T>)
): Animation<ClampAnimation> {
'worklet';
return defineAnimation<ClampAnimation, AnimationObject<T>>(
_animationToClamp,
(): ClampAnimation => {
'worklet';
const animationToClamp =
typeof _animationToClamp === 'function'
? _animationToClamp()
: _animationToClamp;
const strippedMin =
config.min === undefined
? undefined
: recognizePrefixSuffix(config.min).strippedValue;
const strippedMax =
config.max === undefined
? undefined
: recognizePrefixSuffix(config.max).strippedValue;
function clampOnFrame(
animation: ClampAnimation,
now: Timestamp
): boolean {
const finished = animationToClamp.onFrame(animationToClamp, now);
if (animationToClamp.current === undefined) {
console.warn(
"[Reanimated] Error inside 'withClamp' animation, the inner animation has invalid current value"
);
return true;
} else {
const { prefix, strippedValue, suffix } = recognizePrefixSuffix(
animationToClamp.current
);
let newValue;
if (strippedMax !== undefined && strippedMax < strippedValue) {
newValue = strippedMax;
} else if (strippedMin !== undefined && strippedMin > strippedValue) {
newValue = strippedMin;
} else {
newValue = strippedValue;
}
animation.current =
typeof animationToClamp.current === 'number'
? newValue
: `${prefix === undefined ? '' : prefix}${newValue}${
suffix === undefined ? '' : suffix
}`;
}
return finished;
}
function onStart(
animation: Animation<any>,
value: AnimatableValue,
now: Timestamp,
previousAnimation: Animation<any> | null
): void {
animation.current = value;
animation.previousAnimation = animationToClamp;
const animationBeforeClamped = previousAnimation?.previousAnimation;
if (
config.max !== undefined &&
config.min !== undefined &&
config.max < config.min
) {
console.warn(
'[Reanimated] Wrong config was provided to withClamp. Min value is bigger than max'
);
}
animationToClamp.onStart(
animationToClamp,
/** provide the current value of the previous animation of the clamped animation
so we can animate from the original "un-truncated" value
*/
animationBeforeClamped?.current || value,
now,
animationBeforeClamped
);
}
const callback = (finished?: boolean): void => {
if (animationToClamp.callback) {
animationToClamp.callback(finished);
}
};
return {
isHigherOrder: true,
onFrame: clampOnFrame,
onStart,
current: animationToClamp.current!,
callback,
previousAnimation: null,
reduceMotion: getReduceMotionForAnimation(config.reduceMotion),
};
}
);
} as withClampType;

View File

@@ -0,0 +1,59 @@
'use strict';
import type {
StyleProps,
AnimatableValue,
AnimationObject,
Animation,
Timestamp,
AnimationCallback,
} from '../commonTypes';
import type { AnimatedStyle } from '../helperTypes';
export interface HigherOrderAnimation {
isHigherOrder?: boolean;
}
export type NextAnimation<T extends AnimationObject> = T | (() => T);
export interface ClampAnimation
extends Animation<ClampAnimation>,
HigherOrderAnimation {
current: AnimatableValue;
}
export interface DelayAnimation
extends Animation<DelayAnimation>,
HigherOrderAnimation {
startTime: Timestamp;
started: boolean;
previousAnimation: DelayAnimation | null;
current: AnimatableValue;
}
export interface RepeatAnimation
extends Animation<RepeatAnimation>,
HigherOrderAnimation {
reps: number;
startValue: AnimatableValue;
toValue?: AnimatableValue;
previousAnimation?: RepeatAnimation;
}
export interface SequenceAnimation
extends Animation<SequenceAnimation>,
HigherOrderAnimation {
animationIndex: number;
}
export interface StyleLayoutAnimation extends HigherOrderAnimation {
current: StyleProps;
styleAnimations: AnimatedStyle<any>;
onFrame: (animation: StyleLayoutAnimation, timestamp: Timestamp) => boolean;
onStart: (
nextAnimation: StyleLayoutAnimation,
current: AnimatedStyle<any>,
timestamp: Timestamp,
previousAnimation: StyleLayoutAnimation
) => void;
callback?: AnimationCallback;
}

View File

@@ -0,0 +1,120 @@
'use strict';
import { defineAnimation, getReduceMotionForAnimation } from '../util';
import type {
AnimationCallback,
Timestamp,
Animation,
} from '../../commonTypes';
import { rubberBandDecay } from './rubberBandDecay';
import { isValidRubberBandConfig } from './utils';
import type {
DecayAnimation,
DecayConfig,
DefaultDecayConfig,
InnerDecayAnimation,
} from './utils';
import { rigidDecay } from './rigidDecay';
export type WithDecayConfig = DecayConfig;
// TODO TYPESCRIPT This is a temporary type to get rid of .d.ts file.
type withDecayType = (
userConfig: DecayConfig,
callback?: AnimationCallback
) => number;
function validateConfig(config: DefaultDecayConfig): void {
'worklet';
if (config.clamp) {
if (!Array.isArray(config.clamp)) {
throw new Error(
`[Reanimated] \`config.clamp\` must be an array but is ${typeof config.clamp}.`
);
}
if (config.clamp.length !== 2) {
throw new Error(
`[Reanimated] \`clamp array\` must contain 2 items but is given ${
config.clamp.length as number
}.`
);
}
}
if (config.velocityFactor <= 0) {
throw new Error(
`[Reanimated] \`config.velocityFactor\` must be greather then 0 but is ${config.velocityFactor}.`
);
}
if (config.rubberBandEffect && !config.clamp) {
throw new Error(
'[Reanimated] You need to set `clamp` property when using `rubberBandEffect`.'
);
}
}
/**
* Lets you create animations that mimic objects in motion with friction.
*
* @param config - The decay animation configuration - {@link DecayConfig}.
* @param callback - A function called upon animation completion - {@link AnimationCallback}.
* @returns An [animation object](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/glossary#animation-object) which holds the current state of the animation.
* @see https://docs.swmansion.com/react-native-reanimated/docs/animations/withDecay
*/
export const withDecay = function (
userConfig: DecayConfig,
callback?: AnimationCallback
): Animation<DecayAnimation> {
'worklet';
return defineAnimation<DecayAnimation>(0, () => {
'worklet';
const config: DefaultDecayConfig = {
deceleration: 0.998,
velocityFactor: 1,
velocity: 0,
rubberBandFactor: 0.6,
};
if (userConfig) {
Object.keys(userConfig).forEach(
(key) =>
((config as any)[key] = userConfig[key as keyof typeof userConfig])
);
}
const decay: (animation: InnerDecayAnimation, now: number) => boolean =
isValidRubberBandConfig(config)
? (animation, now) => rubberBandDecay(animation, now, config)
: (animation, now) => rigidDecay(animation, now, config);
function onStart(
animation: DecayAnimation,
value: number,
now: Timestamp
): void {
animation.current = value;
animation.lastTimestamp = now;
animation.startTimestamp = now;
animation.initialVelocity = config.velocity;
validateConfig(config);
if (animation.reduceMotion && config.clamp) {
if (value < config.clamp[0]) {
animation.current = config.clamp[0];
} else if (value > config.clamp[1]) {
animation.current = config.clamp[1];
}
}
}
return {
onFrame: decay,
onStart,
callback,
velocity: config.velocity ?? 0,
initialVelocity: 0,
current: 0,
lastTimestamp: 0,
startTimestamp: 0,
reduceMotion: getReduceMotionForAnimation(config.reduceMotion),
} as DecayAnimation;
});
} as unknown as withDecayType;

View File

@@ -0,0 +1,4 @@
'use strict';
export type { WithDecayConfig } from './decay';
export { withDecay } from './decay';
export type { DecayAnimation } from './utils';

View File

@@ -0,0 +1,34 @@
'use strict';
import { SLOPE_FACTOR, VELOCITY_EPS } from './utils';
import type { DefaultDecayConfig, InnerDecayAnimation } from './utils';
export function rigidDecay(
animation: InnerDecayAnimation,
now: number,
config: DefaultDecayConfig
): boolean {
'worklet';
const { lastTimestamp, startTimestamp, initialVelocity, current, velocity } =
animation;
const deltaTime = Math.min(now - lastTimestamp, 64);
const v =
velocity *
Math.exp(
-(1 - config.deceleration) * (now - startTimestamp) * SLOPE_FACTOR
);
animation.current = current + (v * config.velocityFactor * deltaTime) / 1000;
animation.velocity = v;
animation.lastTimestamp = now;
if (config.clamp) {
if (initialVelocity < 0 && animation.current <= config.clamp[0]) {
animation.current = config.clamp[0];
return true;
} else if (initialVelocity > 0 && animation.current >= config.clamp[1]) {
animation.current = config.clamp[1];
return true;
}
}
return Math.abs(v) < VELOCITY_EPS;
}

View File

@@ -0,0 +1,46 @@
'use strict';
import type { RubberBandDecayConfig, InnerDecayAnimation } from './utils';
import { SLOPE_FACTOR, VELOCITY_EPS } from './utils';
const DERIVATIVE_EPS = 0.1;
export function rubberBandDecay(
animation: InnerDecayAnimation,
now: number,
config: RubberBandDecayConfig
): boolean {
'worklet';
const { lastTimestamp, startTimestamp, current, velocity } = animation;
const deltaTime = Math.min(now - lastTimestamp, 64);
const clampIndex =
Math.abs(current - config.clamp[0]) < Math.abs(current - config.clamp[1])
? 0
: 1;
let derivative = 0;
if (current < config.clamp[0] || current > config.clamp[1]) {
derivative = current - config.clamp[clampIndex];
}
const v =
velocity *
Math.exp(
-(1 - config.deceleration) * (now - startTimestamp) * SLOPE_FACTOR
) -
derivative * config.rubberBandFactor;
if (Math.abs(derivative) > DERIVATIVE_EPS) {
animation.springActive = true;
} else if (animation.springActive) {
animation.current = config.clamp[clampIndex];
return true;
} else if (Math.abs(v) < VELOCITY_EPS) {
return true;
}
animation.current = current + (v * config.velocityFactor * deltaTime) / 1000;
animation.velocity = v;
animation.lastTimestamp = now;
return false;
}

View File

@@ -0,0 +1,80 @@
'use strict';
import type {
AnimatableValue,
AnimationObject,
Animation,
ReduceMotion,
Timestamp,
RequiredKeys,
} from '../../../reanimated2/commonTypes';
import { isWeb } from '../../PlatformChecker';
const IS_WEB = isWeb();
export const VELOCITY_EPS = IS_WEB ? 1 / 20 : 1;
export const SLOPE_FACTOR = 0.1;
export interface DecayAnimation extends Animation<DecayAnimation> {
lastTimestamp: Timestamp;
startTimestamp: Timestamp;
initialVelocity: number;
velocity: number;
current: AnimatableValue;
}
export interface InnerDecayAnimation
extends Omit<DecayAnimation, 'current'>,
AnimationObject {
current: number;
springActive?: boolean;
}
/**
* The decay animation configuration.
*
* @param velocity - Initial velocity of the animation. Defaults to 0.
* @param deceleration - The rate at which the velocity decreases over time. Defaults to 0.998.
* @param clamp - Array of two numbers which restricts animation's range. Defaults to [].
* @param velocityFactor - Velocity multiplier. Defaults to 1.
* @param rubberBandEffect - Makes the animation bounce over the limit specified in `clamp`. Defaults to `false`.
* @param rubberBandFactor - Strength of the rubber band effect. Defaults to 0.6.
* @param reduceMotion - Determines how the animation responds to the device's reduced motion accessibility setting. Default to `ReduceMotion.System` - {@link ReduceMotion}.
* @see https://docs.swmansion.com/react-native-reanimated/docs/animations/withDecay#config
*/
export type DecayConfig = {
deceleration?: number;
velocityFactor?: number;
velocity?: number;
reduceMotion?: ReduceMotion;
} & (
| {
rubberBandEffect?: false;
clamp?: [min: number, max: number];
}
| {
rubberBandEffect: true;
clamp: [min: number, max: number];
rubberBandFactor?: number;
}
);
export type DefaultDecayConfig = RequiredKeys<
DecayConfig,
'deceleration' | 'velocityFactor' | 'velocity'
> & { rubberBandFactor: number };
// If user wants to use rubber band decay animation we have to make sure he has provided clamp
export type RubberBandDecayConfig = RequiredKeys<
DefaultDecayConfig,
'clamp'
> & { rubberBandEffect: true };
export function isValidRubberBandConfig(
config: DefaultDecayConfig
): config is RubberBandDecayConfig {
'worklet';
return (
!!config.rubberBandEffect &&
Array.isArray(config.clamp) &&
config.clamp.length === 2
);
}

View File

@@ -0,0 +1,114 @@
'use strict';
import { defineAnimation, getReduceMotionForAnimation } from './util';
import type {
Animation,
Timestamp,
AnimatableValue,
AnimationObject,
ReduceMotion,
} from '../commonTypes';
import type { DelayAnimation } from './commonTypes';
// TODO TYPESCRIPT This is a temporary type to get rid of .d.ts file.
type withDelayType = <T extends AnimatableValue>(
delayMs: number,
delayedAnimation: T,
reduceMotion?: ReduceMotion
) => T;
/**
* An animation modifier that lets you start an animation with a delay.
*
* @param delayMs - Duration (in milliseconds) before the animation starts.
* @param nextAnimation - The animation to delay.
* @param reduceMotion - Determines how the animation responds to the device's reduced motion accessibility setting. Default to `ReduceMotion.System` - {@link ReduceMotion}.
* @returns An [animation object](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/glossary#animation-object) which holds the current state of the animation.
* @see https://docs.swmansion.com/react-native-reanimated/docs/animations/withDelay
*/
export const withDelay = function <T extends AnimationObject>(
delayMs: number,
_nextAnimation: T | (() => T),
reduceMotion?: ReduceMotion
): Animation<DelayAnimation> {
'worklet';
return defineAnimation<DelayAnimation, T>(
_nextAnimation,
(): DelayAnimation => {
'worklet';
const nextAnimation =
typeof _nextAnimation === 'function'
? _nextAnimation()
: _nextAnimation;
function delay(animation: DelayAnimation, now: Timestamp): boolean {
const { startTime, started, previousAnimation } = animation;
const current: AnimatableValue = animation.current;
if (now - startTime > delayMs || animation.reduceMotion) {
if (!started) {
nextAnimation.onStart(
nextAnimation,
current,
now,
previousAnimation!
);
animation.previousAnimation = null;
animation.started = true;
}
const finished = nextAnimation.onFrame(nextAnimation, now);
animation.current = nextAnimation.current!;
return finished;
} else if (previousAnimation) {
const finished =
previousAnimation.finished ||
previousAnimation.onFrame(previousAnimation, now);
animation.current = previousAnimation.current;
if (finished) {
animation.previousAnimation = null;
}
}
return false;
}
function onStart(
animation: Animation<any>,
value: AnimatableValue,
now: Timestamp,
previousAnimation: Animation<any> | null
): void {
animation.startTime = now;
animation.started = false;
animation.current = value;
if (previousAnimation === animation) {
animation.previousAnimation = previousAnimation.previousAnimation;
} else {
animation.previousAnimation = previousAnimation;
}
// child animations inherit the setting, unless they already have it defined
// they will have it defined only if the user used the `reduceMotion` prop
if (nextAnimation.reduceMotion === undefined) {
nextAnimation.reduceMotion = animation.reduceMotion;
}
}
const callback = (finished?: boolean): void => {
if (nextAnimation.callback) {
nextAnimation.callback(finished);
}
};
return {
isHigherOrder: true,
onFrame: delay,
onStart,
current: nextAnimation.current!,
callback,
previousAnimation: null,
startTime: 0,
started: false,
reduceMotion: getReduceMotionForAnimation(reduceMotion),
};
}
);
} as withDelayType;

View File

@@ -0,0 +1,21 @@
'use strict';
export type {
HigherOrderAnimation,
NextAnimation,
DelayAnimation,
RepeatAnimation,
SequenceAnimation,
StyleLayoutAnimation,
} from './commonTypes';
export { cancelAnimation, defineAnimation, initialUpdaterRun } from './util';
export { withTiming } from './timing';
export type { TimingAnimation, WithTimingConfig } from './timing';
export { withSpring } from './spring';
export type { SpringAnimation, WithSpringConfig } from './springUtils';
export { withDecay } from './decay';
export type { DecayAnimation, WithDecayConfig } from './decay';
export { withClamp } from './clamp';
export { withDelay } from './delay';
export { withRepeat } from './repeat';
export { withSequence } from './sequence';
export { withStyleAnimation } from './styleAnimation';

View File

@@ -0,0 +1,138 @@
'use strict';
import { defineAnimation, getReduceMotionForAnimation } from './util';
import type {
Animation,
AnimationCallback,
AnimatableValue,
Timestamp,
AnimationObject,
ReduceMotion,
} from '../commonTypes';
import type { RepeatAnimation } from './commonTypes';
// TODO TYPESCRIPT This is a temporary type to get rid of .d.ts file.
type withRepeatType = <T extends AnimatableValue>(
animation: T,
numberOfReps?: number,
reverse?: boolean,
callback?: AnimationCallback,
reduceMotion?: ReduceMotion
) => T;
/**
* Lets you repeat an animation given number of times or run it indefinitely.
*
* @param animation - An animation object you want to repeat.
* @param numberOfReps - The number of times the animation is going to be repeated. Defaults to 2.
* @param reverse - Whether the animation should run in reverse every other repetition. Defaults to false.
* @param callback - A function called on animation complete.
* @param reduceMotion - Determines how the animation responds to the device's reduced motion accessibility setting. Default to `ReduceMotion.System` - {@link ReduceMotion}.
* @returns An [animation object](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/glossary#animation-object) which holds the current state of the animation.
* @see https://docs.swmansion.com/react-native-reanimated/docs/animations/withRepeat
*/
export const withRepeat = function <T extends AnimationObject>(
_nextAnimation: T | (() => T),
numberOfReps = 2,
reverse = false,
callback?: AnimationCallback,
reduceMotion?: ReduceMotion
): Animation<RepeatAnimation> {
'worklet';
return defineAnimation<RepeatAnimation, T>(
_nextAnimation,
(): RepeatAnimation => {
'worklet';
const nextAnimation =
typeof _nextAnimation === 'function'
? _nextAnimation()
: _nextAnimation;
function repeat(animation: RepeatAnimation, now: Timestamp): boolean {
const finished = nextAnimation.onFrame(nextAnimation, now);
animation.current = nextAnimation.current;
if (finished) {
animation.reps += 1;
// call inner animation's callback on every repetition
// as the second argument the animation's current value is passed
if (nextAnimation.callback) {
nextAnimation.callback(true /* finished */, animation.current);
}
if (
animation.reduceMotion ||
(numberOfReps > 0 && animation.reps >= numberOfReps)
) {
return true;
}
const startValue = reverse
? (nextAnimation.current as number)
: animation.startValue;
if (reverse) {
nextAnimation.toValue = animation.startValue;
animation.startValue = startValue;
}
nextAnimation.onStart(
nextAnimation,
startValue,
now,
nextAnimation.previousAnimation as RepeatAnimation
);
return false;
}
return false;
}
const repCallback = (finished?: boolean): void => {
if (callback) {
callback(finished);
}
// when cancelled call inner animation's callback
if (!finished && nextAnimation.callback) {
nextAnimation.callback(false /* finished */);
}
};
function onStart(
animation: RepeatAnimation,
value: AnimatableValue,
now: Timestamp,
previousAnimation: Animation<any> | null
): void {
animation.startValue = value;
animation.reps = 0;
// child animations inherit the setting, unless they already have it defined
// they will have it defined only if the user used the `reduceMotion` prop
if (nextAnimation.reduceMotion === undefined) {
nextAnimation.reduceMotion = animation.reduceMotion;
}
// don't start the animation if reduced motion is enabled and
// the animation would end at its starting point
if (
animation.reduceMotion &&
reverse &&
(numberOfReps <= 0 || numberOfReps % 2 === 0)
) {
animation.current = animation.startValue;
animation.onFrame = () => true;
} else {
nextAnimation.onStart(nextAnimation, value, now, previousAnimation);
}
}
return {
isHigherOrder: true,
onFrame: repeat,
onStart,
reps: 0,
current: nextAnimation.current,
callback: repCallback,
startValue: 0,
reduceMotion: getReduceMotionForAnimation(reduceMotion),
};
}
);
} as withRepeatType;

View File

@@ -0,0 +1,163 @@
'use strict';
import { defineAnimation, getReduceMotionForAnimation } from './util';
import type { NextAnimation, SequenceAnimation } from './commonTypes';
import type {
Animation,
AnimatableValue,
AnimationObject,
ReduceMotion,
Timestamp,
} from '../commonTypes';
/**
* Lets you run animations in a sequence.
*
* @param reduceMotion - Determines how the animation responds to the device's reduced motion accessibility setting. Default to `ReduceMotion.System` - {@link ReduceMotion}.
* @param animations - Any number of animation objects to be run in a sequence.
* @returns An [animation object](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/glossary#animation-object) which holds the current state of the animation/
* @see https://docs.swmansion.com/react-native-reanimated/docs/animations/withSequence
*/
export function withSequence<T extends AnimatableValue>(
_reduceMotion: ReduceMotion,
...animations: T[]
): T;
export function withSequence<T extends AnimatableValue>(...animations: T[]): T;
export function withSequence(
_reduceMotionOrFirstAnimation?: ReduceMotion | NextAnimation<AnimationObject>,
..._animations: NextAnimation<AnimationObject>[]
): Animation<SequenceAnimation> {
'worklet';
let reduceMotion: ReduceMotion | undefined;
// the first argument is either a config or an animation
// this is done to allow the reduce motion config prop to be optional
if (_reduceMotionOrFirstAnimation) {
if (typeof _reduceMotionOrFirstAnimation === 'string') {
reduceMotion = _reduceMotionOrFirstAnimation as ReduceMotion;
} else {
_animations.unshift(
_reduceMotionOrFirstAnimation as NextAnimation<AnimationObject>
);
}
}
if (_animations.length === 0) {
console.warn('[Reanimated] No animation was provided for the sequence');
return defineAnimation<SequenceAnimation>(0, () => {
'worklet';
return {
onStart: (animation, value) => (animation.current = value),
onFrame: () => true,
current: 0,
animationIndex: 0,
reduceMotion: getReduceMotionForAnimation(reduceMotion),
} as SequenceAnimation;
});
}
return defineAnimation<SequenceAnimation>(
_animations[0] as SequenceAnimation,
() => {
'worklet';
const animations = _animations.map((a) => {
const result = typeof a === 'function' ? a() : a;
result.finished = false;
return result;
});
function findNextNonReducedMotionAnimationIndex(index: number) {
// the last animation is returned even if reduced motion is enabled,
// because we want the sequence to finish at the right spot
while (
index < animations.length - 1 &&
animations[index].reduceMotion
) {
index++;
}
return index;
}
const callback = (finished: boolean): void => {
if (finished) {
// we want to call the callback after every single animation
// not after all of them
return;
}
// this is going to be called only if sequence has been cancelled
animations.forEach((animation) => {
if (typeof animation.callback === 'function' && !animation.finished) {
animation.callback(finished);
}
});
};
function sequence(animation: SequenceAnimation, now: Timestamp): boolean {
const currentAnim = animations[animation.animationIndex];
const finished = currentAnim.onFrame(currentAnim, now);
animation.current = currentAnim.current;
if (finished) {
// we want to call the callback after every single animation
if (currentAnim.callback) {
currentAnim.callback(true /* finished */);
}
currentAnim.finished = true;
animation.animationIndex = findNextNonReducedMotionAnimationIndex(
animation.animationIndex + 1
);
if (animation.animationIndex < animations.length) {
const nextAnim = animations[animation.animationIndex];
nextAnim.onStart(nextAnim, currentAnim.current, now, currentAnim);
return false;
}
return true;
}
return false;
}
function onStart(
animation: SequenceAnimation,
value: AnimatableValue,
now: Timestamp,
previousAnimation: SequenceAnimation
): void {
// child animations inherit the setting, unless they already have it defined
// they will have it defined only if the user used the `reduceMotion` prop
animations.forEach((anim) => {
if (anim.reduceMotion === undefined) {
anim.reduceMotion = animation.reduceMotion;
}
});
animation.animationIndex = findNextNonReducedMotionAnimationIndex(0);
if (previousAnimation === undefined) {
previousAnimation = animations[
animations.length - 1
] as SequenceAnimation;
}
const currentAnimation = animations[animation.animationIndex];
currentAnimation.onStart(
currentAnimation,
value,
now,
previousAnimation
);
}
return {
isHigherOrder: true,
onFrame: sequence,
onStart,
animationIndex: 0,
current: animations[0].current,
callback,
reduceMotion: getReduceMotionForAnimation(reduceMotion),
} as SequenceAnimation;
}
);
}

View File

@@ -0,0 +1,244 @@
'use strict';
import { defineAnimation, getReduceMotionForAnimation } from './util';
import type {
Animation,
AnimationCallback,
AnimatableValue,
Timestamp,
} from '../commonTypes';
import type {
SpringConfig,
SpringAnimation,
InnerSpringAnimation,
SpringConfigInner,
DefaultSpringConfig,
} from './springUtils';
import {
initialCalculations,
calculateNewMassToMatchDuration,
underDampedSpringCalculations,
criticallyDampedSpringCalculations,
isAnimationTerminatingCalculation,
scaleZetaToMatchClamps,
checkIfConfigIsValid,
} from './springUtils';
// TODO TYPESCRIPT This is a temporary type to get rid of .d.ts file.
type withSpringType = <T extends AnimatableValue>(
toValue: T,
userConfig?: SpringConfig,
callback?: AnimationCallback
) => T;
/**
* Lets you create spring-based animations.
*
* @param toValue - the value at which the animation will come to rest - {@link AnimatableValue}
* @param config - the spring animation configuration - {@link SpringConfig}
* @param callback - a function called on animation complete - {@link AnimationCallback}
* @returns an [animation object](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/glossary#animation-object) which holds the current state of the animation
* @see https://docs.swmansion.com/react-native-reanimated/docs/animations/withSpring
*/
export const withSpring = ((
toValue: AnimatableValue,
userConfig?: SpringConfig,
callback?: AnimationCallback
): Animation<SpringAnimation> => {
'worklet';
return defineAnimation<SpringAnimation>(toValue, () => {
'worklet';
const defaultConfig: DefaultSpringConfig = {
damping: 10,
mass: 1,
stiffness: 100,
overshootClamping: false,
restDisplacementThreshold: 0.01,
restSpeedThreshold: 2,
velocity: 0,
duration: 2000,
dampingRatio: 0.5,
reduceMotion: undefined,
clamp: undefined,
} as const;
const config: DefaultSpringConfig & SpringConfigInner = {
...defaultConfig,
...userConfig,
useDuration: !!(userConfig?.duration || userConfig?.dampingRatio),
skipAnimation: false,
};
config.skipAnimation = !checkIfConfigIsValid(config);
if (config.duration === 0) {
config.skipAnimation = true;
}
function springOnFrame(
animation: InnerSpringAnimation,
now: Timestamp
): boolean {
// eslint-disable-next-line @typescript-eslint/no-shadow
const { toValue, startTimestamp, current } = animation;
const timeFromStart = now - startTimestamp;
if (config.useDuration && timeFromStart >= config.duration) {
animation.current = toValue;
// clear lastTimestamp to avoid using stale value by the next spring animation that starts after this one
animation.lastTimestamp = 0;
return true;
}
if (config.skipAnimation) {
animation.current = toValue;
animation.lastTimestamp = 0;
return true;
}
const { lastTimestamp, velocity } = animation;
const deltaTime = Math.min(now - lastTimestamp, 64);
animation.lastTimestamp = now;
const t = deltaTime / 1000;
const v0 = -velocity;
const x0 = toValue - current;
const { zeta, omega0, omega1 } = animation;
const { position: newPosition, velocity: newVelocity } =
zeta < 1
? underDampedSpringCalculations(animation, {
zeta,
v0,
x0,
omega0,
omega1,
t,
})
: criticallyDampedSpringCalculations(animation, {
v0,
x0,
omega0,
t,
});
animation.current = newPosition;
animation.velocity = newVelocity;
const { isOvershooting, isVelocity, isDisplacement } =
isAnimationTerminatingCalculation(animation, config);
const springIsNotInMove =
isOvershooting || (isVelocity && isDisplacement);
if (!config.useDuration && springIsNotInMove) {
animation.velocity = 0;
animation.current = toValue;
// clear lastTimestamp to avoid using stale value by the next spring animation that starts after this one
animation.lastTimestamp = 0;
return true;
}
return false;
}
function isTriggeredTwice(
previousAnimation: SpringAnimation | undefined,
animation: SpringAnimation
) {
return (
previousAnimation?.lastTimestamp &&
previousAnimation?.startTimestamp &&
previousAnimation?.toValue === animation.toValue &&
previousAnimation?.duration === animation.duration &&
previousAnimation?.dampingRatio === animation.dampingRatio
);
}
function onStart(
animation: SpringAnimation,
value: number,
now: Timestamp,
previousAnimation: SpringAnimation | undefined
): void {
animation.current = value;
animation.startValue = value;
let mass = config.mass;
const triggeredTwice = isTriggeredTwice(previousAnimation, animation);
const duration = config.duration;
const x0 = triggeredTwice
? // If animation is triggered twice we want to continue the previous animation
// form the previous starting point
previousAnimation?.startValue
: Number(animation.toValue) - value;
if (previousAnimation) {
animation.velocity =
(triggeredTwice
? previousAnimation?.velocity
: previousAnimation?.velocity + config.velocity) || 0;
} else {
animation.velocity = config.velocity || 0;
}
if (triggeredTwice) {
animation.zeta = previousAnimation?.zeta || 0;
animation.omega0 = previousAnimation?.omega0 || 0;
animation.omega1 = previousAnimation?.omega1 || 0;
} else {
if (config.useDuration) {
const actualDuration = triggeredTwice
? // If animation is triggered twice we want to continue the previous animation
// so we need to include the time that already elapsed
duration -
((previousAnimation?.lastTimestamp || 0) -
(previousAnimation?.startTimestamp || 0))
: duration;
config.duration = actualDuration;
mass = calculateNewMassToMatchDuration(
x0 as number,
config,
animation.velocity
);
}
const { zeta, omega0, omega1 } = initialCalculations(mass, config);
animation.zeta = zeta;
animation.omega0 = omega0;
animation.omega1 = omega1;
if (config.clamp !== undefined) {
animation.zeta = scaleZetaToMatchClamps(animation, config.clamp);
}
}
animation.lastTimestamp = previousAnimation?.lastTimestamp || now;
animation.startTimestamp = triggeredTwice
? previousAnimation?.startTimestamp || now
: now;
}
return {
onFrame: springOnFrame,
onStart,
toValue,
velocity: config.velocity || 0,
current: toValue,
startValue: 0,
callback,
lastTimestamp: 0,
startTimestamp: 0,
zeta: 0,
omega0: 0,
omega1: 0,
reduceMotion: getReduceMotionForAnimation(config.reduceMotion),
} as SpringAnimation;
});
}) as withSpringType;

View File

@@ -0,0 +1,385 @@
'use strict';
import type {
Animation,
AnimatableValue,
Timestamp,
ReduceMotion,
} from '../commonTypes';
/**
* Spring animation configuration.
*
* @param mass - The weight of the spring. Reducing this value makes the animation faster. Defaults to 1.
* @param damping - How quickly a spring slows down. Higher damping means the spring will come to rest faster. Defaults to 10.
* @param duration - Length of the animation (in milliseconds). Defaults to 2000.
* @param dampingRatio - How damped the spring is. Value 1 means the spring is critically damped, and value \>1 means the spring is overdamped. Defaults to 0.5.
* @param stiffness - How bouncy the spring is. Defaults to 100.
* @param velocity - Initial velocity applied to the spring equation. Defaults to 0.
* @param overshootClamping - Whether a spring can bounce over the `toValue`. Defaults to false.
* @param restDisplacementThreshold - The displacement below which the spring will snap to toValue without further oscillations. Defaults to 0.01.
* @param restSpeedThreshold - The speed in pixels per second from which the spring will snap to toValue without further oscillations. Defaults to 2.
* @param reduceMotion - Determines how the animation responds to the device's reduced motion accessibility setting. Default to `ReduceMotion.System` - {@link ReduceMotion}.
* @see https://docs.swmansion.com/react-native-reanimated/docs/animations/withSpring/#config-
*/
export type SpringConfig = {
stiffness?: number;
overshootClamping?: boolean;
restDisplacementThreshold?: number;
restSpeedThreshold?: number;
velocity?: number;
reduceMotion?: ReduceMotion;
} & (
| {
mass?: number;
damping?: number;
duration?: never;
dampingRatio?: never;
clamp?: never;
}
| {
mass?: never;
damping?: never;
duration?: number;
dampingRatio?: number;
clamp?: { min?: number; max?: number };
}
);
// This type contains all the properties from SpringConfig, which are changed to be required,
// except for optional 'reduceMotion' and 'clamp'
export type DefaultSpringConfig = {
[K in keyof Required<SpringConfig>]: K extends 'reduceMotion' | 'clamp'
? Required<SpringConfig>[K] | undefined
: Required<SpringConfig>[K];
};
export type WithSpringConfig = SpringConfig;
export interface SpringConfigInner {
useDuration: boolean;
skipAnimation: boolean;
}
export interface SpringAnimation extends Animation<SpringAnimation> {
current: AnimatableValue;
toValue: AnimatableValue;
velocity: number;
lastTimestamp: Timestamp;
startTimestamp: Timestamp;
startValue: number;
zeta: number;
omega0: number;
omega1: number;
}
export interface InnerSpringAnimation
extends Omit<SpringAnimation, 'toValue' | 'current'> {
toValue: number;
current: number;
}
export function checkIfConfigIsValid(config: DefaultSpringConfig): boolean {
'worklet';
let errorMessage = '';
(
[
'stiffness',
'damping',
'dampingRatio',
'restDisplacementThreshold',
'restSpeedThreshold',
'mass',
] as const
).forEach((prop) => {
const value = config[prop];
if (value <= 0) {
errorMessage += `, ${prop} must be grater than zero but got ${value}`;
}
});
if (config.duration < 0) {
errorMessage += `, duration can't be negative, got ${config.duration}`;
}
if (
config.clamp?.min &&
config.clamp?.max &&
config.clamp.min > config.clamp.max
) {
errorMessage += `, clamp.min should be lower than clamp.max, got clamp: {min: ${config.clamp.min}, max: ${config.clamp.max}} `;
}
if (errorMessage !== '') {
console.warn('[Reanimated] Invalid spring config' + errorMessage);
}
return errorMessage === '';
}
// ts-prune-ignore-next This function is exported to be tested
export function bisectRoot({
min,
max,
func,
maxIterations = 20,
}: {
min: number;
max: number;
func: (x: number) => number;
maxIterations?: number;
}) {
'worklet';
const ACCURACY = 0.00005;
let idx = maxIterations;
let current = (max + min) / 2;
while (Math.abs(func(current)) > ACCURACY && idx > 0) {
idx -= 1;
if (func(current) < 0) {
min = current;
} else {
max = current;
}
current = (min + max) / 2;
}
return current;
}
export function initialCalculations(
mass = 0,
config: DefaultSpringConfig & SpringConfigInner
): {
zeta: number;
omega0: number;
omega1: number;
} {
'worklet';
if (config.skipAnimation) {
return { zeta: 0, omega0: 0, omega1: 0 };
}
if (config.useDuration) {
const { stiffness: k, dampingRatio: zeta } = config;
/** omega0 and omega1 denote angular frequency and natural angular frequency, see this link for formulas:
* https://courses.lumenlearning.com/suny-osuniversityphysics/chapter/15-5-damped-oscillations/
*/
const omega0 = Math.sqrt(k / mass);
const omega1 = omega0 * Math.sqrt(1 - zeta ** 2);
return { zeta, omega0, omega1 };
} else {
const { damping: c, mass: m, stiffness: k } = config;
const zeta = c / (2 * Math.sqrt(k * m)); // damping ratio
const omega0 = Math.sqrt(k / m); // undamped angular frequency of the oscillator (rad/ms)
const omega1 = omega0 * Math.sqrt(1 - zeta ** 2); // exponential decay
return { zeta, omega0, omega1 };
}
}
/** We make an assumption that we can manipulate zeta without changing duration of movement.
* According to theory this change is small and tests shows that we can indeed ignore it.
*/
export function scaleZetaToMatchClamps(
animation: SpringAnimation,
clamp: { min?: number; max?: number }
): number {
'worklet';
const { zeta, toValue, startValue } = animation;
const toValueNum = Number(toValue);
if (toValueNum === startValue) {
return zeta;
}
const [firstBound, secondBound] =
toValueNum - startValue > 0
? [clamp.min, clamp.max]
: [clamp.max, clamp.min];
/** The extrema we get from equation below are relative (we obtain a ratio),
* To get absolute extrema we convert it as follows:
*
* AbsoluteExtremum = startValue ± RelativeExtremum * (toValue - startValue)
* Where ± denotes:
* + if extremum is over the target
* - otherwise
*/
const relativeExtremum1 =
secondBound !== undefined
? Math.abs((secondBound - toValueNum) / (toValueNum - startValue))
: undefined;
const relativeExtremum2 =
firstBound !== undefined
? Math.abs((firstBound - toValueNum) / (toValueNum - startValue))
: undefined;
/** Use this formula http://hyperphysics.phy-astr.gsu.edu/hbase/oscda.html to calculate
* first two extrema. These extrema are located where cos = +- 1
*
* Therefore the first two extrema are:
*
* Math.exp(-zeta * Math.PI); (over the target)
* Math.exp(-zeta * 2 * Math.PI); (before the target)
*/
const newZeta1 =
relativeExtremum1 !== undefined
? Math.abs(Math.log(relativeExtremum1) / Math.PI)
: undefined;
const newZeta2 =
relativeExtremum2 !== undefined
? Math.abs(Math.log(relativeExtremum2) / (2 * Math.PI))
: undefined;
const zetaSatisfyingClamp = [newZeta1, newZeta2].filter(
(x: number | undefined): x is number => x !== undefined
);
// The bigger is zeta the smaller are bounces, we return the biggest one
// because it should satisfy all conditions
return Math.max(...zetaSatisfyingClamp, zeta);
}
/** Runs before initial */
export function calculateNewMassToMatchDuration(
x0: number,
config: DefaultSpringConfig & SpringConfigInner,
v0: number
) {
'worklet';
if (config.skipAnimation) {
return 0;
}
/** Use this formula: https://phys.libretexts.org/Bookshelves/University_Physics/Book%3A_University_Physics_(OpenStax)/Book%3A_University_Physics_I_-_Mechanics_Sound_Oscillations_and_Waves_(OpenStax)/15%3A_Oscillations/15.06%3A_Damped_Oscillations
* to find the asymptote and estimate the damping that gives us the expected duration
⎛ ⎛ c⎞ ⎞
⎜-⎜──⎟ ⋅ duration⎟
⎝ ⎝2m⎠ ⎠
A ⋅ e = threshold
Amplitude calculated using "Conservation of energy"
_________________
2 2
m ⋅ v0 + k ⋅ x0
amplitude = ─────────────────
╲╱ k
And replace mass with damping ratio which is provided: m = (c^2)/(4 * k * zeta^2)
*/
const {
stiffness: k,
dampingRatio: zeta,
restSpeedThreshold: threshold,
duration,
} = config;
const durationForMass = (mass: number) => {
'worklet';
const amplitude =
(mass * v0 * v0 + k * x0 * x0) / (Math.exp(1 - 0.5 * zeta) * k);
const c = zeta * 2 * Math.sqrt(k * mass);
return (
1000 * ((-2 * mass) / c) * Math.log((threshold * 0.01) / amplitude) -
duration
);
};
// Bisection turns out to be much faster than Newton's method in our case
return bisectRoot({ min: 0, max: 100, func: durationForMass });
}
export function criticallyDampedSpringCalculations(
animation: InnerSpringAnimation,
precalculatedValues: {
v0: number;
x0: number;
omega0: number;
t: number;
}
): { position: number; velocity: number } {
'worklet';
const { toValue } = animation;
const { v0, x0, omega0, t } = precalculatedValues;
const criticallyDampedEnvelope = Math.exp(-omega0 * t);
const criticallyDampedPosition =
toValue - criticallyDampedEnvelope * (x0 + (v0 + omega0 * x0) * t);
const criticallyDampedVelocity =
criticallyDampedEnvelope *
(v0 * (t * omega0 - 1) + t * x0 * omega0 * omega0);
return {
position: criticallyDampedPosition,
velocity: criticallyDampedVelocity,
};
}
export function underDampedSpringCalculations(
animation: InnerSpringAnimation,
precalculatedValues: {
zeta: number;
v0: number;
x0: number;
omega0: number;
omega1: number;
t: number;
}
): { position: number; velocity: number } {
'worklet';
const { toValue, current, velocity } = animation;
const { zeta, t, omega0, omega1 } = precalculatedValues;
const v0 = -velocity;
const x0 = toValue - current;
const sin1 = Math.sin(omega1 * t);
const cos1 = Math.cos(omega1 * t);
// under damped
const underDampedEnvelope = Math.exp(-zeta * omega0 * t);
const underDampedFrag1 =
underDampedEnvelope *
(sin1 * ((v0 + zeta * omega0 * x0) / omega1) + x0 * cos1);
const underDampedPosition = toValue - underDampedFrag1;
// This looks crazy -- it's actually just the derivative of the oscillation function
const underDampedVelocity =
zeta * omega0 * underDampedFrag1 -
underDampedEnvelope *
(cos1 * (v0 + zeta * omega0 * x0) - omega1 * x0 * sin1);
return { position: underDampedPosition, velocity: underDampedVelocity };
}
export function isAnimationTerminatingCalculation(
animation: InnerSpringAnimation,
config: DefaultSpringConfig
): {
isOvershooting: boolean;
isVelocity: boolean;
isDisplacement: boolean;
} {
'worklet';
const { toValue, velocity, startValue, current } = animation;
const isOvershooting = config.overshootClamping
? (current > toValue && startValue < toValue) ||
(current < toValue && startValue > toValue)
: false;
const isVelocity = Math.abs(velocity) < config.restSpeedThreshold;
const isDisplacement =
Math.abs(toValue - current) < config.restDisplacementThreshold;
return { isOvershooting, isVelocity, isDisplacement };
}

View File

@@ -0,0 +1,264 @@
'use strict';
import { defineAnimation } from './util';
import type {
Timestamp,
AnimatableValue,
AnimationObject,
Animation,
NestedObject,
NestedObjectValues,
} from '../commonTypes';
import type { AnimatedStyle } from '../helperTypes';
import type { StyleLayoutAnimation } from './commonTypes';
import { withTiming } from './timing';
import { ColorProperties, processColor } from '../Colors';
// resolves path to value for nested objects
// if path cannot be resolved returns undefined
function resolvePath<T>(
obj: NestedObject<T>,
path: AnimatableValue[] | AnimatableValue
): NestedObjectValues<T> | undefined {
'worklet';
const keys: AnimatableValue[] = Array.isArray(path) ? path : [path];
return keys.reduce<NestedObjectValues<T> | undefined>((acc, current) => {
if (Array.isArray(acc) && typeof current === 'number') {
return acc[current];
} else if (
acc !== null &&
typeof acc === 'object' &&
(current as number | string) in acc
) {
return (acc as { [key: string]: NestedObjectValues<T> })[
current as number | string
];
}
return undefined;
}, obj);
}
// set value at given path
type Path = Array<string | number> | string | number;
function setPath<T>(
obj: NestedObject<T>,
path: Path,
value: NestedObjectValues<T>
): void {
'worklet';
const keys: Path = Array.isArray(path) ? path : [path];
let currObj: NestedObjectValues<T> = obj;
for (let i = 0; i < keys.length - 1; i++) {
// creates entry if there isn't one
currObj = currObj as { [key: string]: NestedObjectValues<T> };
if (!(keys[i] in currObj)) {
// if next key is a number create an array
if (typeof keys[i + 1] === 'number') {
currObj[keys[i]] = [];
} else {
currObj[keys[i]] = {};
}
}
currObj = currObj[keys[i]];
}
(currObj as { [key: string]: NestedObjectValues<T> })[keys[keys.length - 1]] =
value;
}
interface NestedObjectEntry<T> {
value: NestedObjectValues<T>;
path: (string | number)[];
}
export function withStyleAnimation(
styleAnimations: AnimatedStyle<any>
): StyleLayoutAnimation {
'worklet';
return defineAnimation<StyleLayoutAnimation>({}, () => {
'worklet';
const onFrame = (
animation: StyleLayoutAnimation,
now: Timestamp
): boolean => {
let stillGoing = false;
const entriesToCheck: NestedObjectEntry<AnimationObject>[] = [
{ value: animation.styleAnimations, path: [] },
];
while (entriesToCheck.length > 0) {
const currentEntry: NestedObjectEntry<AnimationObject> =
entriesToCheck.pop() as NestedObjectEntry<AnimationObject>;
if (Array.isArray(currentEntry.value)) {
for (let index = 0; index < currentEntry.value.length; index++) {
entriesToCheck.push({
value: currentEntry.value[index],
path: currentEntry.path.concat(index),
});
}
} else if (
typeof currentEntry.value === 'object' &&
currentEntry.value.onFrame === undefined
) {
// nested object
for (const key of Object.keys(currentEntry.value)) {
entriesToCheck.push({
value: currentEntry.value[key],
path: currentEntry.path.concat(key),
});
}
} else {
const currentStyleAnimation: AnimationObject =
currentEntry.value as AnimationObject;
if (currentStyleAnimation.finished) {
continue;
}
const finished = currentStyleAnimation.onFrame(
currentStyleAnimation,
now
);
if (finished) {
currentStyleAnimation.finished = true;
if (currentStyleAnimation.callback) {
currentStyleAnimation.callback(true);
}
} else {
stillGoing = true;
}
// When working with animations changing colors, we need to make sure that each one of them begins with a rgba, not a processed number.
// Thus, we only set the path to a processed color, but currentStyleAnimation.current stays as rgba.
const isAnimatingColorProp = ColorProperties.includes(
currentEntry.path[0] as string
);
setPath(
animation.current,
currentEntry.path,
isAnimatingColorProp
? processColor(currentStyleAnimation.current)
: currentStyleAnimation.current
);
}
}
return !stillGoing;
};
const onStart = (
animation: StyleLayoutAnimation,
value: AnimatedStyle<any>,
now: Timestamp,
previousAnimation: StyleLayoutAnimation
): void => {
const entriesToCheck: NestedObjectEntry<
AnimationObject | AnimatableValue
>[] = [{ value: styleAnimations, path: [] }];
while (entriesToCheck.length > 0) {
const currentEntry: NestedObjectEntry<
AnimationObject | AnimatableValue
> = entriesToCheck.pop() as NestedObjectEntry<
AnimationObject | AnimatableValue
>;
if (Array.isArray(currentEntry.value)) {
for (let index = 0; index < currentEntry.value.length; index++) {
entriesToCheck.push({
value: currentEntry.value[index],
path: currentEntry.path.concat(index),
});
}
} else if (
typeof currentEntry.value === 'object' &&
currentEntry.value.onStart === undefined
) {
for (const key of Object.keys(currentEntry.value)) {
entriesToCheck.push({
value: currentEntry.value[key],
path: currentEntry.path.concat(key),
});
}
} else {
const prevAnimation = resolvePath(
previousAnimation?.styleAnimations,
currentEntry.path
);
let prevVal = resolvePath(value, currentEntry.path);
if (prevAnimation && !prevVal) {
prevVal = (prevAnimation as any).current;
}
if (prevVal === undefined) {
console.warn(
`Initial values for animation are missing for property ${currentEntry.path.join(
'.'
)}`
);
}
setPath(animation.current, currentEntry.path, prevVal);
let currentAnimation: AnimationObject;
if (
typeof currentEntry.value !== 'object' ||
!currentEntry.value.onStart
) {
currentAnimation = withTiming(
currentEntry.value as AnimatableValue,
{ duration: 0 }
) as AnimationObject; // TODO TYPESCRIPT this temporary cast is to get rid of .d.ts file.
setPath(
animation.styleAnimations,
currentEntry.path,
currentAnimation
);
} else {
currentAnimation = currentEntry.value as Animation<AnimationObject>;
}
currentAnimation.onStart(
currentAnimation,
prevVal,
now,
prevAnimation
);
}
}
};
const callback = (finished: boolean): void => {
if (!finished) {
const animationsToCheck: NestedObjectValues<AnimationObject>[] = [
styleAnimations,
];
while (animationsToCheck.length > 0) {
const currentAnimation: NestedObjectValues<AnimationObject> =
animationsToCheck.pop() as NestedObjectValues<AnimationObject>;
if (Array.isArray(currentAnimation)) {
for (const element of currentAnimation) {
animationsToCheck.push(element);
}
} else if (
typeof currentAnimation === 'object' &&
currentAnimation.onStart === undefined
) {
for (const value of Object.values(currentAnimation)) {
animationsToCheck.push(value);
}
} else {
const currentStyleAnimation: AnimationObject =
currentAnimation as AnimationObject;
if (
!currentStyleAnimation.finished &&
currentStyleAnimation.callback
) {
currentStyleAnimation.callback(false);
}
}
}
}
};
return {
isHigherOrder: true,
onFrame,
onStart,
current: {},
styleAnimations,
callback,
} as StyleLayoutAnimation;
});
}

View File

@@ -0,0 +1,151 @@
'use strict';
import type { EasingFunction, EasingFunctionFactory } from '../Easing';
import { Easing } from '../Easing';
import {
assertEasingIsWorklet,
defineAnimation,
getReduceMotionForAnimation,
} from './util';
import type {
Animation,
AnimationCallback,
Timestamp,
AnimatableValue,
ReduceMotion,
} from '../commonTypes';
/**
* The timing animation configuration.
*
* @param duration - Length of the animation (in milliseconds). Defaults to 300.
* @param easing - An easing function which defines the animation curve. Defaults to `Easing.inOut(Easing.quad)`.
* @param reduceMotion - Determines how the animation responds to the device's reduced motion accessibility setting. Default to `ReduceMotion.System` - {@link ReduceMotion}.
* @see https://docs.swmansion.com/react-native-reanimated/docs/animations/withTiming#config-
*/
interface TimingConfig {
duration?: number;
reduceMotion?: ReduceMotion;
easing?: EasingFunction | EasingFunctionFactory;
}
export type WithTimingConfig = TimingConfig;
export interface TimingAnimation extends Animation<TimingAnimation> {
type: string;
easing: EasingFunction;
startValue: AnimatableValue;
startTime: Timestamp;
progress: number;
toValue: AnimatableValue;
current: AnimatableValue;
}
interface InnerTimingAnimation
extends Omit<TimingAnimation, 'toValue' | 'current'> {
toValue: number;
current: number;
}
// TODO TYPESCRIPT This is temporary type put in here to get rid of our .d.ts file
type withTimingType = <T extends AnimatableValue>(
toValue: T,
userConfig?: TimingConfig,
callback?: AnimationCallback
) => T;
/**
* Lets you create an animation based on duration and easing.
*
* @param toValue - The value on which the animation will come at rest - {@link AnimatableValue}.
* @param config - The timing animation configuration - {@link TimingConfig}.
* @param callback - A function called on animation complete - {@link AnimationCallback}.
* @returns An [animation object](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/glossary#animation-object) which holds the current state of the animation.
* @see https://docs.swmansion.com/react-native-reanimated/docs/animations/withTiming
*/
export const withTiming = function (
toValue: AnimatableValue,
userConfig?: TimingConfig,
callback?: AnimationCallback
): Animation<TimingAnimation> {
'worklet';
if (__DEV__ && userConfig?.easing) {
assertEasingIsWorklet(userConfig.easing);
}
return defineAnimation<TimingAnimation>(toValue, () => {
'worklet';
const config: Required<Omit<TimingConfig, 'reduceMotion'>> = {
duration: 300,
easing: Easing.inOut(Easing.quad),
};
if (userConfig) {
Object.keys(userConfig).forEach(
(key) =>
((config as any)[key] = userConfig[key as keyof typeof userConfig])
);
}
function timing(animation: InnerTimingAnimation, now: Timestamp): boolean {
// eslint-disable-next-line @typescript-eslint/no-shadow
const { toValue, startTime, startValue } = animation;
const runtime = now - startTime;
if (runtime >= config.duration) {
// reset startTime to avoid reusing finished animation config in `start` method
animation.startTime = 0;
animation.current = toValue;
return true;
}
const progress = animation.easing(runtime / config.duration);
animation.current =
(startValue as number) + (toValue - (startValue as number)) * progress;
return false;
}
function onStart(
animation: TimingAnimation,
value: number,
now: Timestamp,
previousAnimation: Animation<TimingAnimation>
): void {
if (
previousAnimation &&
(previousAnimation as TimingAnimation).type === 'timing' &&
(previousAnimation as TimingAnimation).toValue === toValue &&
(previousAnimation as TimingAnimation).startTime
) {
// to maintain continuity of timing animations we check if we are starting
// new timing over the old one with the same parameters. If so, we want
// to copy animation timeline properties
animation.startTime = (previousAnimation as TimingAnimation).startTime;
animation.startValue = (
previousAnimation as TimingAnimation
).startValue;
} else {
animation.startTime = now;
animation.startValue = value;
}
animation.current = value;
if (typeof config.easing === 'object') {
animation.easing = config.easing.factory();
} else {
animation.easing = config.easing;
}
}
return {
type: 'timing',
onFrame: timing,
onStart: onStart as (animation: TimingAnimation, now: number) => boolean,
progress: 0,
toValue,
startValue: 0,
startTime: 0,
easing: () => 0,
current: toValue,
callback,
reduceMotion: getReduceMotionForAnimation(userConfig?.reduceMotion),
} as TimingAnimation;
});
} as withTimingType;

View File

@@ -0,0 +1,422 @@
'use strict';
type FixedLengthArray<
T,
L extends number,
PassedObject = [T, ...Array<T>]
> = PassedObject & {
readonly length: L;
[I: number]: T;
};
export type AffineMatrix = FixedLengthArray<FixedLengthArray<number, 4>, 4>;
export type AffineMatrixFlat = FixedLengthArray<number, 16>;
type TransformMatrixDecomposition = Record<
'translationMatrix' | 'scaleMatrix' | 'rotationMatrix' | 'skewMatrix',
AffineMatrix
>;
type Axis = 'x' | 'y' | 'z';
interface TansformMatrixDecompositionWithAngles
extends TransformMatrixDecomposition {
rx: number;
ry: number;
rz: number;
}
export function isAffineMatrixFlat(x: unknown): x is AffineMatrixFlat {
'worklet';
return (
Array.isArray(x) &&
x.length === 16 &&
x.every((element) => typeof element === 'number' && !isNaN(element))
);
}
// ts-prune-ignore-next This function is exported to be tested
export function isAffineMatrix(x: unknown): x is AffineMatrix {
'worklet';
return (
Array.isArray(x) &&
x.length === 4 &&
x.every(
(row) =>
Array.isArray(row) &&
row.length === 4 &&
row.every((element) => typeof element === 'number' && !isNaN(element))
)
);
}
export function flatten(matrix: AffineMatrix): AffineMatrixFlat {
'worklet';
return matrix.flat() as AffineMatrixFlat;
}
// ts-prune-ignore-next This function is exported to be tested
export function unflatten(m: AffineMatrixFlat): AffineMatrix {
'worklet';
return [
[m[0], m[1], m[2], m[3]],
[m[4], m[5], m[6], m[7]],
[m[8], m[9], m[10], m[11]],
[m[12], m[13], m[14], m[15]],
] as AffineMatrix;
}
function maybeFlattenMatrix(
matrix: AffineMatrix | AffineMatrixFlat
): AffineMatrixFlat {
'worklet';
return isAffineMatrix(matrix) ? flatten(matrix) : matrix;
}
export function multiplyMatrices(
a: AffineMatrix,
b: AffineMatrix
): AffineMatrix {
'worklet';
return [
[
a[0][0] * b[0][0] +
a[0][1] * b[1][0] +
a[0][2] * b[2][0] +
a[0][3] * b[3][0],
a[0][0] * b[0][1] +
a[0][1] * b[1][1] +
a[0][2] * b[2][1] +
a[0][3] * b[3][1],
a[0][0] * b[0][2] +
a[0][1] * b[1][2] +
a[0][2] * b[2][2] +
a[0][3] * b[3][2],
a[0][0] * b[0][3] +
a[0][1] * b[1][3] +
a[0][2] * b[2][3] +
a[0][3] * b[3][3],
],
[
a[1][0] * b[0][0] +
a[1][1] * b[1][0] +
a[1][2] * b[2][0] +
a[1][3] * b[3][0],
a[1][0] * b[0][1] +
a[1][1] * b[1][1] +
a[1][2] * b[2][1] +
a[1][3] * b[3][1],
a[1][0] * b[0][2] +
a[1][1] * b[1][2] +
a[1][2] * b[2][2] +
a[1][3] * b[3][2],
a[1][0] * b[0][3] +
a[1][1] * b[1][3] +
a[1][2] * b[2][3] +
a[1][3] * b[3][3],
],
[
a[2][0] * b[0][0] +
a[2][1] * b[1][0] +
a[2][2] * b[2][0] +
a[2][3] * b[3][0],
a[2][0] * b[0][1] +
a[2][1] * b[1][1] +
a[2][2] * b[2][1] +
a[2][3] * b[3][1],
a[2][0] * b[0][2] +
a[2][1] * b[1][2] +
a[2][2] * b[2][2] +
a[2][3] * b[3][2],
a[2][0] * b[0][3] +
a[2][1] * b[1][3] +
a[2][2] * b[2][3] +
a[2][3] * b[3][3],
],
[
a[3][0] * b[0][0] +
a[3][1] * b[1][0] +
a[3][2] * b[2][0] +
a[3][3] * b[3][0],
a[3][0] * b[0][1] +
a[3][1] * b[1][1] +
a[3][2] * b[2][1] +
a[3][3] * b[3][1],
a[3][0] * b[0][2] +
a[3][1] * b[1][2] +
a[3][2] * b[2][2] +
a[3][3] * b[3][2],
a[3][0] * b[0][3] +
a[3][1] * b[1][3] +
a[3][2] * b[2][3] +
a[3][3] * b[3][3],
],
];
}
export function subtractMatrices<T extends AffineMatrixFlat | AffineMatrix>(
maybeFlatA: T,
maybeFlatB: T
): T {
'worklet';
const isFlatOnStart = isAffineMatrixFlat(maybeFlatA);
const a: AffineMatrixFlat = maybeFlattenMatrix(maybeFlatA);
const b: AffineMatrixFlat = maybeFlattenMatrix(maybeFlatB);
const c = a.map((_, i) => a[i] - b[i]) as AffineMatrixFlat;
return isFlatOnStart ? (c as T) : (unflatten(c) as T);
}
export function addMatrices<T extends AffineMatrixFlat | AffineMatrix>(
maybeFlatA: T,
maybeFlatB: T
): T {
'worklet';
const isFlatOnStart = isAffineMatrixFlat(maybeFlatA);
const a = maybeFlattenMatrix(maybeFlatA);
const b = maybeFlattenMatrix(maybeFlatB);
const c = a.map((_, i) => a[i] + b[i]) as AffineMatrixFlat;
return isFlatOnStart ? (c as T) : (unflatten(c) as T);
}
export function scaleMatrix<T extends AffineMatrixFlat | AffineMatrix>(
maybeFlatA: T,
scalar: number
): T {
'worklet';
const isFlatOnStart = isAffineMatrixFlat(maybeFlatA);
const a = maybeFlattenMatrix(maybeFlatA);
const b = a.map((x) => x * scalar) as AffineMatrixFlat;
return isFlatOnStart ? (b as T) : (unflatten(b) as T);
}
export function getRotationMatrix(
angle: number,
axis: Axis = 'z'
): AffineMatrix {
'worklet';
const cos = Math.cos(angle);
const sin = Math.sin(angle);
switch (axis) {
case 'z':
return [
[cos, sin, 0, 0],
[-sin, cos, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1],
];
case 'y':
return [
[cos, 0, -sin, 0],
[0, 1, 0, 0],
[sin, 0, cos, 0],
[0, 0, 0, 1],
];
case 'x':
return [
[1, 0, 0, 0],
[0, cos, sin, 0],
[0, -sin, cos, 0],
[0, 0, 0, 1],
];
}
}
function norm3d(x: number, y: number, z: number) {
'worklet';
return Math.sqrt(x * x + y * y + z * z);
}
function transposeMatrix(matrix: AffineMatrix): AffineMatrix {
'worklet';
const m = flatten(matrix);
return [
[m[0], m[4], m[8], m[12]],
[m[1], m[5], m[9], m[13]],
[m[2], m[6], m[10], m[14]],
[m[3], m[7], m[11], m[15]],
];
}
function assertVectorsHaveEqualLengths(a: number[], b: number[]) {
'worklet';
if (__DEV__ && a.length !== b.length) {
throw new Error(
`[Reanimated] Cannot calculate inner product of two vectors of different lengths. Length of ${a.toString()} is ${
a.length
} and length of ${b.toString()} is ${b.length}.`
);
}
}
function innerProduct(a: number[], b: number[]) {
'worklet';
assertVectorsHaveEqualLengths(a, b);
return a.reduce((acc, _, i) => acc + a[i] * b[i], 0);
}
function projection(u: number[], a: number[]) {
'worklet';
assertVectorsHaveEqualLengths(u, a);
const s = innerProduct(u, a) / innerProduct(u, u);
return u.map((e) => e * s);
}
function subtractVectors(a: number[], b: number[]) {
'worklet';
assertVectorsHaveEqualLengths(a, b);
return a.map((_, i) => a[i] - b[i]);
}
function scaleVector(u: number[], a: number) {
'worklet';
return u.map((e) => e * a);
}
function gramSchmidtAlgorithm(matrix: AffineMatrix): {
rotationMatrix: AffineMatrix;
skewMatrix: AffineMatrix;
} {
// Gram-Schmidt orthogonalization decomposes any matrix with non-zero determinant into an orthogonal and a triangular matrix
// These matrices are equal to rotation and skew matrices respectively, because we apply it to transformation matrix
// That is expected to already have extracted the remaining transforms (scale & translation)
'worklet';
const [a0, a1, a2, a3] = matrix;
const u0 = a0;
const u1 = subtractVectors(a1, projection(u0, a1));
const u2 = subtractVectors(
subtractVectors(a2, projection(u0, a2)),
projection(u1, a2)
);
const u3 = subtractVectors(
subtractVectors(
subtractVectors(a3, projection(u0, a3)),
projection(u1, a3)
),
projection(u2, a3)
);
const [e0, e1, e2, e3] = [u0, u1, u2, u3].map((u) =>
scaleVector(u, 1 / Math.sqrt(innerProduct(u, u)))
);
const rotationMatrix: AffineMatrix = [
[e0[0], e1[0], e2[0], e3[0]],
[e0[1], e1[1], e2[1], e3[1]],
[e0[2], e1[2], e2[2], e3[2]],
[e0[3], e1[3], e2[3], e3[3]],
];
const skewMatrix: AffineMatrix = [
[
innerProduct(e0, a0),
innerProduct(e0, a1),
innerProduct(e0, a2),
innerProduct(e0, a3),
],
[0, innerProduct(e1, a1), innerProduct(e1, a2), innerProduct(e1, a3)],
[0, 0, innerProduct(e2, a2), innerProduct(e2, a3)],
[0, 0, 0, innerProduct(e3, a3)],
];
return {
rotationMatrix: transposeMatrix(rotationMatrix),
skewMatrix: transposeMatrix(skewMatrix),
};
}
// ts-prune-ignore-next This function is exported to be tested
export function decomposeMatrix(
unknownTypeMatrix: AffineMatrixFlat | AffineMatrix
): TransformMatrixDecomposition {
'worklet';
const matrix = maybeFlattenMatrix(unknownTypeMatrix);
// normalize matrix
if (matrix[15] === 0) {
throw new Error('[Reanimated] Invalid transform matrix.');
}
matrix.forEach((_, i) => (matrix[i] /= matrix[15]));
const translationMatrix: AffineMatrix = [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[matrix[12], matrix[13], matrix[14], 1],
];
const sx = matrix[15] * norm3d(matrix[0], matrix[4], matrix[8]);
const sy = matrix[15] * norm3d(matrix[1], matrix[5], matrix[9]);
const sz = matrix[15] * norm3d(matrix[2], matrix[6], matrix[10]);
// eslint-disable-next-line @typescript-eslint/no-shadow
const scaleMatrix: AffineMatrix = [
[sx, 0, 0, 0],
[0, sy, 0, 0],
[0, 0, sz, 0],
[0, 0, 0, 1],
];
const rotationAndSkewMatrix: AffineMatrix = [
[matrix[0] / sx, matrix[1] / sx, matrix[2] / sx, 0],
[matrix[4] / sy, matrix[5] / sy, matrix[6] / sy, 0],
[matrix[8] / sz, matrix[9] / sz, matrix[10] / sz, 0],
[0, 0, 0, 1],
];
const { rotationMatrix, skewMatrix } = gramSchmidtAlgorithm(
rotationAndSkewMatrix
);
return {
translationMatrix,
scaleMatrix,
rotationMatrix,
skewMatrix,
};
}
export function decomposeMatrixIntoMatricesAndAngles(
matrix: AffineMatrixFlat | AffineMatrix
): TansformMatrixDecompositionWithAngles {
'worklet';
// eslint-disable-next-line @typescript-eslint/no-shadow
const { scaleMatrix, rotationMatrix, translationMatrix, skewMatrix } =
decomposeMatrix(matrix);
const sinRy = -rotationMatrix[0][2];
const ry = Math.asin(sinRy);
let rx;
let rz;
if (sinRy === 1 || sinRy === -1) {
rz = 0;
rx = Math.atan2(sinRy * rotationMatrix[0][1], sinRy * rotationMatrix[0][2]);
} else {
rz = Math.atan2(rotationMatrix[0][1], rotationMatrix[0][0]);
rx = Math.atan2(rotationMatrix[1][2], rotationMatrix[2][2]);
}
return {
scaleMatrix,
rotationMatrix,
translationMatrix,
skewMatrix,
rx: rx || 0,
ry: ry || 0,
rz: rz || 0,
};
}

View File

@@ -0,0 +1,540 @@
/* eslint-disable @typescript-eslint/no-shadow */
'use strict';
import type { HigherOrderAnimation, StyleLayoutAnimation } from './commonTypes';
import type { ParsedColorArray } from '../Colors';
import {
isColor,
convertToRGBA,
rgbaArrayToRGBAColor,
toGammaSpace,
toLinearSpace,
} from '../Colors';
import { ReduceMotion, isWorkletFunction } from '../commonTypes';
import type {
SharedValue,
AnimatableValue,
Animation,
AnimationObject,
Timestamp,
AnimatableValueObject,
} from '../commonTypes';
import type {
AffineMatrixFlat,
AffineMatrix,
} from './transformationMatrix/matrixUtils';
import {
flatten,
multiplyMatrices,
scaleMatrix,
addMatrices,
decomposeMatrixIntoMatricesAndAngles,
isAffineMatrixFlat,
subtractMatrices,
getRotationMatrix,
} from './transformationMatrix/matrixUtils';
import { isReducedMotion, shouldBeUseWeb } from '../PlatformChecker';
import type { EasingFunction, EasingFunctionFactory } from '../Easing';
let IN_STYLE_UPDATER = false;
const IS_REDUCED_MOTION = isReducedMotion();
const SHOULD_BE_USE_WEB = shouldBeUseWeb();
if (__DEV__ && IS_REDUCED_MOTION) {
console.warn(
`[Reanimated] Reduced motion setting is enabled on this device. This warning is visible only in the development mode. Some animations will be disabled by default. You can override the behavior for individual animations, see https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#reduced-motion-setting-is-enabled-on-this-device.`
);
}
export function assertEasingIsWorklet(
easing: EasingFunction | EasingFunctionFactory
): void {
'worklet';
if (_WORKLET) {
// If this is called on UI (for example from gesture handler with worklets), we don't get easing,
// but its bound copy, which is not a worklet. We don't want to throw any error then.
return;
}
if (SHOULD_BE_USE_WEB) {
// It is possible to run reanimated on web without plugin, so let's skip this check on web
return;
}
// @ts-ignore typescript wants us to use `in` instead, which doesn't work with host objects
if (easing?.factory) {
return;
}
if (!isWorkletFunction(easing)) {
throw new Error(
'[Reanimated] The easing function is not a worklet. Please make sure you import `Easing` from react-native-reanimated.'
);
}
}
export function initialUpdaterRun<T>(updater: () => T) {
IN_STYLE_UPDATER = true;
const result = updater();
IN_STYLE_UPDATER = false;
return result;
}
interface RecognizedPrefixSuffix {
prefix?: string;
suffix?: string;
strippedValue: number;
}
export function recognizePrefixSuffix(
value: string | number
): RecognizedPrefixSuffix {
'worklet';
if (typeof value === 'string') {
const match = value.match(
/([A-Za-z]*)(-?\d*\.?\d*)([eE][-+]?[0-9]+)?([A-Za-z%]*)/
);
if (!match) {
throw new Error("[Reanimated] Couldn't parse animation value.");
}
const prefix = match[1];
const suffix = match[4];
// number with scientific notation
const number = match[2] + (match[3] ?? '');
return { prefix, suffix, strippedValue: parseFloat(number) };
} else {
return { strippedValue: value };
}
}
/**
* Returns whether the motion should be reduced for a specified config.
* By default returns the system setting.
*/
export function getReduceMotionFromConfig(config?: ReduceMotion) {
'worklet';
return !config || config === ReduceMotion.System
? IS_REDUCED_MOTION
: config === ReduceMotion.Always;
}
/**
* Returns the value that should be assigned to `animation.reduceMotion`
* for a given config. If the config is not defined, `undefined` is returned.
*/
export function getReduceMotionForAnimation(config?: ReduceMotion) {
'worklet';
// if the config is not defined, we want `reduceMotion` to be undefined,
// so the parent animation knows if it should overwrite it
if (!config) {
return undefined;
}
return getReduceMotionFromConfig(config);
}
function applyProgressToMatrix(
progress: number,
a: AffineMatrix,
b: AffineMatrix
) {
'worklet';
return addMatrices(a, scaleMatrix(subtractMatrices(b, a), progress));
}
function applyProgressToNumber(progress: number, a: number, b: number) {
'worklet';
return a + progress * (b - a);
}
function decorateAnimation<T extends AnimationObject | StyleLayoutAnimation>(
animation: T
): void {
'worklet';
const baseOnStart = (animation as Animation<AnimationObject>).onStart;
const baseOnFrame = (animation as Animation<AnimationObject>).onFrame;
if ((animation as HigherOrderAnimation).isHigherOrder) {
animation.onStart = (
animation: Animation<AnimationObject>,
value: number,
timestamp: Timestamp,
previousAnimation: Animation<AnimationObject>
) => {
if (animation.reduceMotion === undefined) {
animation.reduceMotion = getReduceMotionFromConfig();
}
return baseOnStart(animation, value, timestamp, previousAnimation);
};
return;
}
const animationCopy = Object.assign({}, animation);
delete animationCopy.callback;
const prefNumberSuffOnStart = (
animation: Animation<AnimationObject>,
value: string | number,
timestamp: number,
previousAnimation: Animation<AnimationObject>
) => {
// recognize prefix, suffix, and updates stripped value on animation start
const { prefix, suffix, strippedValue } = recognizePrefixSuffix(value);
animation.__prefix = prefix;
animation.__suffix = suffix;
animation.strippedCurrent = strippedValue;
const { strippedValue: strippedToValue } = recognizePrefixSuffix(
animation.toValue as string | number
);
animation.current = strippedValue;
animation.startValue = strippedValue;
animation.toValue = strippedToValue;
if (previousAnimation && previousAnimation !== animation) {
const {
prefix: paPrefix,
suffix: paSuffix,
strippedValue: paStrippedValue,
} = recognizePrefixSuffix(previousAnimation.current as string | number);
previousAnimation.current = paStrippedValue;
previousAnimation.__prefix = paPrefix;
previousAnimation.__suffix = paSuffix;
}
baseOnStart(animation, strippedValue, timestamp, previousAnimation);
animation.current =
(animation.__prefix ?? '') +
animation.current +
(animation.__suffix ?? '');
if (previousAnimation && previousAnimation !== animation) {
previousAnimation.current =
(previousAnimation.__prefix ?? '') +
// FIXME
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
previousAnimation.current +
(previousAnimation.__suffix ?? '');
}
};
const prefNumberSuffOnFrame = (
animation: Animation<AnimationObject>,
timestamp: number
) => {
animation.current = animation.strippedCurrent;
const res = baseOnFrame(animation, timestamp);
animation.strippedCurrent = animation.current;
animation.current =
(animation.__prefix ?? '') +
animation.current +
(animation.__suffix ?? '');
return res;
};
const tab = ['R', 'G', 'B', 'A'];
const colorOnStart = (
animation: Animation<AnimationObject>,
value: string | number,
timestamp: Timestamp,
previousAnimation: Animation<AnimationObject>
): void => {
let RGBAValue: ParsedColorArray;
let RGBACurrent: ParsedColorArray;
let RGBAToValue: ParsedColorArray;
const res: Array<number> = [];
if (isColor(value)) {
RGBACurrent = toLinearSpace(convertToRGBA(animation.current));
RGBAValue = toLinearSpace(convertToRGBA(value));
if (animation.toValue) {
RGBAToValue = toLinearSpace(convertToRGBA(animation.toValue));
}
}
tab.forEach((i, index) => {
animation[i] = Object.assign({}, animationCopy);
animation[i].current = RGBACurrent[index];
animation[i].toValue = RGBAToValue ? RGBAToValue[index] : undefined;
animation[i].onStart(
animation[i],
RGBAValue[index],
timestamp,
previousAnimation ? previousAnimation[i] : undefined
);
res.push(animation[i].current);
});
animation.current = rgbaArrayToRGBAColor(
toGammaSpace(res as ParsedColorArray)
);
};
const colorOnFrame = (
animation: Animation<AnimationObject>,
timestamp: Timestamp
): boolean => {
const RGBACurrent = toLinearSpace(convertToRGBA(animation.current));
const res: Array<number> = [];
let finished = true;
tab.forEach((i, index) => {
animation[i].current = RGBACurrent[index];
const result = animation[i].onFrame(animation[i], timestamp);
// We really need to assign this value to result, instead of passing it directly - otherwise once "finished" is false, onFrame won't be called
finished = finished && result;
res.push(animation[i].current);
});
animation.current = rgbaArrayToRGBAColor(
toGammaSpace(res as ParsedColorArray)
);
return finished;
};
const transformationMatrixOnStart = (
animation: Animation<AnimationObject>,
value: AffineMatrixFlat,
timestamp: Timestamp,
previousAnimation: Animation<AnimationObject>
): void => {
const toValue = animation.toValue as AffineMatrixFlat;
animation.startMatrices = decomposeMatrixIntoMatricesAndAngles(value);
animation.stopMatrices = decomposeMatrixIntoMatricesAndAngles(toValue);
// We create an animation copy to animate single value between 0 and 100
// We set limits from 0 to 100 (instead of 0-1) to make spring look good
// with default thresholds.
animation[0] = Object.assign({}, animationCopy);
animation[0].current = 0;
animation[0].toValue = 100;
animation[0].onStart(
animation[0],
0,
timestamp,
previousAnimation ? previousAnimation[0] : undefined
);
animation.current = value;
};
const transformationMatrixOnFrame = (
animation: Animation<AnimationObject>,
timestamp: Timestamp
): boolean => {
let finished = true;
const result = animation[0].onFrame(animation[0], timestamp);
// We really need to assign this value to result, instead of passing it directly - otherwise once "finished" is false, onFrame won't be called
finished = finished && result;
const progress = animation[0].current / 100;
const transforms = ['translationMatrix', 'scaleMatrix', 'skewMatrix'];
const mappedTransforms: Array<AffineMatrix> = [];
transforms.forEach((key, _) =>
mappedTransforms.push(
applyProgressToMatrix(
progress,
animation.startMatrices[key],
animation.stopMatrices[key]
)
)
);
const [currentTranslation, currentScale, skewMatrix] = mappedTransforms;
const rotations: Array<'x' | 'y' | 'z'> = ['x', 'y', 'z'];
const mappedRotations: Array<AffineMatrix> = [];
rotations.forEach((key, _) => {
const angle = applyProgressToNumber(
progress,
animation.startMatrices['r' + key],
animation.stopMatrices['r' + key]
);
mappedRotations.push(getRotationMatrix(angle, key));
});
const [rotationMatrixX, rotationMatrixY, rotationMatrixZ] = mappedRotations;
const rotationMatrix = multiplyMatrices(
rotationMatrixX,
multiplyMatrices(rotationMatrixY, rotationMatrixZ)
);
const updated = flatten(
multiplyMatrices(
multiplyMatrices(
currentScale,
multiplyMatrices(skewMatrix, rotationMatrix)
),
currentTranslation
)
);
animation.current = updated;
return finished;
};
const arrayOnStart = (
animation: Animation<AnimationObject>,
value: Array<number>,
timestamp: Timestamp,
previousAnimation: Animation<AnimationObject>
): void => {
value.forEach((v, i) => {
animation[i] = Object.assign({}, animationCopy);
animation[i].current = v;
animation[i].toValue = (animation.toValue as Array<number>)[i];
animation[i].onStart(
animation[i],
v,
timestamp,
previousAnimation ? previousAnimation[i] : undefined
);
});
animation.current = value;
};
const arrayOnFrame = (
animation: Animation<AnimationObject>,
timestamp: Timestamp
): boolean => {
let finished = true;
(animation.current as Array<number>).forEach((_, i) => {
const result = animation[i].onFrame(animation[i], timestamp);
// We really need to assign this value to result, instead of passing it directly - otherwise once "finished" is false, onFrame won't be called
finished = finished && result;
(animation.current as Array<number>)[i] = animation[i].current;
});
return finished;
};
const objectOnStart = (
animation: Animation<AnimationObject>,
value: AnimatableValueObject,
timestamp: Timestamp,
previousAnimation: Animation<AnimationObject>
): void => {
for (const key in value) {
animation[key] = Object.assign({}, animationCopy);
animation[key].onStart = animation.onStart;
animation[key].current = value[key];
animation[key].toValue = (animation.toValue as AnimatableValueObject)[
key
];
animation[key].onStart(
animation[key],
value[key],
timestamp,
previousAnimation ? previousAnimation[key] : undefined
);
}
animation.current = value;
};
const objectOnFrame = (
animation: Animation<AnimationObject>,
timestamp: Timestamp
): boolean => {
let finished = true;
const newObject: AnimatableValueObject = {};
for (const key in animation.current as AnimatableValueObject) {
const result = animation[key].onFrame(animation[key], timestamp);
// We really need to assign this value to result, instead of passing it directly - otherwise once "finished" is false, onFrame won't be called
finished = finished && result;
newObject[key] = animation[key].current;
}
animation.current = newObject;
return finished;
};
animation.onStart = (
animation: Animation<AnimationObject>,
value: number,
timestamp: Timestamp,
previousAnimation: Animation<AnimationObject>
) => {
if (animation.reduceMotion === undefined) {
animation.reduceMotion = getReduceMotionFromConfig();
}
if (animation.reduceMotion) {
if (animation.toValue !== undefined) {
animation.current = animation.toValue;
} else {
// if there is no `toValue`, then the base function is responsible for setting the current value
baseOnStart(animation, value, timestamp, previousAnimation);
}
animation.startTime = 0;
animation.onFrame = () => true;
return;
}
if (isColor(value)) {
colorOnStart(animation, value, timestamp, previousAnimation);
animation.onFrame = colorOnFrame;
return;
} else if (isAffineMatrixFlat(value)) {
transformationMatrixOnStart(
animation,
value,
timestamp,
previousAnimation
);
animation.onFrame = transformationMatrixOnFrame;
return;
} else if (Array.isArray(value)) {
arrayOnStart(animation, value, timestamp, previousAnimation);
animation.onFrame = arrayOnFrame;
return;
} else if (typeof value === 'string') {
prefNumberSuffOnStart(animation, value, timestamp, previousAnimation);
animation.onFrame = prefNumberSuffOnFrame;
return;
} else if (typeof value === 'object' && value !== null) {
objectOnStart(animation, value, timestamp, previousAnimation);
animation.onFrame = objectOnFrame;
return;
}
baseOnStart(animation, value, timestamp, previousAnimation);
};
}
type AnimationToDecoration<
T extends AnimationObject | StyleLayoutAnimation,
U extends AnimationObject | StyleLayoutAnimation
> = T extends StyleLayoutAnimation
? Record<string, unknown>
: U | (() => U) | AnimatableValue;
export function defineAnimation<
T extends AnimationObject | StyleLayoutAnimation, // type that's supposed to be returned
U extends AnimationObject | StyleLayoutAnimation = T // type that's received
>(starting: AnimationToDecoration<T, U>, factory: () => T): T {
'worklet';
if (IN_STYLE_UPDATER) {
return starting as unknown as T;
}
const create = () => {
'worklet';
const animation = factory();
decorateAnimation<U>(animation as unknown as U);
return animation;
};
if (_WORKLET || SHOULD_BE_USE_WEB) {
return create();
}
// @ts-ignore: eslint-disable-line
return create;
}
/**
* Lets you cancel a running animation paired to a shared value.
*
* @param sharedValue - The shared value of a running animation that you want to cancel.
* @see https://docs.swmansion.com/react-native-reanimated/docs/core/cancelAnimation
*/
export function cancelAnimation<T>(sharedValue: SharedValue<T>): void {
'worklet';
// setting the current value cancels the animation if one is currently running
sharedValue.value = sharedValue.value; // eslint-disable-line no-self-assign
}

View File

@@ -0,0 +1,308 @@
'use strict';
import type { ViewStyle, TextStyle } from 'react-native';
export type RequiredKeys<T, K extends keyof T> = T & Required<Pick<T, K>>;
export interface StyleProps extends ViewStyle, TextStyle {
originX?: number;
originY?: number;
[key: string]: any;
}
/**
* A value that can be used both on the [JavaScript thread](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/glossary#javascript-thread) and the [UI thread](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/glossary#ui-thread).
*
* Shared values are defined using [useSharedValue](https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue) hook. You access and modify shared values by their `.value` property.
*/
export interface SharedValue<Value = unknown> {
value: Value;
addListener: (listenerID: number, listener: (value: Value) => void) => void;
removeListener: (listenerID: number) => void;
modify: (
modifier?: <T extends Value>(value: T) => T,
forceUpdate?: boolean
) => void;
}
export interface Mutable<Value = unknown> extends SharedValue<Value> {
_isReanimatedSharedValue: true;
_animation?: AnimationObject<Value> | null; // only in Native
_value: Value;
}
// The below type is used for HostObjects returned by the JSI API that don't have
// any accessible fields or methods but can carry data that is accessed from the
// c++ side. We add a field to the type to make it possible for typescript to recognize
// which JSI methods accept those types as arguments and to be able to correctly type
// check other methods that may use them. However, this field is not actually defined
// nor should be used for anything else as assigning any data to those objects will
// throw an error.
export type ShareableRef<T = unknown> = {
__hostObjectShareableJSRef: T;
};
// In case of objects with depth or arrays of objects or arrays of arrays etc.
// we add this utility type that makes it a SharaebleRef of the outermost type.
export type FlatShareableRef<T> = T extends ShareableRef<infer U>
? ShareableRef<U>
: ShareableRef<T>;
export type MapperRawInputs = unknown[];
export type MapperOutputs = SharedValue[];
export type MapperRegistry = {
start: (
mapperID: number,
worklet: () => void,
inputs: MapperRawInputs,
outputs?: MapperOutputs
) => void;
stop: (mapperID: number) => void;
};
export type WorkletStackDetails = [
error: Error,
lineOffset: number,
columnOffset: number
];
type WorkletClosure = Record<string, unknown>;
interface WorkletInitDataCommon {
code: string;
}
type WorkletInitDataRelease = WorkletInitDataCommon;
interface WorkletInitDataDev extends WorkletInitDataCommon {
location: string;
sourceMap: string;
version: string;
}
interface WorkletBaseCommon {
__closure: WorkletClosure;
__workletHash: number;
}
interface WorkletBaseRelease extends WorkletBaseCommon {
__initData: WorkletInitDataRelease;
}
interface WorkletBaseDev extends WorkletBaseCommon {
__initData: WorkletInitDataDev;
/**
* `__stackDetails` is removed after parsing.
*/
__stackDetails?: WorkletStackDetails;
}
export type WorkletFunction<
Args extends unknown[] = unknown[],
ReturnValue = unknown
> = ((...args: Args) => ReturnValue) & (WorkletBaseRelease | WorkletBaseDev);
/**
* This function allows you to determine if a given function is a worklet. It only works
* with Reanimated Babel plugin enabled. Unless you are doing something with internals of
* Reanimated you shouldn't need to use this function.
*
* ### Note
* Do not call it before the worklet is declared, as it will always return false then. E.g.:
*
* ```ts
* isWorkletFunction(myWorklet); // Will always return false.
*
* function myWorklet() {
* 'worklet';
* };
* ```
*
* ### Maintainer note
* This function works well on the JS thread performance-wise, since the JIT can inline it.
* However, on other threads it will not get optimized and we will get a function call overhead.
* We want to change it in the future, but it's not feasible at the moment.
*/
export function isWorkletFunction<
Args extends unknown[] = unknown[],
ReturnValue = unknown,
BuildType extends WorkletBaseDev | WorkletBaseRelease = WorkletBaseDev
>(value: unknown): value is WorkletFunction<Args, ReturnValue> & BuildType {
'worklet';
// Since host objects always return true for `in` operator, we have to use dot notation to check if the property exists.
// See https://github.com/facebook/hermes/blob/340726ef8cf666a7cce75bc60b02fa56b3e54560/lib/VM/JSObject.cpp#L1276.
return (
typeof value === 'function' &&
!!(value as unknown as Record<string, unknown>).__workletHash
);
}
export type AnimatedPropsAdapterFunction = (
props: Record<string, unknown>
) => void;
export type AnimatedPropsAdapterWorklet = WorkletFunction<
[props: Record<string, unknown>],
void
>;
export interface NestedObject<T> {
[key: string]: NestedObjectValues<T>;
}
export type NestedObjectValues<T> =
| T
| Array<NestedObjectValues<T>>
| NestedObject<T>;
type Animatable = number | string | Array<number>;
export type AnimatableValueObject = { [key: string]: Animatable };
export type AnimatableValue = Animatable | AnimatableValueObject;
export interface AnimationObject<T = AnimatableValue> {
[key: string]: any;
callback?: AnimationCallback;
current?: T;
toValue?: AnimationObject<T>['current'];
startValue?: AnimationObject<T>['current'];
finished?: boolean;
strippedCurrent?: number;
cancelled?: boolean;
reduceMotion?: boolean;
__prefix?: string;
__suffix?: string;
onFrame: (animation: any, timestamp: Timestamp) => boolean;
onStart: (
nextAnimation: any,
current: any,
timestamp: Timestamp,
previousAnimation: any
) => void;
}
export interface Animation<T extends AnimationObject> extends AnimationObject {
onFrame: (animation: T, timestamp: Timestamp) => boolean;
onStart: (
nextAnimation: T,
current: AnimatableValue,
timestamp: Timestamp,
previousAnimation: Animation<any> | null | T
) => void;
}
export enum SensorType {
ACCELEROMETER = 1,
GYROSCOPE = 2,
GRAVITY = 3,
MAGNETIC_FIELD = 4,
ROTATION = 5,
}
export enum IOSReferenceFrame {
XArbitraryZVertical,
XArbitraryCorrectedZVertical,
XMagneticNorthZVertical,
XTrueNorthZVertical,
Auto,
}
export type SensorConfig = {
interval: number | 'auto';
adjustToInterfaceOrientation: boolean;
iosReferenceFrame: IOSReferenceFrame;
};
export type AnimatedSensor<T extends Value3D | ValueRotation> = {
sensor: SharedValue<T>;
unregister: () => void;
isAvailable: boolean;
config: SensorConfig;
};
/**
* A function called upon animation completion. If the animation is cancelled, the callback will receive `false` as the argument; otherwise, it will receive `true`.
*/
export type AnimationCallback = (
finished?: boolean,
current?: AnimatableValue
) => void;
export type Timestamp = number;
export type Value3D = {
x: number;
y: number;
z: number;
interfaceOrientation: InterfaceOrientation;
};
export type ValueRotation = {
qw: number;
qx: number;
qy: number;
qz: number;
yaw: number;
pitch: number;
roll: number;
interfaceOrientation: InterfaceOrientation;
};
export enum InterfaceOrientation {
ROTATION_0 = 0,
ROTATION_90 = 90,
ROTATION_180 = 180,
ROTATION_270 = 270,
}
export type ShadowNodeWrapper = {
__hostObjectShadowNodeWrapper: never;
};
export enum KeyboardState {
UNKNOWN = 0,
OPENING = 1,
OPEN = 2,
CLOSING = 3,
CLOSED = 4,
}
export type AnimatedKeyboardInfo = {
height: SharedValue<number>;
state: SharedValue<KeyboardState>;
};
/**
* @param x - A number representing X coordinate relative to the parent component.
* @param y - A number representing Y coordinate relative to the parent component.
* @param width - A number representing the width of the component.
* @param height - A number representing the height of the component.
* @param pageX - A number representing X coordinate relative to the screen.
* @param pageY - A number representing Y coordinate relative to the screen.
* @see https://docs.swmansion.com/react-native-reanimated/docs/advanced/measure#returns
*/
export interface MeasuredDimensions {
x: number;
y: number;
width: number;
height: number;
pageX: number;
pageY: number;
}
export interface AnimatedKeyboardOptions {
isStatusBarTranslucentAndroid?: boolean;
}
/**
* @param System - If the `Reduce motion` accessibility setting is enabled on the device, disable the animation. Otherwise, enable the animation.
* @param Always - Disable the animation.
* @param Never - Enable the animation.
* @see https://docs.swmansion.com/react-native-reanimated/docs/guides/accessibility
*/
export enum ReduceMotion {
System = 'system',
Always = 'always',
Never = 'never',
}

View File

@@ -0,0 +1,121 @@
'use strict';
import React, { forwardRef, useRef } from 'react';
import type {
FlatListProps,
LayoutChangeEvent,
StyleProp,
ViewStyle,
} from 'react-native';
import { FlatList } from 'react-native';
import { AnimatedView } from './View';
import { createAnimatedComponent } from '../../createAnimatedComponent';
import type { ILayoutAnimationBuilder } from '../layoutReanimation/animationBuilder/commonTypes';
import { LayoutAnimationConfig } from './LayoutAnimationConfig';
import type { AnimatedProps, AnimatedStyle } from '../helperTypes';
const AnimatedFlatList = createAnimatedComponent(FlatList);
interface CellRendererComponentProps {
onLayout?: ((event: LayoutChangeEvent) => void) | undefined;
children: React.ReactNode;
style?: StyleProp<AnimatedStyle<ViewStyle>>;
}
const createCellRendererComponent = (
itemLayoutAnimationRef?: React.MutableRefObject<
ILayoutAnimationBuilder | undefined
>
) => {
const CellRendererComponent = (props: CellRendererComponentProps) => {
return (
<AnimatedView
// TODO TYPESCRIPT This is temporary cast is to get rid of .d.ts file.
layout={itemLayoutAnimationRef?.current as any}
onLayout={props.onLayout}
style={props.style}>
{props.children}
</AnimatedView>
);
};
return CellRendererComponent;
};
interface ReanimatedFlatListPropsWithLayout<T>
extends AnimatedProps<FlatListProps<T>> {
/**
* Lets you pass layout animation directly to the FlatList item.
*/
itemLayoutAnimation?: ILayoutAnimationBuilder;
/**
* Lets you skip entering and exiting animations of FlatList items when on FlatList mount or unmount.
*/
skipEnteringExitingAnimations?: boolean;
}
export type FlatListPropsWithLayout<T> = ReanimatedFlatListPropsWithLayout<T>;
// Since createAnimatedComponent return type is ComponentClass that has the props of the argument,
// but not things like NativeMethods, etc. we need to add them manually by extending the type.
interface AnimatedFlatListComplement<T> extends FlatList<T> {
getNode(): FlatList<T>;
}
// We need explicit any here, because this is the exact same type that is used in React Native types.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const FlatListForwardRefRender = function <Item = any>(
props: ReanimatedFlatListPropsWithLayout<Item>,
ref: React.ForwardedRef<FlatList>
) {
const { itemLayoutAnimation, skipEnteringExitingAnimations, ...restProps } =
props;
// Set default scrollEventThrottle, because user expects
// to have continuous scroll events and
// react-native defaults it to 50 for FlatLists.
// We set it to 1, so we have peace until
// there are 960 fps screens.
if (!('scrollEventThrottle' in restProps)) {
restProps.scrollEventThrottle = 1;
}
const itemLayoutAnimationRef = useRef(itemLayoutAnimation);
itemLayoutAnimationRef.current = itemLayoutAnimation;
const CellRendererComponent = React.useMemo(
() => createCellRendererComponent(itemLayoutAnimationRef),
[itemLayoutAnimationRef]
);
const animatedFlatList = (
// @ts-expect-error In its current type state, createAnimatedComponent cannot create generic components.
<AnimatedFlatList
ref={ref}
{...restProps}
CellRendererComponent={CellRendererComponent}
/>
);
if (skipEnteringExitingAnimations === undefined) {
return animatedFlatList;
}
return (
<LayoutAnimationConfig skipEntering skipExiting>
{animatedFlatList}
</LayoutAnimationConfig>
);
};
export const ReanimatedFlatList = forwardRef(FlatListForwardRefRender) as <
// We need explicit any here, because this is the exact same type that is used in React Native types.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ItemT = any
>(
props: ReanimatedFlatListPropsWithLayout<ItemT> & {
ref?: React.ForwardedRef<FlatList>;
}
) => React.ReactElement;
export type ReanimatedFlatList<T> = typeof AnimatedFlatList &
AnimatedFlatListComplement<T>;

View File

@@ -0,0 +1,13 @@
'use strict';
import { Image } from 'react-native';
import { createAnimatedComponent } from '../../createAnimatedComponent';
// Since createAnimatedComponent return type is ComponentClass that has the props of the argument,
// but not things like NativeMethods, etc. we need to add them manually by extending the type.
interface AnimatedImageComplement extends Image {
getNode(): Image;
}
export const AnimatedImage = createAnimatedComponent(Image);
export type AnimatedImage = typeof AnimatedImage & AnimatedImageComplement;

View File

@@ -0,0 +1,89 @@
'use strict';
import React, {
Children,
Component,
createContext,
useEffect,
useRef,
} from 'react';
import type { ReactNode } from 'react';
import { setShouldAnimateExitingForTag } from '../core';
import { findNodeHandle } from 'react-native';
export const SkipEnteringContext =
createContext<React.MutableRefObject<boolean> | null>(null);
// skipEntering - don't animate entering of children on wrapper mount
// skipExiting - don't animate exiting of children on wrapper unmount
interface LayoutAnimationConfigProps {
skipEntering?: boolean;
skipExiting?: boolean;
children: ReactNode;
}
function SkipEntering(props: { shouldSkip: boolean; children: ReactNode }) {
const skipValueRef = useRef(props.shouldSkip);
useEffect(() => {
skipValueRef.current = false;
}, [skipValueRef]);
return (
<SkipEnteringContext.Provider value={skipValueRef}>
{props.children}
</SkipEnteringContext.Provider>
);
}
// skipExiting (unlike skipEntering) cannot be done by conditionally
// configuring the animation in `createAnimatedComponent`, since at this stage
// we don't know if the wrapper is going to be unmounted or not.
// That's why we need to pass the skipExiting flag to the native side
// when the wrapper is unmounted to prevent the animation.
// Since `ReactNode` can be a list of nodes, we wrap every child with our wrapper
// so we are able to access its tag with `findNodeHandle`.
/**
* A component that lets you skip entering and exiting animations.
*
* @param skipEntering - A boolean indicating whether children's entering animations should be skipped when `LayoutAnimationConfig` is mounted.
* @param skipExiting - A boolean indicating whether children's exiting animations should be skipped when LayoutAnimationConfig is unmounted.
* @see https://docs.swmansion.com/react-native-reanimated/docs/layout-animations/layout-animation-config/
*/
export class LayoutAnimationConfig extends Component<LayoutAnimationConfigProps> {
getMaybeWrappedChildren() {
return Children.count(this.props.children) > 1 && this.props.skipExiting
? Children.map(this.props.children, (child) => (
<LayoutAnimationConfig skipExiting>{child}</LayoutAnimationConfig>
))
: this.props.children;
}
setShouldAnimateExiting() {
if (Children.count(this.props.children) === 1) {
const tag = findNodeHandle(this);
if (tag) {
setShouldAnimateExitingForTag(tag, !this.props.skipExiting);
}
}
}
componentWillUnmount(): void {
if (this.props.skipExiting !== undefined) {
this.setShouldAnimateExiting();
}
}
render(): ReactNode {
const children = this.getMaybeWrappedChildren();
if (this.props.skipEntering === undefined) {
return children;
}
return (
<SkipEntering shouldSkip={this.props.skipEntering}>
{children}
</SkipEntering>
);
}
}

View File

@@ -0,0 +1,216 @@
'use strict';
import React, { useEffect, useRef } from 'react';
import { TextInput, StyleSheet, View } from 'react-native';
import type { FrameInfo } from '../frameCallback';
import type { SharedValue } from '../commonTypes';
import { useSharedValue, useAnimatedProps, useFrameCallback } from '../hook';
import { createAnimatedComponent } from '../../createAnimatedComponent';
import { addWhitelistedNativeProps } from '../../ConfigHelper';
type CircularBuffer = ReturnType<typeof createCircularDoublesBuffer>;
function createCircularDoublesBuffer(size: number) {
'worklet';
return {
next: 0 as number,
buffer: new Float32Array(size),
size,
count: 0 as number,
push(value: number): number | null {
const oldValue = this.buffer[this.next];
const oldCount = this.count;
this.buffer[this.next] = value;
this.next = (this.next + 1) % this.size;
this.count = Math.min(this.size, this.count + 1);
return oldCount === this.size ? oldValue : null;
},
front(): number | null {
const notEmpty = this.count > 0;
if (notEmpty) {
const current = this.next - 1;
const index = current < 0 ? this.size - 1 : current;
return this.buffer[index];
}
return null;
},
back(): number | null {
const notEmpty = this.count > 0;
return notEmpty ? this.buffer[this.next] : null;
},
};
}
const DEFAULT_BUFFER_SIZE = 60;
addWhitelistedNativeProps({ text: true });
const AnimatedTextInput = createAnimatedComponent(TextInput);
function loopAnimationFrame(fn: (lastTime: number, time: number) => void) {
let lastTime = 0;
function loop() {
requestAnimationFrame((time) => {
if (lastTime > 0) {
fn(lastTime, time);
}
lastTime = time;
requestAnimationFrame(loop);
});
}
loop();
}
function getFps(renderTimeInMs: number): number {
'worklet';
return 1000 / renderTimeInMs;
}
function getTimeDelta(
timestamp: number,
previousTimestamp: number | null
): number {
'worklet';
return previousTimestamp !== null ? timestamp - previousTimestamp : 0;
}
function completeBufferRoutine(
buffer: CircularBuffer,
timestamp: number,
previousTimestamp: number,
totalRenderTime: SharedValue<number>
): number {
'worklet';
timestamp = Math.round(timestamp);
previousTimestamp = Math.round(previousTimestamp) ?? timestamp;
const droppedTimestamp = buffer.push(timestamp);
const nextToDrop = buffer.back()!;
const delta = getTimeDelta(timestamp, previousTimestamp);
const droppedDelta = getTimeDelta(nextToDrop, droppedTimestamp);
totalRenderTime.value += delta - droppedDelta;
return getFps(totalRenderTime.value / buffer.count);
}
function JsPerformance() {
const jsFps = useSharedValue<string | null>(null);
const totalRenderTime = useSharedValue(0);
const circularBuffer = useRef<CircularBuffer>(
createCircularDoublesBuffer(DEFAULT_BUFFER_SIZE)
);
useEffect(() => {
loopAnimationFrame((_, timestamp) => {
timestamp = Math.round(timestamp);
const previousTimestamp = circularBuffer.current.front() ?? timestamp;
const currentFps = completeBufferRoutine(
circularBuffer.current,
timestamp,
previousTimestamp,
totalRenderTime
);
// JS fps have to be measured every 2nd frame,
// thus 2x multiplication has to occur here
jsFps.value = (currentFps * 2).toFixed(0);
});
}, []);
const animatedProps = useAnimatedProps(() => {
const text = 'JS: ' + jsFps.value ?? 'N/A';
return { text, defaultValue: text };
});
return (
<View style={styles.container}>
<AnimatedTextInput
style={styles.text}
animatedProps={animatedProps}
editable={false}
/>
</View>
);
}
function UiPerformance() {
const uiFps = useSharedValue<string | null>(null);
const totalRenderTime = useSharedValue(0);
const circularBuffer = useSharedValue<CircularBuffer | null>(null);
useFrameCallback(({ timestamp }: FrameInfo) => {
if (circularBuffer.value === null) {
circularBuffer.value = createCircularDoublesBuffer(DEFAULT_BUFFER_SIZE);
}
timestamp = Math.round(timestamp);
const previousTimestamp = circularBuffer.value.front() ?? timestamp;
const currentFps = completeBufferRoutine(
circularBuffer.value,
timestamp,
previousTimestamp,
totalRenderTime
);
uiFps.value = currentFps.toFixed(0);
});
const animatedProps = useAnimatedProps(() => {
const text = 'UI: ' + uiFps.value ?? 'N/A';
return { text, defaultValue: text };
});
return (
<View style={styles.container}>
<AnimatedTextInput
style={styles.text}
animatedProps={animatedProps}
editable={false}
/>
</View>
);
}
export function PerformanceMonitor() {
return (
<View style={styles.monitor}>
<JsPerformance />
<UiPerformance />
</View>
);
}
const styles = StyleSheet.create({
monitor: {
flexDirection: 'row',
position: 'absolute',
backgroundColor: '#0006',
zIndex: 1000,
},
header: {
fontSize: 14,
color: '#ffff',
paddingHorizontal: 5,
},
text: {
fontSize: 13,
color: '#ffff',
fontFamily: 'monospace',
paddingHorizontal: 3,
},
container: {
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
flexWrap: 'wrap',
},
});

View File

@@ -0,0 +1,53 @@
'use strict';
import type { ForwardedRef } from 'react';
import React, { forwardRef } from 'react';
import type { ScrollViewProps } from 'react-native';
import { ScrollView } from 'react-native';
import { createAnimatedComponent } from '../../createAnimatedComponent';
import type { SharedValue } from '../commonTypes';
import type { AnimatedRef } from '../hook';
import { useAnimatedRef, useScrollViewOffset } from '../hook';
import type { AnimatedProps } from '../helperTypes';
export interface AnimatedScrollViewProps
extends AnimatedProps<ScrollViewProps> {
scrollViewOffset?: SharedValue<number>;
}
// Since createAnimatedComponent return type is ComponentClass that has the props of the argument,
// but not things like NativeMethods, etc. we need to add them manually by extending the type.
interface AnimatedScrollViewComplement extends ScrollView {
getNode(): ScrollView;
}
const AnimatedScrollViewComponent = createAnimatedComponent(ScrollView);
export const AnimatedScrollView = forwardRef(
(props: AnimatedScrollViewProps, ref: ForwardedRef<AnimatedScrollView>) => {
const { scrollViewOffset, ...restProps } = props;
const animatedRef = (
ref === null
? // eslint-disable-next-line react-hooks/rules-of-hooks
useAnimatedRef<ScrollView>()
: ref
) as AnimatedRef<AnimatedScrollView>;
if (scrollViewOffset) {
// eslint-disable-next-line react-hooks/rules-of-hooks
useScrollViewOffset(animatedRef, scrollViewOffset);
}
// Set default scrollEventThrottle, because user expects
// to have continuous scroll events.
// We set it to 1 so we have peace until
// there are 960 fps screens.
if (!('scrollEventThrottle' in restProps)) {
restProps.scrollEventThrottle = 1;
}
return <AnimatedScrollViewComponent ref={animatedRef} {...restProps} />;
}
);
export type AnimatedScrollView = AnimatedScrollViewComplement &
typeof AnimatedScrollViewComponent;

View File

@@ -0,0 +1,13 @@
'use strict';
import { Text } from 'react-native';
import { createAnimatedComponent } from '../../createAnimatedComponent';
// Since createAnimatedComponent return type is ComponentClass that has the props of the argument,
// but not things like NativeMethods, etc. we need to add them manually by extending the type.
interface AnimatedTextComplement extends Text {
getNode(): Text;
}
export const AnimatedText = createAnimatedComponent(Text);
export type AnimatedText = typeof AnimatedText & AnimatedTextComplement;

View File

@@ -0,0 +1,13 @@
'use strict';
import { View } from 'react-native';
import { createAnimatedComponent } from '../../createAnimatedComponent';
// Since createAnimatedComponent return type is ComponentClass that has the props of the argument,
// but not things like NativeMethods, etc. we need to add them manually by extending the type.
interface AnimatedViewComplement extends View {
getNode(): View;
}
export const AnimatedView = createAnimatedComponent(View);
export type AnimatedView = typeof AnimatedView & AnimatedViewComplement;

View File

@@ -0,0 +1,216 @@
'use strict';
import NativeReanimatedModule from './NativeReanimated';
import { isWeb, shouldBeUseWeb, isFabric } from './PlatformChecker';
import type {
AnimatedKeyboardOptions,
SensorConfig,
SensorType,
SharedValue,
Value3D,
ValueRotation,
} from './commonTypes';
import { makeShareableCloneRecursive } from './shareables';
import { initializeUIRuntime } from './initializers';
import type { LayoutAnimationBatchItem } from './layoutReanimation/animationBuilder/commonTypes';
import { SensorContainer } from './SensorContainer';
export { startMapper, stopMapper } from './mappers';
export { runOnJS, runOnUI, executeOnUIRuntimeSync } from './threads';
export { createWorkletRuntime, runOnRuntime } from './runtimes';
export type { WorkletRuntime } from './runtimes';
export { makeShareable, makeShareableCloneRecursive } from './shareables';
export { makeMutable } from './mutables';
const SHOULD_BE_USE_WEB = shouldBeUseWeb();
/**
* @returns `true` in Reanimated 3, doesn't exist in Reanimated 2 or 1
*/
export const isReanimated3 = () => true;
// Superseded by check in `/src/threads.ts`.
// Used by `react-navigation` to detect if using Reanimated 2 or 3.
/**
* @deprecated This function was superseded by other checks.
* We keep it here for backward compatibility reasons.
* If you need to check if you are using Reanimated 3 or Reanimated 2
* please use `isReanimated3` function instead.
* @returns `true` in Reanimated 3, doesn't exist in Reanimated 2
*/
export const isConfigured = isReanimated3;
// this is for web implementation
if (SHOULD_BE_USE_WEB) {
global._WORKLET = false;
global._log = console.log;
global._getAnimationTimestamp = () => performance.now();
}
export function getViewProp<T>(
viewTag: number,
propName: string,
component?: React.Component // required on Fabric
): Promise<T> {
if (isFabric() && !component) {
throw new Error(
'[Reanimated] Function `getViewProp` requires a component to be passed as an argument on Fabric.'
);
}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
return new Promise((resolve, reject) => {
return NativeReanimatedModule.getViewProp(
viewTag,
propName,
component,
(result: T) => {
if (typeof result === 'string' && result.substr(0, 6) === 'error:') {
reject(result);
} else {
resolve(result);
}
}
);
});
}
function getSensorContainer(): SensorContainer {
if (!global.__sensorContainer) {
global.__sensorContainer = new SensorContainer();
}
return global.__sensorContainer;
}
export function registerEventHandler<T>(
eventHandler: (event: T) => void,
eventName: string,
emitterReactTag = -1
): number {
function handleAndFlushAnimationFrame(eventTimestamp: number, event: T) {
'worklet';
global.__frameTimestamp = eventTimestamp;
eventHandler(event);
global.__flushAnimationFrame(eventTimestamp);
global.__frameTimestamp = undefined;
}
return NativeReanimatedModule.registerEventHandler(
makeShareableCloneRecursive(handleAndFlushAnimationFrame),
eventName,
emitterReactTag
);
}
export function unregisterEventHandler(id: number): void {
return NativeReanimatedModule.unregisterEventHandler(id);
}
export function subscribeForKeyboardEvents(
eventHandler: (state: number, height: number) => void,
options: AnimatedKeyboardOptions
): number {
// TODO: this should really go with the same code path as other events, that is
// via registerEventHandler. For now we are copying the code from there.
function handleAndFlushAnimationFrame(state: number, height: number) {
'worklet';
const now = global._getAnimationTimestamp();
global.__frameTimestamp = now;
eventHandler(state, height);
global.__flushAnimationFrame(now);
global.__frameTimestamp = undefined;
}
return NativeReanimatedModule.subscribeForKeyboardEvents(
makeShareableCloneRecursive(handleAndFlushAnimationFrame),
options.isStatusBarTranslucentAndroid ?? false
);
}
export function unsubscribeFromKeyboardEvents(listenerId: number): void {
return NativeReanimatedModule.unsubscribeFromKeyboardEvents(listenerId);
}
export function registerSensor(
sensorType: SensorType,
config: SensorConfig,
eventHandler: (
data: Value3D | ValueRotation,
orientationDegrees: number
) => void
): number {
const sensorContainer = getSensorContainer();
return sensorContainer.registerSensor(
sensorType,
config,
makeShareableCloneRecursive(eventHandler)
);
}
export function initializeSensor(
sensorType: SensorType,
config: SensorConfig
): SharedValue<Value3D | ValueRotation> {
const sensorContainer = getSensorContainer();
return sensorContainer.initializeSensor(sensorType, config);
}
export function unregisterSensor(sensorId: number): void {
const sensorContainer = getSensorContainer();
return sensorContainer.unregisterSensor(sensorId);
}
if (!isWeb()) {
initializeUIRuntime();
}
type FeaturesConfig = {
enableLayoutAnimations: boolean;
setByUser: boolean;
};
let featuresConfig: FeaturesConfig = {
enableLayoutAnimations: false,
setByUser: false,
};
export function enableLayoutAnimations(
flag: boolean,
isCallByUser = true
): void {
if (isCallByUser) {
featuresConfig = {
enableLayoutAnimations: flag,
setByUser: true,
};
NativeReanimatedModule.enableLayoutAnimations(flag);
} else if (
!featuresConfig.setByUser &&
featuresConfig.enableLayoutAnimations !== flag
) {
featuresConfig.enableLayoutAnimations = flag;
NativeReanimatedModule.enableLayoutAnimations(flag);
}
}
export function configureLayoutAnimationBatch(
layoutAnimationsBatch: LayoutAnimationBatchItem[]
): void {
NativeReanimatedModule.configureLayoutAnimationBatch(layoutAnimationsBatch);
}
export function setShouldAnimateExitingForTag(
viewTag: number | HTMLElement,
shouldAnimate: boolean
) {
NativeReanimatedModule.setShouldAnimateExitingForTag(
viewTag as number,
shouldAnimate
);
}
export function jsiConfigureProps(
uiProps: string[],
nativeProps: string[]
): void {
if (!SHOULD_BE_USE_WEB) {
NativeReanimatedModule.configureProps(uiProps, nativeProps);
}
}

View File

@@ -0,0 +1,59 @@
'use strict';
import type { WorkletStackDetails } from './commonTypes';
const _workletStackDetails = new Map<number, WorkletStackDetails>();
export function registerWorkletStackDetails(
hash: number,
stackDetails: WorkletStackDetails
) {
_workletStackDetails.set(hash, stackDetails);
}
function getBundleOffset(error: Error): [string, number, number] {
const frame = error.stack?.split('\n')?.[0];
if (frame) {
const parsedFrame = /@([^@]+):(\d+):(\d+)/.exec(frame);
if (parsedFrame) {
const [, file, line, col] = parsedFrame;
return [file, Number(line), Number(col)];
}
}
return ['unknown', 0, 0];
}
function processStack(stack: string): string {
const workletStackEntries = stack.match(/worklet_(\d+):(\d+):(\d+)/g);
let result = stack;
workletStackEntries?.forEach((match) => {
const [, hash, origLine, origCol] = match.split(/:|_/).map(Number);
const errorDetails = _workletStackDetails.get(hash);
if (!errorDetails) {
return;
}
const [error, lineOffset, colOffset] = errorDetails;
const [bundleFile, bundleLine, bundleCol] = getBundleOffset(error);
const line = origLine + bundleLine + lineOffset;
const col = origCol + bundleCol + colOffset;
result = result.replace(match, `${bundleFile}:${line}:${col}`);
});
return result;
}
export function reportFatalErrorOnJS({
message,
stack,
}: {
message: string;
stack?: string;
}) {
const error = new Error();
error.message = message;
error.stack = stack ? processStack(stack) : undefined;
error.name = 'ReanimatedError';
// @ts-ignore React Native's ErrorUtils implementation extends the Error type with jsEngine field
error.jsEngine = 'reanimated';
// @ts-ignore the reportFatalError method is an internal method of ErrorUtils not exposed in the type definitions
global.ErrorUtils.reportFatalError(error);
}

View File

@@ -0,0 +1,58 @@
'use strict';
/* eslint-disable */
import type { ShadowNodeWrapper } from './commonTypes';
let findHostInstance_DEPRECATED: (ref: unknown) => void;
let getInternalInstanceHandleFromPublicInstance: (ref: unknown) => {
stateNode: { node: unknown };
};
export function getShadowNodeWrapperFromRef(
ref: React.Component
): ShadowNodeWrapper {
// load findHostInstance_DEPRECATED lazily because it may not be available before render
if (findHostInstance_DEPRECATED === undefined) {
try {
findHostInstance_DEPRECATED =
require('react-native/Libraries/Renderer/shims/ReactFabric').findHostInstance_DEPRECATED;
} catch (e) {
findHostInstance_DEPRECATED = (_ref: unknown) => null;
}
}
if (getInternalInstanceHandleFromPublicInstance === undefined) {
try {
getInternalInstanceHandleFromPublicInstance =
require('react-native/Libraries/ReactNative/ReactFabricPublicInstance/ReactFabricPublicInstance')
.getInternalInstanceHandleFromPublicInstance ??
((_ref: any) => _ref._internalInstanceHandle);
} catch (e) {
getInternalInstanceHandleFromPublicInstance = (_ref: any) =>
_ref._internalInstanceHandle;
}
}
// taken from https://github.com/facebook/react-native/commit/803bb16531697233686efd475f004c1643e03617#diff-d8172256c6d63b5d32db10e54d7b10f37a26b337d5280d89f5bfd7bcea778292R196
// @ts-ignore some weird stuff on RN 0.74 - see examples with scrollView
const scrollViewRef = ref?.getScrollResponder?.()?.getNativeScrollRef?.();
// @ts-ignore some weird stuff on RN 0.74 - see examples with scrollView
const otherScrollViewRef = ref?.getNativeScrollRef?.();
// @ts-ignore some weird stuff on RN 0.74 - see setNativeProps example
const textInputRef = ref?.__internalInstanceHandle?.stateNode?.node;
let resolvedRef;
if (scrollViewRef) {
resolvedRef = scrollViewRef.__internalInstanceHandle.stateNode.node;
} else if (otherScrollViewRef) {
resolvedRef = otherScrollViewRef.__internalInstanceHandle.stateNode.node;
} else if (textInputRef) {
resolvedRef = textInputRef;
} else {
resolvedRef = getInternalInstanceHandleFromPublicInstance(
findHostInstance_DEPRECATED(ref)
).stateNode.node;
}
return resolvedRef;
}

View File

@@ -0,0 +1,6 @@
'use strict';
export function getShadowNodeWrapperFromRef() {
throw new Error(
'[Reanimated] Trying to call `getShadowNodeWrapperFromRef` on web.'
);
}

View File

@@ -0,0 +1,39 @@
'use strict';
import { runOnUI } from '../core';
import type { FrameInfo } from './FrameCallbackRegistryUI';
import { prepareUIRegistry } from './FrameCallbackRegistryUI';
export default class FrameCallbackRegistryJS {
private nextCallbackId = 0;
constructor() {
prepareUIRegistry();
}
registerFrameCallback(callback: (frameInfo: FrameInfo) => void): number {
if (!callback) {
return -1;
}
const callbackId = this.nextCallbackId;
this.nextCallbackId++;
runOnUI(() => {
global._frameCallbackRegistry.registerFrameCallback(callback, callbackId);
})();
return callbackId;
}
unregisterFrameCallback(callbackId: number): void {
runOnUI(() => {
global._frameCallbackRegistry.unregisterFrameCallback(callbackId);
})();
}
manageStateFrameCallback(callbackId: number, state: boolean): void {
runOnUI(() => {
global._frameCallbackRegistry.manageStateFrameCallback(callbackId, state);
})();
}
}

View File

@@ -0,0 +1,124 @@
'use strict';
import { runOnUIImmediately } from '../threads';
type CallbackDetails = {
callback: (frameInfo: FrameInfo) => void;
startTime: number | null;
};
export type FrameInfo = {
timestamp: number;
timeSincePreviousFrame: number | null;
timeSinceFirstFrame: number;
};
export interface FrameCallbackRegistryUI {
frameCallbackRegistry: Map<number, CallbackDetails>;
activeFrameCallbacks: Set<number>;
previousFrameTimestamp: number | null;
runCallbacks: (callId: number) => void;
nextCallId: number;
registerFrameCallback: (
callback: (frameInfo: FrameInfo) => void,
callbackId: number
) => void;
unregisterFrameCallback: (callbackId: number) => void;
manageStateFrameCallback: (callbackId: number, state: boolean) => void;
}
export const prepareUIRegistry = runOnUIImmediately(() => {
'worklet';
const frameCallbackRegistry: FrameCallbackRegistryUI = {
frameCallbackRegistry: new Map<number, CallbackDetails>(),
activeFrameCallbacks: new Set<number>(),
previousFrameTimestamp: null,
nextCallId: 0,
runCallbacks(callId) {
const loop = (timestamp: number) => {
if (callId !== this.nextCallId) {
return;
}
if (this.previousFrameTimestamp === null) {
this.previousFrameTimestamp = timestamp;
}
const delta = timestamp - this.previousFrameTimestamp;
this.activeFrameCallbacks.forEach((callbackId: number) => {
const callbackDetails = this.frameCallbackRegistry.get(callbackId)!;
const { startTime } = callbackDetails;
if (startTime === null) {
// First frame
callbackDetails.startTime = timestamp;
callbackDetails.callback({
timestamp,
timeSincePreviousFrame: null,
timeSinceFirstFrame: 0,
});
} else {
// Next frame
callbackDetails.callback({
timestamp,
timeSincePreviousFrame: delta,
timeSinceFirstFrame: timestamp - startTime,
});
}
});
if (this.activeFrameCallbacks.size > 0) {
this.previousFrameTimestamp = timestamp;
requestAnimationFrame(loop);
} else {
this.previousFrameTimestamp = null;
}
};
// runCallback() should only be called after registering a callback,
// so if there is only one active callback, then it means that there were
// zero previously and the loop isn't running yet.
if (this.activeFrameCallbacks.size === 1 && callId === this.nextCallId) {
requestAnimationFrame(loop);
}
},
registerFrameCallback(
callback: (frameInfo: FrameInfo) => void,
callbackId: number
) {
this.frameCallbackRegistry.set(callbackId, {
callback,
startTime: null,
});
},
unregisterFrameCallback(callbackId: number) {
this.manageStateFrameCallback(callbackId, false);
this.frameCallbackRegistry.delete(callbackId);
},
manageStateFrameCallback(callbackId: number, state: boolean) {
if (callbackId === -1) {
return;
}
if (state) {
this.activeFrameCallbacks.add(callbackId);
this.runCallbacks(this.nextCallId);
} else {
const callback = this.frameCallbackRegistry.get(callbackId)!;
callback.startTime = null;
this.activeFrameCallbacks.delete(callbackId);
if (this.activeFrameCallbacks.size === 0) {
this.nextCallId += 1;
}
}
},
};
global._frameCallbackRegistry = frameCallbackRegistry;
});

View File

@@ -0,0 +1,2 @@
'use strict';
export type { FrameInfo } from './FrameCallbackRegistryUI';

View File

@@ -0,0 +1,115 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-var */
'use strict';
import type {
StyleProps,
MeasuredDimensions,
MapperRegistry,
ShareableRef,
ShadowNodeWrapper,
FlatShareableRef,
} from './commonTypes';
import type { AnimatedStyle } from './helperTypes';
import type { FrameCallbackRegistryUI } from './frameCallback/FrameCallbackRegistryUI';
import type { NativeReanimatedModule } from './NativeReanimated/NativeReanimated';
import type { SensorContainer } from './SensorContainer';
import type { LayoutAnimationsManager } from './layoutReanimation/animationsManager';
import type { ProgressTransitionRegister } from './layoutReanimation/sharedTransitions';
import type { UpdatePropsManager } from './UpdateProps';
import type { callGuardDEV } from './initializers';
import type { WorkletRuntime } from './runtimes';
import type { RNScreensTurboModuleType } from './screenTransition/commonTypes';
declare global {
var _REANIMATED_IS_REDUCED_MOTION: boolean | undefined;
var _IS_FABRIC: boolean | undefined;
var _REANIMATED_VERSION_CPP: string | undefined;
var _REANIMATED_VERSION_JS: string | undefined;
var __reanimatedModuleProxy: NativeReanimatedModule | undefined;
var __callGuardDEV: typeof callGuardDEV | undefined;
var evalWithSourceMap:
| ((js: string, sourceURL: string, sourceMap: string) => any)
| undefined;
var evalWithSourceUrl: ((js: string, sourceURL: string) => any) | undefined;
var _log: (value: unknown) => void;
var _toString: (value: unknown) => string;
var _notifyAboutProgress: (
tag: number,
value: Record<string, unknown>,
isSharedTransition: boolean
) => void;
var _notifyAboutEnd: (tag: number, removeView: boolean) => void;
var _setGestureState: (handlerTag: number, newState: number) => void;
var _makeShareableClone: <T>(
value: T,
nativeStateSource?: object
) => FlatShareableRef<T>;
var _scheduleOnJS: (fun: (...args: A) => R, args?: A) => void;
var _scheduleOnRuntime: (
runtime: WorkletRuntime,
worklet: ShareableRef<() => void>
) => void;
var _updatePropsPaper:
| ((
operations: {
tag: number;
name: string | null;
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
updates: StyleProps | AnimatedStyle<any>;
}[]
) => void)
| undefined;
var _updatePropsFabric:
| ((
operations: {
shadowNodeWrapper: ShadowNodeWrapper;
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
updates: StyleProps | AnimatedStyle<any>;
}[]
) => void)
| undefined;
var _removeFromPropsRegistry: (viewTags: number[]) => void | undefined;
var _measurePaper:
| ((viewTag: number | null) => MeasuredDimensions)
| undefined;
var _measureFabric:
| ((shadowNodeWrapper: ShadowNodeWrapper | null) => MeasuredDimensions)
| undefined;
var _scrollToPaper:
| ((viewTag: number, x: number, y: number, animated: boolean) => void)
| undefined;
var _dispatchCommandPaper:
| ((viewTag: number, commandName: string, args: Array<unknown>) => void)
| undefined;
var _dispatchCommandFabric:
| ((
shadowNodeWrapper: ShadowNodeWrapper,
commandName: string,
args: Array<unknown>
) => void)
| undefined;
var _getAnimationTimestamp: () => number;
var __ErrorUtils: {
reportFatalError: (error: Error) => void;
};
var _frameCallbackRegistry: FrameCallbackRegistryUI;
var console: Console;
var __frameTimestamp: number | undefined;
var __flushAnimationFrame: (timestamp: number) => void;
var __workletsCache: Map<string, any>;
var __handleCache: WeakMap<object, any>;
var __callMicrotasks: () => void;
var __mapperRegistry: MapperRegistry;
var __sensorContainer: SensorContainer;
var _maybeFlushUIUpdatesQueue: () => void;
var LayoutAnimationsManager: LayoutAnimationsManager;
var UpdatePropsManager: UpdatePropsManager;
var ProgressTransitionRegister: ProgressTransitionRegister;
var updateJSProps: (viewTag: number, props: Record<string, unknown>) => void;
var RNScreensTurboModule: RNScreensTurboModuleType | undefined;
var _obtainPropPaper: (viewTag: number, propName: string) => string;
var _obtainPropFabric: (
shadowNodeWrapper: ShadowNodeWrapper,
propName: string
) => string;
}

View File

@@ -0,0 +1,206 @@
'use strict';
/*
This file is a legacy remainder of manual types from react-native-reanimated.d.ts file.
I wasn't able to get rid of all of them from the code.
They should be treated as a temporary solution
until time comes to refactor the code and get necessary types right.
This will not be easy though!
*/
import type {
ImageStyle,
RegisteredStyle,
StyleProp,
TextStyle,
TransformsStyle,
ViewStyle,
} from 'react-native';
import type { AnimatableValue, SharedValue } from './commonTypes';
import type { BaseAnimationBuilder } from './layoutReanimation/animationBuilder/BaseAnimationBuilder';
import type {
EntryExitAnimationFunction,
LayoutAnimationFunction,
} from './layoutReanimation/animationBuilder/commonTypes';
import type { ReanimatedKeyframe } from './layoutReanimation/animationBuilder/Keyframe';
import type { SharedTransition } from './layoutReanimation/sharedTransitions';
export type TransformArrayItem = Extract<
TransformsStyle['transform'],
Array<unknown>
>[number];
export type AnimatedTransform = MaybeSharedValueRecursive<
TransformsStyle['transform']
>;
type MaybeSharedValue<Value> =
| Value
| (Value extends AnimatableValue ? SharedValue<Value> : never);
type MaybeSharedValueRecursive<Value> = Value extends (infer Item)[]
? SharedValue<Item[]> | (MaybeSharedValueRecursive<Item> | Item)[]
: Value extends object
?
| SharedValue<Value>
| {
[Key in keyof Value]:
| MaybeSharedValueRecursive<Value[Key]>
| Value[Key];
}
: MaybeSharedValue<Value>;
type DefaultStyle = ViewStyle & ImageStyle & TextStyle;
// Ideally we want AnimatedStyle to not be generic, but there are
// so many depenedencies on it being generic that it's not feasible at the moment.
export type AnimatedStyle<Style = DefaultStyle> =
| Style
| MaybeSharedValueRecursive<Style>;
type EntryOrExitLayoutType =
| BaseAnimationBuilder
| typeof BaseAnimationBuilder
| EntryExitAnimationFunction
| ReanimatedKeyframe;
/*
Style type properties (properties that extends StyleProp<ViewStyle>)
can be defined with other property names than "style". For example `contentContainerStyle` in FlatList.
Type definition for all style type properties should act similarly, hence we
pick keys with 'Style' substring with the use of this utility type.
*/
type PickStyleProps<Props> = Pick<
Props,
{
[Key in keyof Props]-?: Key extends `${string}Style` | 'style'
? Key
: never;
}[keyof Props]
>;
type AnimatedStyleProps<Props extends object> = {
[Key in keyof PickStyleProps<Props>]: StyleProp<AnimatedStyle<Props[Key]>>;
};
/**
* Component props that are not specially handled by us.
*/
type RestProps<Props extends object> = {
[K in keyof Omit<Props, keyof PickStyleProps<Props> | 'style'>]:
| Props[K]
| SharedValue<Props[K]>;
};
type LayoutProps = {
/**
* Lets you animate the layout changes when components are added to or removed from the view hierarchy.
*
* You can use the predefined layout transitions (eg. `LinearTransition`, `FadingTransition`) or create your own ones.
*
* @see https://docs.swmansion.com/react-native-reanimated/docs/layout-animations/layout-transitions
*/
layout?:
| BaseAnimationBuilder
| LayoutAnimationFunction
| typeof BaseAnimationBuilder;
/**
* Lets you animate an element when it's added to or removed from the view hierarchy.
*
* You can use the predefined entering animations (eg. `FadeIn`, `SlideInLeft`) or create your own ones.
*
* @see https://docs.swmansion.com/react-native-reanimated/docs/layout-animations/entering-exiting-animations
*/
entering?: EntryOrExitLayoutType;
/**
* Lets you animate an element when it's added to or removed from the view hierarchy.
*
* You can use the predefined entering animations (eg. `FadeOut`, `SlideOutRight`) or create your own ones.
*
* @see https://docs.swmansion.com/react-native-reanimated/docs/layout-animations/entering-exiting-animations
*/
exiting?: EntryOrExitLayoutType;
};
type SharedTransitionProps = {
/**
* Lets you animate components between two navigation screens.
*
* Assign the same `sharedTransitionTag` to [animated components](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/glossary#animated-component) on two different navigation screens to create a shared transition.
*
* @see https://docs.swmansion.com/react-native-reanimated/docs/shared-element-transitions/overview
* @experimental
*/
sharedTransitionTag?: string;
/**
* Lets you create a custom shared transition animation.
*
* Used alongside `SharedTransition.custom()` method.
*
* @see https://docs.swmansion.com/react-native-reanimated/docs/shared-element-transitions/overview
* @experimental
*/
sharedTransitionStyle?: SharedTransition;
};
type AnimatedPropsProp<Props extends object> = RestProps<Props> &
AnimatedStyleProps<Props> &
LayoutProps &
SharedTransitionProps;
export type AnimatedProps<Props extends object> = RestProps<Props> &
AnimatedStyleProps<Props> &
LayoutProps &
SharedTransitionProps & {
/**
* Lets you animate component props.
*
* @see https://docs.swmansion.com/react-native-reanimated/docs/core/useAnimatedProps
*/
animatedProps?: Partial<AnimatedPropsProp<Props>>;
};
// THE LAND OF THE DEPRECATED
/**
* @deprecated This type is no longer relevant.
*/
export type Adaptable<T> =
| T
| ReadonlyArray<T | ReadonlyArray<T>>
| SharedValue<T>;
/**
* @deprecated This type is no longer relevant.
*/
export type AdaptTransforms<T> = {
[P in keyof T]: Adaptable<T[P]>;
};
/**
* @deprecated Please use {@link TransformArrayItem} type instead.
*/
export type TransformStyleTypes = TransformArrayItem;
/**
* @deprecated Please use {@link AnimatedStyle} type instead.
*/
export type AnimateStyle<Style = DefaultStyle> = AnimatedStyle<Style>;
/**
* @deprecated This type is no longer relevant.
*/
export type StylesOrDefault<T> = 'style' extends keyof T
? MaybeSharedValueRecursive<T['style']>
: Record<string, unknown>;
/**
* @deprecated This type is no longer relevant.
*/
export type AnimatedStyleProp<T> =
| AnimatedStyle<T>
| RegisteredStyle<AnimatedStyle<T>>;
/**
* @deprecated Please use {@link AnimatedProps} type instead.
*/
export type AnimateProps<Props extends object> = AnimatedProps<Props>;

View File

@@ -0,0 +1,119 @@
'use strict';
import type { Component, MutableRefObject } from 'react';
import type {
AnimatedPropsAdapterFunction,
ShadowNodeWrapper,
SharedValue,
WorkletFunction,
} from '../commonTypes';
import type {
ImageStyle,
NativeSyntheticEvent,
TextStyle,
ViewStyle,
NativeScrollEvent,
} from 'react-native';
import type { ViewDescriptorsSet, ViewRefSet } from '../ViewDescriptorsSet';
import type { AnimatedStyle } from '../helperTypes';
export type DependencyList = Array<unknown> | undefined;
export interface Descriptor {
tag: number;
name: string;
shadowNodeWrapper: ShadowNodeWrapper;
}
export interface AnimatedRef<T extends Component> {
(component?: T):
| number // Paper
| ShadowNodeWrapper // Fabric
| HTMLElement; // web
current: T | null;
getTag: () => number;
}
// Might make that type generic if it's ever needed.
export type AnimatedRefOnJS = AnimatedRef<Component>;
/**
* `AnimatedRef` is mapped to this type on the UI thread via a shareable handle.
*/
export type AnimatedRefOnUI = {
(): number | ShadowNodeWrapper | null;
/**
* @remarks `viewName` is required only on iOS with Paper and it's value is null on other platforms.
*/
viewName: SharedValue<string | null>;
};
type ReanimatedPayload = {
eventName: string;
};
/**
* This utility type is to convert type of events that would normally be
* sent by React Native (they have `nativeEvent` field) to the type
* that is sent by Reanimated.
*/
export type ReanimatedEvent<Event extends object> = ReanimatedPayload &
(Event extends {
nativeEvent: infer NativeEvent extends object;
}
? NativeEvent
: Event);
export type EventPayload<Event extends object> = Event extends {
nativeEvent: infer NativeEvent extends object;
}
? NativeEvent
: Omit<Event, 'eventName'>;
export type NativeEventWrapper<Event extends object> = {
nativeEvent: Event;
};
export type DefaultStyle = ViewStyle | ImageStyle | TextStyle;
export type RNNativeScrollEvent = NativeSyntheticEvent<NativeScrollEvent>;
export type ReanimatedScrollEvent = ReanimatedEvent<RNNativeScrollEvent>;
export interface IWorkletEventHandler<Event extends object> {
updateEventHandler: (
newWorklet: (event: ReanimatedEvent<Event>) => void,
newEvents: string[]
) => void;
registerForEvents: (viewTag: number, fallbackEventName?: string) => void;
unregisterFromEvents: (viewTag: number) => void;
}
export interface AnimatedStyleHandle<
Style extends DefaultStyle = DefaultStyle
> {
viewDescriptors: ViewDescriptorsSet;
initial: {
value: AnimatedStyle<Style>;
updater: () => AnimatedStyle<Style>;
};
/**
* @remarks `viewsRef` is only defined in Web implementation.
*/
viewsRef: ViewRefSet<unknown> | undefined;
}
export interface JestAnimatedStyleHandle<
Style extends DefaultStyle = DefaultStyle
> extends AnimatedStyleHandle<Style> {
jestAnimatedStyle: MutableRefObject<AnimatedStyle<Style>>;
}
export type UseAnimatedStyleInternal<Style extends DefaultStyle> = (
updater: WorkletFunction<[], Style> | (() => Style),
dependencies?: DependencyList | null,
adapters?:
| AnimatedPropsAdapterFunction
| AnimatedPropsAdapterFunction[]
| null,
isAnimatedProps?: boolean
) => AnimatedStyleHandle<Style> | JestAnimatedStyleHandle<Style>;

View File

@@ -0,0 +1,41 @@
'use strict';
export type {
DependencyList,
AnimatedRef,
ReanimatedScrollEvent as ScrollEvent,
ReanimatedEvent,
} from './commonTypes';
export { useAnimatedProps } from './useAnimatedProps';
export { useWorkletCallback } from './useWorkletCallback';
export { useSharedValue } from './useSharedValue';
export { useReducedMotion } from './useReducedMotion';
export { useAnimatedStyle } from './useAnimatedStyle';
export { useAnimatedGestureHandler } from './useAnimatedGestureHandler';
export type {
GestureHandlerEvent,
GestureHandlers,
} from './useAnimatedGestureHandler';
export { useAnimatedReaction } from './useAnimatedReaction';
export { useAnimatedRef } from './useAnimatedRef';
export { useAnimatedScrollHandler } from './useAnimatedScrollHandler';
export type {
ScrollHandler,
ScrollHandlers,
ScrollHandlerProcessed,
ScrollHandlerInternal,
} from './useAnimatedScrollHandler';
export { useDerivedValue } from './useDerivedValue';
export type { DerivedValue } from './useDerivedValue';
export { useAnimatedSensor } from './useAnimatedSensor';
export { useFrameCallback } from './useFrameCallback';
export type { FrameCallback } from './useFrameCallback';
export { useAnimatedKeyboard } from './useAnimatedKeyboard';
export { useScrollViewOffset } from './useScrollViewOffset';
export type {
EventHandler,
EventHandlerProcessed,
EventHandlerInternal,
} from './useEvent';
export { useEvent } from './useEvent';
export type { UseHandlerContext } from './useHandler';
export { useHandler } from './useHandler';

View File

@@ -0,0 +1,151 @@
'use strict';
import type {
DependencyList,
NativeEventWrapper,
ReanimatedEvent,
} from './commonTypes';
import { useHandler } from './useHandler';
import { useEvent } from './useEvent';
const EVENT_TYPE = {
UNDETERMINED: 0,
FAILED: 1,
BEGAN: 2,
CANCELLED: 3,
ACTIVE: 4,
END: 5,
} as const;
type StateType = (typeof EVENT_TYPE)[keyof typeof EVENT_TYPE];
// This type comes from React Native Gesture Handler
// import type { PanGestureHandlerGestureEvent as DefaultEvent } from 'react-native-gesture-handler';
type DefaultEvent = {
nativeEvent: {
readonly handlerTag: number;
readonly numberOfPointers: number;
readonly state: (typeof EVENT_TYPE)[keyof typeof EVENT_TYPE];
readonly x: number;
readonly y: number;
readonly absoluteX: number;
readonly absoluteY: number;
readonly translationX: number;
readonly translationY: number;
readonly velocityX: number;
readonly velocityY: number;
};
};
interface PropsUsedInUseAnimatedGestureHandler {
handlerTag?: number;
numberOfPointers?: number;
state?: StateType;
oldState?: StateType;
}
export type GestureHandlerEvent<Event extends object> =
| ReanimatedEvent<Event>
| Event;
type GestureHandler<
Event extends NativeEventWrapper<PropsUsedInUseAnimatedGestureHandler>,
Context extends Record<string, unknown>
> = (
eventPayload: ReanimatedEvent<Event>,
context: Context,
isCanceledOrFailed?: boolean
) => void;
export interface GestureHandlers<
Event extends NativeEventWrapper<PropsUsedInUseAnimatedGestureHandler>,
Context extends Record<string, unknown>
> {
[key: string]: GestureHandler<Event, Context> | undefined;
onStart?: GestureHandler<Event, Context>;
onActive?: GestureHandler<Event, Context>;
onEnd?: GestureHandler<Event, Context>;
onFail?: GestureHandler<Event, Context>;
onCancel?: GestureHandler<Event, Context>;
onFinish?: GestureHandler<Event, Context>;
}
/**
* @deprecated useAnimatedGestureHandler is an old API which is no longer supported.
*
* Please check https://docs.swmansion.com/react-native-gesture-handler/docs/guides/upgrading-to-2/
* for information about how to migrate to `react-native-gesture-handler` v2
*/
export function useAnimatedGestureHandler<
Event extends NativeEventWrapper<PropsUsedInUseAnimatedGestureHandler> = DefaultEvent,
Context extends Record<string, unknown> = Record<string, unknown>
>(handlers: GestureHandlers<Event, Context>, dependencies?: DependencyList) {
type WebOrNativeEvent = Event | ReanimatedEvent<Event>;
const { context, doDependenciesDiffer, useWeb } = useHandler<Event, Context>(
handlers,
dependencies
);
const handler = (e: WebOrNativeEvent) => {
'worklet';
const event = useWeb
? // On Web we get events straight from React Native and they don't have
// the `eventName` field there. To simplify the types here we just
// cast it as the field was available.
((e as Event).nativeEvent as ReanimatedEvent<Event>)
: (e as ReanimatedEvent<Event>);
if (event.state === EVENT_TYPE.BEGAN && handlers.onStart) {
handlers.onStart(event, context);
}
if (event.state === EVENT_TYPE.ACTIVE && handlers.onActive) {
handlers.onActive(event, context);
}
if (
event.oldState === EVENT_TYPE.ACTIVE &&
event.state === EVENT_TYPE.END &&
handlers.onEnd
) {
handlers.onEnd(event, context);
}
if (
event.oldState === EVENT_TYPE.BEGAN &&
event.state === EVENT_TYPE.FAILED &&
handlers.onFail
) {
handlers.onFail(event, context);
}
if (
event.oldState === EVENT_TYPE.ACTIVE &&
event.state === EVENT_TYPE.CANCELLED &&
handlers.onCancel
) {
handlers.onCancel(event, context);
}
if (
(event.oldState === EVENT_TYPE.BEGAN ||
event.oldState === EVENT_TYPE.ACTIVE) &&
event.state !== EVENT_TYPE.BEGAN &&
event.state !== EVENT_TYPE.ACTIVE &&
handlers.onFinish
) {
handlers.onFinish(
event,
context,
event.state === EVENT_TYPE.CANCELLED ||
event.state === EVENT_TYPE.FAILED
);
}
};
if (useWeb) {
return handler;
}
// eslint-disable-next-line react-hooks/rules-of-hooks
return useEvent<Event>(
handler,
['onGestureHandlerStateChange', 'onGestureHandlerEvent'],
doDependenciesDiffer
// This is not correct but we want to make GH think it receives a function.
) as unknown as (e: Event) => void;
}

View File

@@ -0,0 +1,58 @@
'use strict';
import { useEffect, useRef } from 'react';
import {
makeMutable,
subscribeForKeyboardEvents,
unsubscribeFromKeyboardEvents,
} from '../core';
import type {
AnimatedKeyboardInfo,
AnimatedKeyboardOptions,
} from '../commonTypes';
import { KeyboardState } from '../commonTypes';
/**
* Lets you synchronously get the position and state of the keyboard.
*
* @param options - An additional keyboard configuration options.
* @returns An object with the current keyboard `height` and `state` as [shared values](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/glossary#shared-value).
* @see https://docs.swmansion.com/react-native-reanimated/docs/device/useAnimatedKeyboard
*/
export function useAnimatedKeyboard(
options: AnimatedKeyboardOptions = { isStatusBarTranslucentAndroid: false }
): AnimatedKeyboardInfo {
const ref = useRef<AnimatedKeyboardInfo | null>(null);
const listenerId = useRef<number>(-1);
const isSubscribed = useRef<boolean>(false);
if (ref.current === null) {
const keyboardEventData: AnimatedKeyboardInfo = {
state: makeMutable<KeyboardState>(KeyboardState.UNKNOWN),
height: makeMutable(0),
};
listenerId.current = subscribeForKeyboardEvents((state, height) => {
'worklet';
keyboardEventData.state.value = state;
keyboardEventData.height.value = height;
}, options);
ref.current = keyboardEventData;
isSubscribed.current = true;
}
useEffect(() => {
if (isSubscribed.current === false && ref.current !== null) {
const keyboardEventData = ref.current;
// subscribe again after Fast Refresh
listenerId.current = subscribeForKeyboardEvents((state, height) => {
'worklet';
keyboardEventData.state.value = state;
keyboardEventData.height.value = height;
}, options);
isSubscribed.current = true;
}
return () => {
unsubscribeFromKeyboardEvents(listenerId.current);
isSubscribed.current = false;
};
}, []);
return ref.current;
}

View File

@@ -0,0 +1,48 @@
'use strict';
import { useAnimatedStyle } from './useAnimatedStyle';
import type { DependencyList, UseAnimatedStyleInternal } from './commonTypes';
import { shouldBeUseWeb } from '../PlatformChecker';
import type { AnimatedPropsAdapterFunction } from '../commonTypes';
// TODO: we should make sure that when useAP is used we are not assigning styles
type UseAnimatedProps = <Props extends object>(
updater: () => Partial<Props>,
dependencies?: DependencyList | null,
adapters?:
| AnimatedPropsAdapterFunction
| AnimatedPropsAdapterFunction[]
| null,
isAnimatedProps?: boolean
) => Partial<Props>;
function useAnimatedPropsJS<Props extends object>(
updater: () => Props,
deps?: DependencyList | null,
adapters?:
| AnimatedPropsAdapterFunction
| AnimatedPropsAdapterFunction[]
| null
) {
return (useAnimatedStyle as UseAnimatedStyleInternal<Props>)(
updater,
deps,
adapters,
true
);
}
const useAnimatedPropsNative = useAnimatedStyle;
/**
* Lets you create an animated props object which can be animated using shared values.
*
* @param updater - A function returning an object with properties you want to animate.
* @param dependencies - An optional array of dependencies. Only relevant when using Reanimated without the Babel plugin on the Web.
* @param adapters - An optional function or array of functions allowing to adopt prop naming between JS and the native side.
* @returns An animated props object which has to be passed to `animatedProps` property of an Animated component that you want to animate.
* @see https://docs.swmansion.com/react-native-reanimated/docs/core/useAnimatedProps
*/
export const useAnimatedProps: UseAnimatedProps = shouldBeUseWeb()
? (useAnimatedPropsJS as UseAnimatedProps)
: useAnimatedPropsNative;

View File

@@ -0,0 +1,66 @@
'use strict';
import { useEffect } from 'react';
import type { WorkletFunction } from '../commonTypes';
import { startMapper, stopMapper } from '../core';
import type { DependencyList } from './commonTypes';
import { useSharedValue } from './useSharedValue';
import { shouldBeUseWeb } from '../PlatformChecker';
/**
* Lets you to respond to changes in a [shared value](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/glossary#shared-value). It's especially useful when comparing values previously stored in the shared value with the current one.
*
* @param prepare - A function that should return a value to which you'd like to react.
* @param react - A function that reacts to changes in the value returned by the `prepare` function.
* @param dependencies - an optional array of dependencies. Only relevant when using Reanimated without the Babel plugin on the Web.
* @see https://docs.swmansion.com/react-native-reanimated/docs/advanced/useAnimatedReaction
*/
// @ts-expect-error This overload is required by our API.
export function useAnimatedReaction<PreparedResult>(
prepare: () => PreparedResult,
react: (prepared: PreparedResult, previous: PreparedResult | null) => void,
dependencies?: DependencyList
): void;
export function useAnimatedReaction<PreparedResult>(
prepare: WorkletFunction<[], PreparedResult>,
react: WorkletFunction<
[prepare: PreparedResult, previous: PreparedResult | null],
void
>,
dependencies?: DependencyList
) {
const previous = useSharedValue<PreparedResult | null>(null);
let inputs = Object.values(prepare.__closure ?? {});
if (shouldBeUseWeb()) {
if (!inputs.length && dependencies?.length) {
// let web work without a Reanimated Babel plugin
inputs = dependencies;
}
}
if (dependencies === undefined) {
dependencies = [
...Object.values(prepare.__closure ?? {}),
...Object.values(react.__closure ?? {}),
prepare.__workletHash,
react.__workletHash,
];
} else {
dependencies.push(prepare.__workletHash, react.__workletHash);
}
useEffect(() => {
const fun = () => {
'worklet';
const input = prepare();
react(input, previous.value);
previous.value = input;
};
const mapperId = startMapper(fun, inputs);
return () => {
stopMapper(mapperId);
};
}, dependencies);
}

View File

@@ -0,0 +1,98 @@
'use strict';
import type { Component } from 'react';
import { useRef } from 'react';
import { useSharedValue } from './useSharedValue';
import type { AnimatedRef, AnimatedRefOnUI } from './commonTypes';
import type { ShadowNodeWrapper } from '../commonTypes';
import { getShadowNodeWrapperFromRef } from '../fabricUtils';
import { makeShareableCloneRecursive } from '../shareables';
import { shareableMappingCache } from '../shareableMappingCache';
import { Platform, findNodeHandle } from 'react-native';
import type { ScrollView, FlatList } from 'react-native';
import { isFabric, isWeb } from '../PlatformChecker';
const IS_WEB = isWeb();
interface MaybeScrollableComponent extends Component {
getNativeScrollRef?: FlatList['getNativeScrollRef'];
getScrollableNode?:
| ScrollView['getScrollableNode']
| FlatList['getScrollableNode'];
viewConfig?: {
uiViewClassName?: string;
};
}
function getComponentOrScrollable(component: MaybeScrollableComponent) {
if (isFabric() && component.getNativeScrollRef) {
return component.getNativeScrollRef();
} else if (!isFabric() && component.getScrollableNode) {
return component.getScrollableNode();
}
return component;
}
/**
* Lets you get a reference of a view that you can use inside a worklet.
*
* @returns An object with a `.current` property which contains an instance of a component.
* @see https://docs.swmansion.com/react-native-reanimated/docs/core/useAnimatedRef
*/
export function useAnimatedRef<
TComponent extends Component
>(): AnimatedRef<TComponent> {
const tag = useSharedValue<number | ShadowNodeWrapper | null>(-1);
const viewName = useSharedValue<string | null>(null);
const ref = useRef<AnimatedRef<TComponent>>();
if (!ref.current) {
const fun: AnimatedRef<TComponent> = <AnimatedRef<TComponent>>((
component
) => {
// enters when ref is set by attaching to a component
if (component) {
const getTagValueFunction = isFabric()
? getShadowNodeWrapperFromRef
: findNodeHandle;
const getTagOrShadowNodeWrapper = () => {
return IS_WEB
? getComponentOrScrollable(component)
: getTagValueFunction(getComponentOrScrollable(component));
};
tag.value = getTagOrShadowNodeWrapper();
// On Fabric we have to unwrap the tag from the shadow node wrapper
fun.getTag = isFabric()
? () => findNodeHandle(getComponentOrScrollable(component))
: getTagOrShadowNodeWrapper;
fun.current = component;
// viewName is required only on iOS with Paper
if (Platform.OS === 'ios' && !isFabric()) {
viewName.value =
(component as MaybeScrollableComponent)?.viewConfig
?.uiViewClassName || 'RCTView';
}
}
return tag.value;
});
fun.current = null;
const animatedRefShareableHandle = makeShareableCloneRecursive({
__init: () => {
'worklet';
const f: AnimatedRefOnUI = () => tag.value;
f.viewName = viewName;
return f;
},
});
shareableMappingCache.set(fun, animatedRefShareableHandle);
ref.current = fun;
}
return ref.current;
}

View File

@@ -0,0 +1,108 @@
'use strict';
import type {
DependencyList,
RNNativeScrollEvent,
ReanimatedScrollEvent,
} from './commonTypes';
import { useHandler } from './useHandler';
import type { EventHandlerInternal, EventHandlerProcessed } from './useEvent';
import { useEvent } from './useEvent';
export type ScrollHandler<
Context extends Record<string, unknown> = Record<string, unknown>
> = (event: ReanimatedScrollEvent, context: Context) => void;
export interface ScrollHandlers<Context extends Record<string, unknown>> {
onScroll?: ScrollHandler<Context>;
onBeginDrag?: ScrollHandler<Context>;
onEndDrag?: ScrollHandler<Context>;
onMomentumBegin?: ScrollHandler<Context>;
onMomentumEnd?: ScrollHandler<Context>;
}
export type ScrollHandlerProcessed<
Context extends Record<string, unknown> = Record<string, unknown>
> = EventHandlerProcessed<RNNativeScrollEvent, Context>;
export type ScrollHandlerInternal = EventHandlerInternal<RNNativeScrollEvent>;
/**
* Lets you run callbacks on ScrollView events. Supports `onScroll`, `onBeginDrag`, `onEndDrag`, `onMomentumBegin`, and `onMomentumEnd` events.
*
* These callbacks are automatically workletized and ran on the UI thread.
*
* @param handlers - An object containing event handlers.
* @param dependencies - An optional array of dependencies. Only relevant when using Reanimated without the Babel plugin on the Web.
* @returns An object you need to pass to `onScroll` prop on the `Animated.ScrollView` component.
* @see https://docs.swmansion.com/react-native-reanimated/docs/scroll/useAnimatedScrollHandler
*/
// @ts-expect-error This overload is required by our API.
export function useAnimatedScrollHandler<
Context extends Record<string, unknown>
>(
handlers: ScrollHandler<Context> | ScrollHandlers<Context>,
dependencies?: DependencyList
): ScrollHandlerProcessed<Context>;
export function useAnimatedScrollHandler<
Context extends Record<string, unknown>
>(
handlers: ScrollHandlers<Context> | ScrollHandler<Context>,
dependencies?: DependencyList
) {
// case when handlers is a function
const scrollHandlers: ScrollHandlers<Context> =
typeof handlers === 'function' ? { onScroll: handlers } : handlers;
const { context, doDependenciesDiffer } = useHandler<
RNNativeScrollEvent,
Context
>(scrollHandlers as Record<string, ScrollHandler<Context>>, dependencies);
// build event subscription array
const subscribeForEvents = ['onScroll'];
if (scrollHandlers.onBeginDrag !== undefined) {
subscribeForEvents.push('onScrollBeginDrag');
}
if (scrollHandlers.onEndDrag !== undefined) {
subscribeForEvents.push('onScrollEndDrag');
}
if (scrollHandlers.onMomentumBegin !== undefined) {
subscribeForEvents.push('onMomentumScrollBegin');
}
if (scrollHandlers.onMomentumEnd !== undefined) {
subscribeForEvents.push('onMomentumScrollEnd');
}
return useEvent<RNNativeScrollEvent, Context>(
(event: ReanimatedScrollEvent) => {
'worklet';
const {
onScroll,
onBeginDrag,
onEndDrag,
onMomentumBegin,
onMomentumEnd,
} = scrollHandlers;
if (onScroll && event.eventName.endsWith('onScroll')) {
onScroll(event, context);
} else if (onBeginDrag && event.eventName.endsWith('onScrollBeginDrag')) {
onBeginDrag(event, context);
} else if (onEndDrag && event.eventName.endsWith('onScrollEndDrag')) {
onEndDrag(event, context);
} else if (
onMomentumBegin &&
event.eventName.endsWith('onMomentumScrollBegin')
) {
onMomentumBegin(event, context);
} else if (
onMomentumEnd &&
event.eventName.endsWith('onMomentumScrollEnd')
) {
onMomentumEnd(event, context);
}
},
subscribeForEvents,
doDependenciesDiffer
// Read https://github.com/software-mansion/react-native-reanimated/pull/5056
// for more information about this cast.
) as unknown as ScrollHandlerInternal;
}

View File

@@ -0,0 +1,173 @@
'use strict';
import { useEffect, useMemo, useRef } from 'react';
import { initializeSensor, registerSensor, unregisterSensor } from '../core';
import type {
SensorConfig,
AnimatedSensor,
Value3D,
ValueRotation,
} from '../commonTypes';
import {
SensorType,
IOSReferenceFrame,
InterfaceOrientation,
} from '../commonTypes';
import { callMicrotasks } from '../threads';
// euler angles are in order ZXY, z = yaw, x = pitch, y = roll
// https://github.com/mrdoob/three.js/blob/dev/src/math/Quaternion.js#L237
function eulerToQuaternion(pitch: number, roll: number, yaw: number) {
'worklet';
const c1 = Math.cos(pitch / 2);
const s1 = Math.sin(pitch / 2);
const c2 = Math.cos(roll / 2);
const s2 = Math.sin(roll / 2);
const c3 = Math.cos(yaw / 2);
const s3 = Math.sin(yaw / 2);
return [
s1 * c2 * c3 - c1 * s2 * s3,
c1 * s2 * c3 + s1 * c2 * s3,
c1 * c2 * s3 + s1 * s2 * c3,
c1 * c2 * c3 - s1 * s2 * s3,
];
}
function adjustRotationToInterfaceOrientation(data: ValueRotation) {
'worklet';
const { interfaceOrientation, pitch, roll, yaw } = data;
if (interfaceOrientation === InterfaceOrientation.ROTATION_90) {
data.pitch = roll;
data.roll = -pitch;
data.yaw = yaw - Math.PI / 2;
} else if (interfaceOrientation === InterfaceOrientation.ROTATION_270) {
data.pitch = -roll;
data.roll = pitch;
data.yaw = yaw + Math.PI / 2;
} else if (interfaceOrientation === InterfaceOrientation.ROTATION_180) {
data.pitch *= -1;
data.roll *= -1;
data.yaw *= -1;
}
const q = eulerToQuaternion(data.pitch, data.roll, data.yaw);
data.qx = q[0];
data.qy = q[1];
data.qz = q[2];
data.qw = q[3];
return data;
}
function adjustVectorToInterfaceOrientation(data: Value3D) {
'worklet';
const { interfaceOrientation, x, y } = data;
if (interfaceOrientation === InterfaceOrientation.ROTATION_90) {
data.x = -y;
data.y = x;
} else if (interfaceOrientation === InterfaceOrientation.ROTATION_270) {
data.x = y;
data.y = -x;
} else if (interfaceOrientation === InterfaceOrientation.ROTATION_180) {
data.x *= -1;
data.y *= -1;
}
return data;
}
/**
* Lets you create animations based on data from the device's sensors.
*
* @param sensorType - Type of the sensor to use. Configured with {@link SensorType} enum.
* @param config - The sensor configuration - {@link SensorConfig}.
* @returns An object containing the sensor measurements [shared value](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/glossary#shared-value) and a function to unregister the sensor
* @see https://docs.swmansion.com/react-native-reanimated/docs/device/useAnimatedSensor
*/
export function useAnimatedSensor(
sensorType: SensorType.ROTATION,
userConfig?: Partial<SensorConfig>
): AnimatedSensor<ValueRotation>;
export function useAnimatedSensor(
sensorType: Exclude<SensorType, SensorType.ROTATION>,
userConfig?: Partial<SensorConfig>
): AnimatedSensor<Value3D>;
export function useAnimatedSensor(
sensorType: SensorType,
userConfig?: Partial<SensorConfig>
): AnimatedSensor<ValueRotation> | AnimatedSensor<Value3D> {
const userConfigRef = useRef(userConfig);
const hasConfigChanged =
userConfigRef.current?.adjustToInterfaceOrientation !==
userConfig?.adjustToInterfaceOrientation ||
userConfigRef.current?.interval !== userConfig?.interval ||
userConfigRef.current?.iosReferenceFrame !== userConfig?.iosReferenceFrame;
if (hasConfigChanged) {
userConfigRef.current = { ...userConfig };
}
const config: SensorConfig = useMemo(
() => ({
interval: 'auto',
adjustToInterfaceOrientation: true,
iosReferenceFrame: IOSReferenceFrame.Auto,
...userConfigRef.current,
}),
[userConfigRef.current]
);
const ref = useRef<AnimatedSensor<Value3D | ValueRotation>>({
sensor: initializeSensor(sensorType, config),
unregister: () => {
// NOOP
},
isAvailable: false,
config,
});
useEffect(() => {
ref.current = {
sensor: initializeSensor(sensorType, config),
unregister: () => {
// NOOP
},
isAvailable: false,
config,
};
const sensorData = ref.current.sensor;
const adjustToInterfaceOrientation =
ref.current.config.adjustToInterfaceOrientation;
const id = registerSensor(sensorType, config, (data) => {
'worklet';
if (adjustToInterfaceOrientation) {
if (sensorType === SensorType.ROTATION) {
data = adjustRotationToInterfaceOrientation(data as ValueRotation);
} else {
data = adjustVectorToInterfaceOrientation(data as Value3D);
}
}
sensorData.value = data;
callMicrotasks();
});
if (id !== -1) {
// if sensor is available
ref.current.unregister = () => unregisterSensor(id);
ref.current.isAvailable = true;
} else {
// if sensor is unavailable
ref.current.unregister = () => {
// NOOP
};
ref.current.isAvailable = false;
}
return () => {
ref.current.unregister();
};
}, [sensorType, config]);
return ref.current as AnimatedSensor<ValueRotation> | AnimatedSensor<Value3D>;
}

View File

@@ -0,0 +1,555 @@
'use strict';
import type { MutableRefObject } from 'react';
import { useEffect, useRef } from 'react';
import { makeShareable, startMapper, stopMapper } from '../core';
import updateProps, { updatePropsJestWrapper } from '../UpdateProps';
import { initialUpdaterRun } from '../animation';
import { useSharedValue } from './useSharedValue';
import {
buildWorkletsHash,
isAnimated,
shallowEqual,
validateAnimatedStyles,
} from './utils';
import type {
AnimatedStyleHandle,
DefaultStyle,
DependencyList,
Descriptor,
JestAnimatedStyleHandle,
} from './commonTypes';
import type { ViewDescriptorsSet, ViewRefSet } from '../ViewDescriptorsSet';
import { makeViewDescriptorsSet, useViewRefSet } from '../ViewDescriptorsSet';
import { isJest, shouldBeUseWeb } from '../PlatformChecker';
import type {
AnimationObject,
Timestamp,
NestedObjectValues,
SharedValue,
StyleProps,
WorkletFunction,
AnimatedPropsAdapterFunction,
AnimatedPropsAdapterWorklet,
} from '../commonTypes';
import type { AnimatedStyle } from '../helperTypes';
import { isWorkletFunction } from '../commonTypes';
const SHOULD_BE_USE_WEB = shouldBeUseWeb();
interface AnimatedState {
last: AnimatedStyle<any>;
animations: AnimatedStyle<any>;
isAnimationRunning: boolean;
isAnimationCancelled: boolean;
}
interface AnimatedUpdaterData {
initial: {
value: AnimatedStyle<any>;
updater: () => AnimatedStyle<any>;
};
remoteState: AnimatedState;
viewDescriptors: ViewDescriptorsSet;
}
function prepareAnimation(
frameTimestamp: number,
animatedProp: AnimatedStyle<any>,
lastAnimation: AnimatedStyle<any>,
lastValue: AnimatedStyle<any>
): void {
'worklet';
if (Array.isArray(animatedProp)) {
animatedProp.forEach((prop, index) => {
prepareAnimation(
frameTimestamp,
prop,
lastAnimation && lastAnimation[index],
lastValue && lastValue[index]
);
});
// return animatedProp;
}
if (typeof animatedProp === 'object' && animatedProp.onFrame) {
const animation = animatedProp;
let value = animation.current;
if (lastValue !== undefined && lastValue !== null) {
if (typeof lastValue === 'object') {
if (lastValue.value !== undefined) {
// previously it was a shared value
value = lastValue.value;
} else if (lastValue.onFrame !== undefined) {
if (lastAnimation?.current !== undefined) {
// it was an animation before, copy its state
value = lastAnimation.current;
} else if (lastValue?.current !== undefined) {
// it was initialized
value = lastValue.current;
}
}
} else {
// previously it was a plain value, just set it as starting point
value = lastValue;
}
}
animation.callStart = (timestamp: Timestamp) => {
animation.onStart(animation, value, timestamp, lastAnimation);
};
animation.callStart(frameTimestamp);
animation.callStart = null;
} else if (typeof animatedProp === 'object') {
// it is an object
Object.keys(animatedProp).forEach((key) =>
prepareAnimation(
frameTimestamp,
animatedProp[key],
lastAnimation && lastAnimation[key],
lastValue && lastValue[key]
)
);
}
}
function runAnimations(
animation: AnimatedStyle<any>,
timestamp: Timestamp,
key: number | string,
result: AnimatedStyle<any>,
animationsActive: SharedValue<boolean>
): boolean {
'worklet';
if (!animationsActive.value) {
return true;
}
if (Array.isArray(animation)) {
result[key] = [];
let allFinished = true;
animation.forEach((entry, index) => {
if (
!runAnimations(entry, timestamp, index, result[key], animationsActive)
) {
allFinished = false;
}
});
return allFinished;
} else if (typeof animation === 'object' && animation.onFrame) {
let finished = true;
if (!animation.finished) {
if (animation.callStart) {
animation.callStart(timestamp);
animation.callStart = null;
}
finished = animation.onFrame(animation, timestamp);
animation.timestamp = timestamp;
if (finished) {
animation.finished = true;
animation.callback && animation.callback(true /* finished */);
}
}
result[key] = animation.current;
return finished;
} else if (typeof animation === 'object') {
result[key] = {};
let allFinished = true;
Object.keys(animation).forEach((k) => {
if (
!runAnimations(
animation[k],
timestamp,
k,
result[key],
animationsActive
)
) {
allFinished = false;
}
});
return allFinished;
} else {
result[key] = animation;
return true;
}
}
function styleUpdater(
viewDescriptors: SharedValue<Descriptor[]>,
updater: WorkletFunction<[], AnimatedStyle<any>> | (() => AnimatedStyle<any>),
state: AnimatedState,
maybeViewRef: ViewRefSet<any> | undefined,
animationsActive: SharedValue<boolean>,
isAnimatedProps = false
): void {
'worklet';
const animations = state.animations ?? {};
const newValues = updater() ?? {};
const oldValues = state.last;
const nonAnimatedNewValues: StyleProps = {};
let hasAnimations = false;
let frameTimestamp: number | undefined;
let hasNonAnimatedValues = false;
for (const key in newValues) {
const value = newValues[key];
if (isAnimated(value)) {
frameTimestamp =
global.__frameTimestamp || global._getAnimationTimestamp();
prepareAnimation(frameTimestamp, value, animations[key], oldValues[key]);
animations[key] = value;
hasAnimations = true;
} else {
hasNonAnimatedValues = true;
nonAnimatedNewValues[key] = value;
delete animations[key];
}
}
if (hasAnimations) {
const frame = (timestamp: Timestamp) => {
// eslint-disable-next-line @typescript-eslint/no-shadow
const { animations, last, isAnimationCancelled } = state;
if (isAnimationCancelled) {
state.isAnimationRunning = false;
return;
}
const updates: AnimatedStyle<any> = {};
let allFinished = true;
for (const propName in animations) {
const finished = runAnimations(
animations[propName],
timestamp,
propName,
updates,
animationsActive
);
if (finished) {
last[propName] = updates[propName];
delete animations[propName];
} else {
allFinished = false;
}
}
if (updates) {
updateProps(viewDescriptors, updates, maybeViewRef);
}
if (!allFinished) {
requestAnimationFrame(frame);
} else {
state.isAnimationRunning = false;
}
};
state.animations = animations;
if (!state.isAnimationRunning) {
state.isAnimationCancelled = false;
state.isAnimationRunning = true;
frame(frameTimestamp!);
}
if (hasNonAnimatedValues) {
updateProps(viewDescriptors, nonAnimatedNewValues, maybeViewRef);
}
} else {
state.isAnimationCancelled = true;
state.animations = [];
if (!shallowEqual(oldValues, newValues)) {
updateProps(viewDescriptors, newValues, maybeViewRef, isAnimatedProps);
}
}
state.last = newValues;
}
function jestStyleUpdater(
viewDescriptors: SharedValue<Descriptor[]>,
updater: WorkletFunction<[], AnimatedStyle<any>> | (() => AnimatedStyle<any>),
state: AnimatedState,
maybeViewRef: ViewRefSet<any> | undefined,
animationsActive: SharedValue<boolean>,
animatedStyle: MutableRefObject<AnimatedStyle<any>>,
adapters: AnimatedPropsAdapterFunction[]
): void {
'worklet';
const animations: AnimatedStyle<any> = state.animations ?? {};
const newValues = updater() ?? {};
const oldValues = state.last;
// extract animated props
let hasAnimations = false;
let frameTimestamp: number | undefined;
Object.keys(animations).forEach((key) => {
const value = newValues[key];
if (!isAnimated(value)) {
delete animations[key];
}
});
Object.keys(newValues).forEach((key) => {
const value = newValues[key];
if (isAnimated(value)) {
frameTimestamp =
global.__frameTimestamp || global._getAnimationTimestamp();
prepareAnimation(frameTimestamp, value, animations[key], oldValues[key]);
animations[key] = value;
hasAnimations = true;
}
});
function frame(timestamp: Timestamp) {
// eslint-disable-next-line @typescript-eslint/no-shadow
const { animations, last, isAnimationCancelled } = state;
if (isAnimationCancelled) {
state.isAnimationRunning = false;
return;
}
const updates: AnimatedStyle<any> = {};
let allFinished = true;
Object.keys(animations).forEach((propName) => {
const finished = runAnimations(
animations[propName],
timestamp,
propName,
updates,
animationsActive
);
if (finished) {
last[propName] = updates[propName];
delete animations[propName];
} else {
allFinished = false;
}
});
if (Object.keys(updates).length) {
updatePropsJestWrapper(
viewDescriptors,
updates,
maybeViewRef,
animatedStyle,
adapters
);
}
if (!allFinished) {
requestAnimationFrame(frame);
} else {
state.isAnimationRunning = false;
}
}
if (hasAnimations) {
state.animations = animations;
if (!state.isAnimationRunning) {
state.isAnimationCancelled = false;
state.isAnimationRunning = true;
frame(frameTimestamp!);
}
} else {
state.isAnimationCancelled = true;
state.animations = [];
}
// calculate diff
state.last = newValues;
if (!shallowEqual(oldValues, newValues)) {
updatePropsJestWrapper(
viewDescriptors,
newValues,
maybeViewRef,
animatedStyle,
adapters
);
}
}
// check for invalid usage of shared values in returned object
function checkSharedValueUsage(
prop: NestedObjectValues<AnimationObject>,
currentKey?: string
): void {
if (Array.isArray(prop)) {
// if it's an array (i.ex. transform) validate all its elements
for (const element of prop) {
checkSharedValueUsage(element, currentKey);
}
} else if (
typeof prop === 'object' &&
prop !== null &&
prop.value === undefined
) {
// if it's a nested object, run validation for all its props
for (const key of Object.keys(prop)) {
checkSharedValueUsage(prop[key], key);
}
} else if (
currentKey !== undefined &&
typeof prop === 'object' &&
prop !== null &&
prop.value !== undefined
) {
// if shared value is passed insted of its value, throw an error
throw new Error(
`[Reanimated] Invalid value passed to \`${currentKey}\`, maybe you forgot to use \`.value\`?`
);
}
}
/**
* Lets you create a styles object, similar to StyleSheet styles, which can be animated using shared values.
*
* @param updater - A function returning an object with style properties you want to animate.
* @param dependencies - An optional array of dependencies. Only relevant when using Reanimated without the Babel plugin on the Web.
* @returns An animated style object which has to be passed to the `style` property of an Animated component you want to animate.
* @see https://docs.swmansion.com/react-native-reanimated/docs/core/useAnimatedStyle
*/
// You cannot pass Shared Values to `useAnimatedStyle` directly.
// @ts-expect-error This overload is required by our API.
export function useAnimatedStyle<Style extends DefaultStyle>(
updater: () => Style,
dependencies?: DependencyList | null
): Style;
export function useAnimatedStyle<Style extends DefaultStyle>(
updater:
| WorkletFunction<[], Style>
| ((() => Style) & Record<string, unknown>),
dependencies?: DependencyList | null,
adapters?: AnimatedPropsAdapterWorklet | AnimatedPropsAdapterWorklet[] | null,
isAnimatedProps = false
): AnimatedStyleHandle<Style> | JestAnimatedStyleHandle<Style> {
const viewsRef: ViewRefSet<unknown> | undefined = useViewRefSet();
const animatedUpdaterData = useRef<AnimatedUpdaterData>();
let inputs = Object.values(updater.__closure ?? {});
if (SHOULD_BE_USE_WEB) {
if (!inputs.length && dependencies?.length) {
// let web work without a Babel plugin
inputs = dependencies;
}
if (
__DEV__ &&
!inputs.length &&
!dependencies &&
!isWorkletFunction(updater)
) {
throw new Error(
`[Reanimated] \`useAnimatedStyle\` was used without a dependency array or Babel plugin. Please explicitly pass a dependency array, or enable the Babel plugin.
For more, see the docs: \`https://docs.swmansion.com/react-native-reanimated/docs/guides/web-support#web-without-the-babel-plugin\`.`
);
}
}
const adaptersArray = adapters
? Array.isArray(adapters)
? adapters
: [adapters]
: [];
const adaptersHash = adapters ? buildWorkletsHash(adaptersArray) : null;
const areAnimationsActive = useSharedValue<boolean>(true);
const jestAnimatedStyle = useRef<Style>({} as Style);
// build dependencies
if (!dependencies) {
dependencies = [...inputs, updater.__workletHash];
} else {
dependencies.push(updater.__workletHash);
}
adaptersHash && dependencies.push(adaptersHash);
if (!animatedUpdaterData.current) {
const initialStyle = initialUpdaterRun(updater);
if (__DEV__) {
validateAnimatedStyles(initialStyle);
}
animatedUpdaterData.current = {
initial: {
value: initialStyle,
updater,
},
remoteState: makeShareable({
last: initialStyle,
animations: {},
isAnimationCancelled: false,
isAnimationRunning: false,
}),
viewDescriptors: makeViewDescriptorsSet(),
};
}
const { initial, remoteState, viewDescriptors } = animatedUpdaterData.current;
const shareableViewDescriptors = viewDescriptors.shareableViewDescriptors;
dependencies.push(shareableViewDescriptors);
useEffect(() => {
let fun;
let updaterFn = updater;
if (adapters) {
updaterFn = (() => {
'worklet';
const newValues = updater();
adaptersArray.forEach((adapter) => {
adapter(newValues as Record<string, unknown>);
});
return newValues;
}) as WorkletFunction<[], Style>;
}
if (isJest()) {
fun = () => {
'worklet';
jestStyleUpdater(
shareableViewDescriptors,
updater,
remoteState,
viewsRef,
areAnimationsActive,
jestAnimatedStyle,
adaptersArray
);
};
} else {
fun = () => {
'worklet';
styleUpdater(
shareableViewDescriptors,
updaterFn,
remoteState,
viewsRef,
areAnimationsActive,
isAnimatedProps
);
};
}
const mapperId = startMapper(fun, inputs);
return () => {
stopMapper(mapperId);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, dependencies);
useEffect(() => {
areAnimationsActive.value = true;
return () => {
areAnimationsActive.value = false;
};
}, [areAnimationsActive]);
checkSharedValueUsage(initial.value);
const animatedStyleHandle = useRef<
AnimatedStyleHandle<Style> | JestAnimatedStyleHandle<Style> | null
>(null);
if (!animatedStyleHandle.current) {
animatedStyleHandle.current = isJest()
? { viewDescriptors, initial, viewsRef, jestAnimatedStyle }
: { initial, viewsRef, viewDescriptors };
}
return animatedStyleHandle.current;
}

View File

@@ -0,0 +1,69 @@
'use strict';
import { useEffect, useRef } from 'react';
import { initialUpdaterRun } from '../animation';
import type { SharedValue, WorkletFunction } from '../commonTypes';
import { makeMutable, startMapper, stopMapper } from '../core';
import type { DependencyList } from './commonTypes';
import { shouldBeUseWeb } from '../PlatformChecker';
export type DerivedValue<Value> = Readonly<SharedValue<Value>>;
/**
* Lets you create new shared values based on existing ones while keeping them reactive.
*
* @param updater - A function called whenever at least one of the shared values or state used in the function body changes.
* @param dependencies - An optional array of dependencies. Only relevant when using Reanimated without the Babel plugin on the Web.
* @returns A new readonly shared value based on a value returned from the updater function
* @see https://docs.swmansion.com/react-native-reanimated/docs/core/useDerivedValue
*/
// @ts-expect-error This overload is required by our API.
export function useDerivedValue<Value>(
updater: () => Value,
dependencies?: DependencyList
): DerivedValue<Value>;
export function useDerivedValue<Value>(
updater: WorkletFunction<[], Value>,
dependencies?: DependencyList
): DerivedValue<Value> {
const initRef = useRef<SharedValue<Value> | null>(null);
let inputs = Object.values(updater.__closure ?? {});
if (shouldBeUseWeb()) {
if (!inputs.length && dependencies?.length) {
// let web work without a Babel/SWC plugin
inputs = dependencies;
}
}
// build dependencies
if (dependencies === undefined) {
dependencies = [...inputs, updater.__workletHash];
} else {
dependencies.push(updater.__workletHash);
}
if (initRef.current === null) {
initRef.current = makeMutable(initialUpdaterRun(updater));
}
const sharedValue: SharedValue<Value> = initRef.current;
useEffect(() => {
const fun = () => {
'worklet';
sharedValue.value = updater();
};
const mapperId = startMapper(fun, inputs, [sharedValue]);
return () => {
stopMapper(mapperId);
};
}, dependencies);
useEffect(() => {
return () => {
initRef.current = null;
};
}, []);
return sharedValue;
}

View File

@@ -0,0 +1,65 @@
'use strict';
import { useRef } from 'react';
import { WorkletEventHandler } from '../WorkletEventHandler';
import type { IWorkletEventHandler, ReanimatedEvent } from './commonTypes';
/**
* Worklet to provide as an argument to `useEvent` hook.
*/
export type EventHandler<
Event extends object,
Context extends Record<string, unknown> = never
> = (event: ReanimatedEvent<Event>, context?: Context) => void;
export type EventHandlerProcessed<
Event extends object,
Context extends Record<string, unknown> = never
> = (event: Event, context?: Context) => void;
export type EventHandlerInternal<Event extends object> = {
workletEventHandler: IWorkletEventHandler<Event>;
};
/**
* Lets you run a function whenever a specified native event occurs.
*
* @param handler - A function that receives an event object with event data - {@link EventHandler}.
* @param eventNames - An array of event names the `handler` callback will react to.
* @param rebuild - Whether the event handler should be rebuilt. Defaults to `false`.
* @returns A function that will be called when the event occurs - {@link EventHandlerProcessed}.
* @see https://docs.swmansion.com/react-native-reanimated/docs/advanced/useEvent
*/
// @ts-expect-error This overload is required by our API.
// We don't know which properites of a component that is made into
// an AnimatedComponent are event handlers and we don't want to force the user to define it.
// Therefore we disguise `useEvent` return type as a simple function and we handle
// it being a React Ref in `createAnimatedComponent`.
export function useEvent<
Event extends object,
Context extends Record<string, unknown> = never
>(
handler: EventHandler<Event, Context>,
eventNames?: string[],
rebuild?: boolean
): EventHandlerProcessed<Event, Context>;
export function useEvent<Event extends object, Context = never>(
handler: (event: ReanimatedEvent<Event>, context?: Context) => void,
eventNames: string[] = [],
rebuild = false
): EventHandlerInternal<Event> {
const initRef = useRef<EventHandlerInternal<Event>>(null!);
if (initRef.current === null) {
const workletEventHandler = new WorkletEventHandler<Event>(
handler,
eventNames
);
initRef.current = { workletEventHandler };
} else if (rebuild) {
const workletEventHandler = initRef.current.workletEventHandler;
workletEventHandler.updateEventHandler(handler, eventNames);
initRef.current = { workletEventHandler };
}
return initRef.current;
}

View File

@@ -0,0 +1,55 @@
'use strict';
import { useEffect, useRef } from 'react';
import FrameCallbackRegistryJS from '../frameCallback/FrameCallbackRegistryJS';
import type { FrameInfo } from '../frameCallback/FrameCallbackRegistryUI';
/**
* @param setActive - A function that lets you start the frame callback or stop it from running.
* @param isActive - A boolean indicating whether a callback is running.
* @param callbackId - A number indicating a unique identifier of the frame callback.
* @see https://docs.swmansion.com/react-native-reanimated/docs/advanced/useFrameCallback#returns
*/
export type FrameCallback = {
setActive: (isActive: boolean) => void;
isActive: boolean;
callbackId: number;
};
const frameCallbackRegistry = new FrameCallbackRegistryJS();
/**
* Lets you run a function on every frame update.
*
* @param callback - A function executed on every frame update.
* @param autostart - Whether the callback should start automatically. Defaults to `true`.
* @returns A frame callback object - {@link FrameCallback}.
* @see https://docs.swmansion.com/react-native-reanimated/docs/advanced/useFrameCallback
*/
export function useFrameCallback(
callback: (frameInfo: FrameInfo) => void,
autostart = true
): FrameCallback {
const ref = useRef<FrameCallback>({
setActive: (isActive: boolean) => {
frameCallbackRegistry.manageStateFrameCallback(
ref.current.callbackId,
isActive
);
ref.current.isActive = isActive;
},
isActive: autostart,
callbackId: -1,
});
useEffect(() => {
ref.current.callbackId =
frameCallbackRegistry.registerFrameCallback(callback);
ref.current.setActive(ref.current.isActive);
return () => {
frameCallbackRegistry.unregisterFrameCallback(ref.current.callbackId);
ref.current.callbackId = -1;
};
}, [callback, autostart]);
return ref.current;
}

View File

@@ -0,0 +1,96 @@
'use strict';
import { useEffect, useRef } from 'react';
import type { WorkletFunction } from '../commonTypes';
import { isWeb, isJest } from '../PlatformChecker';
import type { DependencyList, ReanimatedEvent } from './commonTypes';
import { areDependenciesEqual, buildDependencies } from './utils';
import { makeShareable } from '../shareables';
interface GeneralHandler<
Event extends object,
Context extends Record<string, unknown>
> {
(event: ReanimatedEvent<Event>, context: Context): void;
}
type GeneralWorkletHandler<
Event extends object,
Context extends Record<string, unknown>
> = WorkletFunction<[event: ReanimatedEvent<Event>, context: Context]>;
type GeneralHandlers<
Event extends object,
Context extends Record<string, unknown>
> = Record<string, GeneralHandler<Event, Context> | undefined>;
type GeneralWorkletHandlers<
Event extends object,
Context extends Record<string, unknown>
> = Record<string, GeneralWorkletHandler<Event, Context> | undefined>;
interface ContextWithDependencies<Context extends Record<string, unknown>> {
context: Context;
savedDependencies: DependencyList;
}
export interface UseHandlerContext<Context extends Record<string, unknown>> {
context: Context;
doDependenciesDiffer: boolean;
useWeb: boolean;
}
/**
* Lets you find out whether the event handler dependencies have changed.
*
* @param handlers - An object of event handlers.
* @param dependencies - An optional array of dependencies.
* @returns An object containing a boolean indicating whether the dependencies have changed, and a boolean indicating whether the code is running on the web.
* @see https://docs.swmansion.com/react-native-reanimated/docs/advanced/useHandler
*/
// @ts-expect-error This overload is required by our API.
export function useHandler<
Event extends object,
Context extends Record<string, unknown>
>(
handlers: GeneralHandlers<Event, Context>,
dependencies?: DependencyList
): UseHandlerContext<Context>;
export function useHandler<
Event extends object,
Context extends Record<string, unknown>
>(
handlers: GeneralWorkletHandlers<Event, Context>,
dependencies?: DependencyList
): UseHandlerContext<Context> {
const initRef = useRef<ContextWithDependencies<Context> | null>(null);
if (initRef.current === null) {
const context = makeShareable({} as Context);
initRef.current = {
context,
savedDependencies: [],
};
}
useEffect(() => {
return () => {
initRef.current = null;
};
}, []);
const { context, savedDependencies } = initRef.current;
dependencies = buildDependencies(
dependencies,
handlers as Record<string, WorkletFunction | undefined>
);
const doDependenciesDiffer = !areDependenciesEqual(
dependencies,
savedDependencies
);
initRef.current.savedDependencies = dependencies;
const useWeb = isWeb() || isJest();
return { context, doDependenciesDiffer, useWeb };
}

View File

@@ -0,0 +1,16 @@
'use strict';
import { isReducedMotion } from '../PlatformChecker';
const IS_REDUCED_MOTION = isReducedMotion();
/**
* Lets you query the reduced motion system setting.
*
* Changing the reduced motion system setting doesn't cause your components to rerender.
*
* @returns A boolean indicating whether the reduced motion setting was enabled when the app started.
* @see https://docs.swmansion.com/react-native-reanimated/docs/device/useReducedMotion
*/
export function useReducedMotion() {
return IS_REDUCED_MOTION;
}

View File

@@ -0,0 +1,142 @@
'use strict';
import { useEffect, useRef, useCallback } from 'react';
import type { SharedValue } from '../commonTypes';
import type { EventHandlerInternal } from './useEvent';
import { useEvent } from './useEvent';
import { useSharedValue } from './useSharedValue';
import type { AnimatedScrollView } from '../component/ScrollView';
import type {
AnimatedRef,
RNNativeScrollEvent,
ReanimatedScrollEvent,
} from './commonTypes';
import { isWeb } from '../PlatformChecker';
const IS_WEB = isWeb();
/**
* Lets you synchronously get the current offset of a `ScrollView`.
*
* @param animatedRef - An [animated ref](https://docs.swmansion.com/react-native-reanimated/docs/core/useAnimatedRef) attached to an Animated.ScrollView component.
* @returns A shared value which holds the current offset of the `ScrollView`.
* @see https://docs.swmansion.com/react-native-reanimated/docs/scroll/useScrollViewOffset
*/
export const useScrollViewOffset = IS_WEB
? useScrollViewOffsetWeb
: useScrollViewOffsetNative;
function useScrollViewOffsetWeb(
animatedRef: AnimatedRef<AnimatedScrollView>,
providedOffset?: SharedValue<number>
): SharedValue<number> {
const internalOffset = useSharedValue(0);
const offset = useRef(providedOffset ?? internalOffset).current;
const scrollRef = useRef<AnimatedScrollView | null>(null);
const eventHandler = useCallback(() => {
'worklet';
const element = getWebScrollableElement(animatedRef.current);
// scrollLeft is the X axis scrolled offset, works properly also with RTL layout
offset.value =
element.scrollLeft === 0 ? element.scrollTop : element.scrollLeft;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [animatedRef, animatedRef.current]);
useEffect(() => {
// We need to make sure that listener for old animatedRef value is removed
if (scrollRef.current !== null) {
getWebScrollableElement(scrollRef.current).removeEventListener(
'scroll',
eventHandler
);
}
scrollRef.current = animatedRef.current;
const element = getWebScrollableElement(animatedRef.current);
element.addEventListener('scroll', eventHandler);
return () => {
element.removeEventListener('scroll', eventHandler);
};
// React here has a problem with `animatedRef.current` since a Ref .current
// field shouldn't be used as a dependency. However, in this case we have
// to do it this way.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [animatedRef, animatedRef.current, eventHandler]);
return offset;
}
function useScrollViewOffsetNative(
animatedRef: AnimatedRef<AnimatedScrollView>,
providedOffset?: SharedValue<number>
): SharedValue<number> {
const internalOffset = useSharedValue(0);
const offset = useRef(providedOffset ?? internalOffset).current;
const scrollRef = useRef<AnimatedScrollView | null>(null);
const scrollRefTag = useRef<number | null>(null);
const eventHandler = useEvent<RNNativeScrollEvent>(
(event: ReanimatedScrollEvent) => {
'worklet';
offset.value =
event.contentOffset.x === 0
? event.contentOffset.y
: event.contentOffset.x;
},
scrollNativeEventNames
// Read https://github.com/software-mansion/react-native-reanimated/pull/5056
// for more information about this cast.
) as unknown as EventHandlerInternal<ReanimatedScrollEvent>;
useEffect(() => {
// We need to make sure that listener for old animatedRef value is removed
if (scrollRef.current !== null && scrollRefTag.current !== null) {
eventHandler.workletEventHandler.unregisterFromEvents(
scrollRefTag.current
);
}
// Store the ref and viewTag for future cleanup
scrollRef.current = animatedRef.current;
scrollRefTag.current = animatedRef.getTag();
if (scrollRefTag === null) {
console.warn(
'[Reanimated] ScrollViewOffset failed to resolve the view tag from animated ref. Did you forget to attach the ref to a component?'
);
} else {
eventHandler.workletEventHandler.registerForEvents(scrollRefTag.current);
}
return () => {
if (scrollRefTag.current !== null) {
eventHandler.workletEventHandler.unregisterFromEvents(
scrollRefTag.current
);
}
};
// React here has a problem with `animatedRef.current` since a Ref .current
// field shouldn't be used as a dependency. However, in this case we have
// to do it this way.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [animatedRef, animatedRef.current, eventHandler]);
return offset;
}
function getWebScrollableElement(
scrollComponent: AnimatedScrollView | null
): HTMLElement {
return (
(scrollComponent?.getScrollableNode() as unknown as HTMLElement) ??
scrollComponent
);
}
const scrollNativeEventNames = [
'onScroll',
'onScrollBeginDrag',
'onScrollEndDrag',
'onMomentumScrollBegin',
'onMomentumScrollEnd',
];

View File

@@ -0,0 +1,22 @@
'use strict';
import { useEffect, useState } from 'react';
import { cancelAnimation } from '../animation';
import type { SharedValue } from '../commonTypes';
import { makeMutable } from '../core';
/**
* Lets you define [shared values](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/glossary#shared-value) in your components.
*
* @param initialValue - The value you want to be initially stored to a `.value` property.
* @returns A shared value with a single `.value` property initially set to the `initialValue` - {@link SharedValue}.
* @see https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue
*/
export function useSharedValue<Value>(initialValue: Value): SharedValue<Value> {
const [mutable] = useState(() => makeMutable(initialValue));
useEffect(() => {
return () => {
cancelAnimation(mutable);
};
}, [mutable]);
return mutable;
}

View File

@@ -0,0 +1,13 @@
'use strict';
import { useCallback } from 'react';
import type { DependencyList } from './commonTypes';
/**
* @deprecated don't use
*/
export function useWorkletCallback<Args extends unknown[], ReturnValue>(
worklet: (...args: Args) => ReturnValue,
deps?: DependencyList
) {
return useCallback(worklet, deps ?? []);
}

View File

@@ -0,0 +1,119 @@
'use strict';
import type { WorkletFunction } from '../commonTypes';
import type { DependencyList } from './commonTypes';
// Builds one big hash from multiple worklets' hashes.
export function buildWorkletsHash<Args extends unknown[], ReturnValue>(
worklets:
| Record<string, WorkletFunction<Args, ReturnValue>>
| WorkletFunction<Args, ReturnValue>[]
) {
// For arrays `Object.values` returns the array itself.
return Object.values(worklets).reduce(
(acc, worklet: WorkletFunction<Args, ReturnValue>) =>
acc + worklet.__workletHash.toString(),
''
);
}
// Builds dependencies array for useEvent handlers.
export function buildDependencies(
dependencies: DependencyList,
handlers: Record<string, WorkletFunction | undefined>
) {
type Handler = (typeof handlers)[keyof typeof handlers];
const handlersList = Object.values(handlers).filter(
(handler) => handler !== undefined
) as NonNullable<Handler>[];
if (!dependencies) {
dependencies = handlersList.map((handler) => {
return {
workletHash: handler.__workletHash,
closure: handler.__closure,
};
});
} else {
dependencies.push(buildWorkletsHash(handlersList));
}
return dependencies;
}
// This is supposed to work as useEffect comparison.
export function areDependenciesEqual(
nextDependencies: DependencyList,
prevDependencies: DependencyList
) {
function is(x: number, y: number) {
return (
(x === y && (x !== 0 || 1 / x === 1 / y)) ||
(Number.isNaN(x) && Number.isNaN(y))
);
}
const objectIs: (nextDeps: unknown, prevDeps: unknown) => boolean =
typeof Object.is === 'function' ? Object.is : is;
function areHookInputsEqual(
nextDeps: DependencyList,
prevDeps: DependencyList
) {
if (!nextDeps || !prevDeps || prevDeps.length !== nextDeps.length) {
return false;
}
for (let i = 0; i < prevDeps.length; ++i) {
if (!objectIs(nextDeps[i], prevDeps[i])) {
return false;
}
}
return true;
}
return areHookInputsEqual(nextDependencies, prevDependencies);
}
export function isAnimated(prop: unknown) {
'worklet';
if (Array.isArray(prop)) {
return prop.some(isAnimated);
} else if (typeof prop === 'object' && prop !== null) {
if ((prop as Record<string, unknown>).onFrame !== undefined) {
return true;
} else {
return Object.values(prop).some(isAnimated);
}
}
return false;
}
// This function works because `Object.keys`
// return empty array of primitives and on arrays
// it returns array of its indices.
export function shallowEqual<
T extends Record<string | number | symbol, unknown>
>(a: T, b: T) {
'worklet';
const aKeys = Object.keys(a);
const bKeys = Object.keys(b);
if (aKeys.length !== bKeys.length) {
return false;
}
for (let i = 0; i < aKeys.length; i++) {
if (a[aKeys[i]] !== b[aKeys[i]]) {
return false;
}
}
return true;
}
export function validateAnimatedStyles(styles: unknown[] | object) {
'worklet';
if (typeof styles !== 'object') {
throw new Error(
`[Reanimated] \`useAnimatedStyle\` has to return an object, found ${typeof styles} instead.`
);
} else if (Array.isArray(styles)) {
throw new Error(
'[Reanimated] `useAnimatedStyle` has to return an object and cannot return static styles combined with dynamic ones. Please do merging where a component receives props.'
);
}
}

View File

@@ -0,0 +1,286 @@
'use strict';
import './publicGlobals';
export type { WorkletRuntime } from './core';
export {
runOnJS,
runOnUI,
createWorkletRuntime,
runOnRuntime,
makeMutable,
makeShareableCloneRecursive,
isReanimated3,
isConfigured,
enableLayoutAnimations,
getViewProp,
executeOnUIRuntimeSync,
} from './core';
export type {
GestureHandlers,
AnimatedRef,
DerivedValue,
ScrollHandler,
ScrollHandlers,
ScrollHandlerProcessed,
FrameCallback,
ScrollEvent,
EventHandler,
EventHandlerProcessed,
UseHandlerContext,
ReanimatedEvent,
} from './hook';
export {
useAnimatedProps,
useEvent,
useHandler,
useWorkletCallback,
useSharedValue,
useReducedMotion,
useAnimatedStyle,
useAnimatedGestureHandler,
useAnimatedReaction,
useAnimatedRef,
useAnimatedScrollHandler,
useDerivedValue,
useAnimatedSensor,
useFrameCallback,
useAnimatedKeyboard,
useScrollViewOffset,
} from './hook';
export type {
DelayAnimation,
RepeatAnimation,
SequenceAnimation,
StyleLayoutAnimation,
WithTimingConfig,
TimingAnimation,
WithSpringConfig,
SpringAnimation,
WithDecayConfig,
DecayAnimation,
} from './animation';
export {
cancelAnimation,
defineAnimation,
withClamp,
withDecay,
withDelay,
withRepeat,
withSequence,
withSpring,
withTiming,
} from './animation';
export type { ExtrapolationConfig, ExtrapolationType } from './interpolation';
export { Extrapolation, interpolate, clamp } from './interpolation';
export type {
InterpolationOptions,
InterpolateConfig,
InterpolateRGB,
InterpolateHSV,
} from './interpolateColor';
export {
/**
* @deprecated Please use {@link Extrapolation} instead.
*/
Extrapolate,
ColorSpace,
interpolateColor,
useInterpolateConfig,
} from './interpolateColor';
export type {
EasingFunction,
EasingFn,
EasingFunctionFactory,
EasingFactoryFn,
} from './Easing';
export { Easing } from './Easing';
export type { ComponentCoords } from './platformFunctions';
export {
measure,
dispatchCommand,
scrollTo,
setGestureState,
setNativeProps,
getRelativeCoords,
} from './platformFunctions';
export type { ParsedColorArray } from './Colors';
export { isColor, processColor, convertToRGBA } from './Colors';
export { createAnimatedPropAdapter } from './PropAdapters';
export type {
LayoutAnimation,
EntryAnimationsValues,
ExitAnimationsValues,
EntryExitAnimationFunction,
LayoutAnimationsValues,
LayoutAnimationFunction,
ILayoutAnimationBuilder,
IEntryExitAnimationBuilder,
} from './layoutReanimation';
export {
BaseAnimationBuilder,
ComplexAnimationBuilder,
Keyframe,
// Flip
FlipInXUp,
FlipInYLeft,
FlipInXDown,
FlipInYRight,
FlipInEasyX,
FlipInEasyY,
FlipOutXUp,
FlipOutYLeft,
FlipOutXDown,
FlipOutYRight,
FlipOutEasyX,
FlipOutEasyY,
// Stretch
StretchInX,
StretchInY,
StretchOutX,
StretchOutY,
// Fade
FadeIn,
FadeInRight,
FadeInLeft,
FadeInUp,
FadeInDown,
FadeOut,
FadeOutRight,
FadeOutLeft,
FadeOutUp,
FadeOutDown,
// Slide
SlideInRight,
SlideInLeft,
SlideOutRight,
SlideOutLeft,
SlideInUp,
SlideInDown,
SlideOutUp,
SlideOutDown,
// Zoom
ZoomIn,
ZoomInRotate,
ZoomInLeft,
ZoomInRight,
ZoomInUp,
ZoomInDown,
ZoomInEasyUp,
ZoomInEasyDown,
ZoomOut,
ZoomOutRotate,
ZoomOutLeft,
ZoomOutRight,
ZoomOutUp,
ZoomOutDown,
ZoomOutEasyUp,
ZoomOutEasyDown,
// Bounce
BounceIn,
BounceInDown,
BounceInUp,
BounceInLeft,
BounceInRight,
BounceOut,
BounceOutDown,
BounceOutUp,
BounceOutLeft,
BounceOutRight,
// Lightspeed
LightSpeedInRight,
LightSpeedInLeft,
LightSpeedOutRight,
LightSpeedOutLeft,
// Pinwheel
PinwheelIn,
PinwheelOut,
// Rotate
RotateInDownLeft,
RotateInDownRight,
RotateInUpLeft,
RotateInUpRight,
RotateOutDownLeft,
RotateOutDownRight,
RotateOutUpLeft,
RotateOutUpRight,
// Roll
RollInLeft,
RollInRight,
RollOutLeft,
RollOutRight,
// Transitions
Layout,
LinearTransition,
FadingTransition,
SequencedTransition,
JumpingTransition,
CurvedTransition,
EntryExitTransition,
combineTransition,
// SET
SharedTransition,
SharedTransitionType,
} from './layoutReanimation';
export { isSharedValue } from './isSharedValue';
export type {
StyleProps,
SharedValue,
AnimatableValueObject,
AnimatableValue,
AnimationObject,
SensorConfig,
Animation,
AnimatedSensor,
AnimationCallback,
Value3D,
ValueRotation,
AnimatedKeyboardInfo,
AnimatedKeyboardOptions,
MeasuredDimensions,
} from './commonTypes';
export {
SensorType,
IOSReferenceFrame,
InterfaceOrientation,
KeyboardState,
ReduceMotion,
isWorkletFunction,
} from './commonTypes';
export type { FrameInfo } from './frameCallback';
export { getUseOfValueInStyleWarning } from './pluginUtils';
export {
withReanimatedTimer,
advanceAnimationByTime,
advanceAnimationByFrame,
setUpTests,
getAnimatedStyle,
} from './jestUtils';
export { LayoutAnimationConfig } from './component/LayoutAnimationConfig';
export { PerformanceMonitor } from './component/PerformanceMonitor';
export type {
Adaptable,
AdaptTransforms,
AnimateProps,
AnimatedProps,
AnimatedTransform,
TransformStyleTypes,
TransformArrayItem,
AnimateStyle,
AnimatedStyle,
AnimatedStyleProp,
StylesOrDefault,
} from './helperTypes';
export type { AnimatedScrollViewProps } from './component/ScrollView';
export type { FlatListPropsWithLayout } from './component/FlatList';
export { startMapper, stopMapper } from './mappers';
export {
startScreenTransition,
finishScreenTransition,
ScreenTransition,
} from './screenTransition';
export type {
AnimatedScreenTransition,
GoBackGesture,
ScreenTransitionConfig,
} from './screenTransition';

View File

@@ -0,0 +1,131 @@
'use strict';
import { reportFatalErrorOnJS } from './errors';
import { isChromeDebugger, isJest, shouldBeUseWeb } from './PlatformChecker';
import {
runOnJS,
setupMicrotasks,
callMicrotasks,
runOnUIImmediately,
} from './threads';
import { mockedRequestAnimationFrame } from './mockedRequestAnimationFrame';
const IS_JEST = isJest();
const SHOULD_BE_USE_WEB = shouldBeUseWeb();
const IS_CHROME_DEBUGGER = isChromeDebugger();
// callGuard is only used with debug builds
export function callGuardDEV<Args extends unknown[], ReturnValue>(
fn: (...args: Args) => ReturnValue,
...args: Args
): ReturnValue | void {
'worklet';
try {
return fn(...args);
} catch (e) {
if (global.__ErrorUtils) {
global.__ErrorUtils.reportFatalError(e as Error);
} else {
throw e;
}
}
}
export function setupCallGuard() {
'worklet';
global.__callGuardDEV = callGuardDEV;
global.__ErrorUtils = {
reportFatalError: (error: Error) => {
runOnJS(reportFatalErrorOnJS)({
message: error.message,
stack: error.stack,
});
},
};
}
// We really have to create a copy of console here. Function runOnJS we use on elements inside
// this object makes it not configurable
const capturableConsole = { ...console };
export function setupConsole() {
'worklet';
if (!IS_CHROME_DEBUGGER) {
// @ts-ignore TypeScript doesn't like that there are missing methods in console object, but we don't provide all the methods for the UI runtime console version
global.console = {
/* eslint-disable @typescript-eslint/unbound-method */
assert: runOnJS(capturableConsole.assert),
debug: runOnJS(capturableConsole.debug),
log: runOnJS(capturableConsole.log),
warn: runOnJS(capturableConsole.warn),
error: runOnJS(capturableConsole.error),
info: runOnJS(capturableConsole.info),
/* eslint-enable @typescript-eslint/unbound-method */
};
}
}
function setupRequestAnimationFrame() {
'worklet';
// Jest mocks requestAnimationFrame API and it does not like if that mock gets overridden
// so we avoid doing requestAnimationFrame batching in Jest environment.
const nativeRequestAnimationFrame = global.requestAnimationFrame;
let animationFrameCallbacks: Array<(timestamp: number) => void> = [];
let lastNativeAnimationFrameTimestamp = -1;
global.__flushAnimationFrame = (frameTimestamp: number) => {
const currentCallbacks = animationFrameCallbacks;
animationFrameCallbacks = [];
currentCallbacks.forEach((f) => f(frameTimestamp));
callMicrotasks();
};
global.requestAnimationFrame = (
callback: (timestamp: number) => void
): number => {
animationFrameCallbacks.push(callback);
if (animationFrameCallbacks.length === 1) {
// We schedule native requestAnimationFrame only when the first callback
// is added and then use it to execute all the enqueued callbacks. Once
// the callbacks are run, we clear the array.
nativeRequestAnimationFrame((timestamp) => {
if (lastNativeAnimationFrameTimestamp >= timestamp) {
// Make sure we only execute the callbacks once for a given frame
return;
}
lastNativeAnimationFrameTimestamp = timestamp;
global.__frameTimestamp = timestamp;
global.__flushAnimationFrame(timestamp);
global.__frameTimestamp = undefined;
});
}
// Reanimated currently does not support cancelling callbacks requested with
// requestAnimationFrame. We return -1 as identifier which isn't in line
// with the spec but it should give users better clue in case they actually
// attempt to store the value returned from rAF and use it for cancelling.
return -1;
};
}
export function initializeUIRuntime() {
if (IS_JEST) {
// requestAnimationFrame react-native jest's setup is incorrect as it polyfills
// the method directly using setTimeout, therefore the callback doesn't get the
// expected timestamp as the only argument: https://github.com/facebook/react-native/blob/main/packages/react-native/jest/setup.js#L28
// We override this setup here to make sure that callbacks get the proper timestamps
// when executed. For non-jest environments we define requestAnimationFrame in setupRequestAnimationFrame
// @ts-ignore TypeScript uses Node definition for rAF, setTimeout, etc which returns a Timeout object rather than a number
globalThis.requestAnimationFrame = mockedRequestAnimationFrame;
}
runOnUIImmediately(() => {
'worklet';
setupCallGuard();
setupConsole();
if (!SHOULD_BE_USE_WEB) {
setupMicrotasks();
setupRequestAnimationFrame();
}
})();
}

View File

@@ -0,0 +1,272 @@
'use strict';
import {
hsvToColor,
RGBtoHSV,
rgbaColor,
processColor,
red,
green,
blue,
opacity,
} from './Colors';
import { makeMutable } from './core';
import { Extrapolation, interpolate } from './interpolation';
import type { SharedValue } from './commonTypes';
import { useSharedValue } from './hook/useSharedValue';
/**
* @deprecated Please use Extrapolation instead
*/
export const Extrapolate = Extrapolation;
/**
* Options for color interpolation.
*
* @param gamma - Gamma value used in gamma correction. Defaults to `2.2`.
* @param useCorrectedHSVInterpolation - Whether to reduce the number of colors the interpolation has to go through. Defaults to `true`.
*/
export type InterpolationOptions = {
gamma?: number;
useCorrectedHSVInterpolation?: boolean;
};
const interpolateColorsHSV = (
value: number,
inputRange: readonly number[],
colors: InterpolateHSV,
options: InterpolationOptions
) => {
'worklet';
let h = 0;
const { useCorrectedHSVInterpolation = true } = options;
if (useCorrectedHSVInterpolation) {
// if the difference between hues in a range is > 180 deg
// then move the hue at the right end of the range +/- 360 deg
// and add the next point in the original place + 0.00001 with original hue
// to not break the next range
const correctedInputRange = [inputRange[0]];
const originalH = colors.h;
const correctedH = [originalH[0]];
for (let i = 1; i < originalH.length; ++i) {
const d = originalH[i] - originalH[i - 1];
if (originalH[i] > originalH[i - 1] && d > 0.5) {
correctedInputRange.push(inputRange[i]);
correctedInputRange.push(inputRange[i] + 0.00001);
correctedH.push(originalH[i] - 1);
correctedH.push(originalH[i]);
} else if (originalH[i] < originalH[i - 1] && d < -0.5) {
correctedInputRange.push(inputRange[i]);
correctedInputRange.push(inputRange[i] + 0.00001);
correctedH.push(originalH[i] + 1);
correctedH.push(originalH[i]);
} else {
correctedInputRange.push(inputRange[i]);
correctedH.push(originalH[i]);
}
}
h =
(interpolate(
value,
correctedInputRange,
correctedH,
Extrapolation.CLAMP
) +
1) %
1;
} else {
h = interpolate(value, inputRange, colors.h, Extrapolation.CLAMP);
}
const s = interpolate(value, inputRange, colors.s, Extrapolation.CLAMP);
const v = interpolate(value, inputRange, colors.v, Extrapolation.CLAMP);
const a = interpolate(value, inputRange, colors.a, Extrapolation.CLAMP);
return hsvToColor(h, s, v, a);
};
const toLinearSpace = (x: number[], gamma: number): number[] => {
'worklet';
return x.map((v) => Math.pow(v / 255, gamma));
};
const toGammaSpace = (x: number, gamma: number): number => {
'worklet';
return Math.round(Math.pow(x, 1 / gamma) * 255);
};
const interpolateColorsRGB = (
value: number,
inputRange: readonly number[],
colors: InterpolateRGB,
options: InterpolationOptions
) => {
'worklet';
const { gamma = 2.2 } = options;
let { r: outputR, g: outputG, b: outputB } = colors;
if (gamma !== 1) {
outputR = toLinearSpace(outputR, gamma);
outputG = toLinearSpace(outputG, gamma);
outputB = toLinearSpace(outputB, gamma);
}
const r = interpolate(value, inputRange, outputR, Extrapolation.CLAMP);
const g = interpolate(value, inputRange, outputG, Extrapolation.CLAMP);
const b = interpolate(value, inputRange, outputB, Extrapolation.CLAMP);
const a = interpolate(value, inputRange, colors.a, Extrapolation.CLAMP);
if (gamma === 1) {
return rgbaColor(r, g, b, a);
}
return rgbaColor(
toGammaSpace(r, gamma),
toGammaSpace(g, gamma),
toGammaSpace(b, gamma),
a
);
};
export interface InterpolateRGB {
r: number[];
g: number[];
b: number[];
a: number[];
}
const getInterpolateRGB = (
colors: readonly (string | number)[]
): InterpolateRGB => {
'worklet';
const r = [];
const g = [];
const b = [];
const a = [];
for (let i = 0; i < colors.length; ++i) {
const color = colors[i];
const processedColor = processColor(color);
// explicit check in case if processedColor is 0
if (processedColor !== null && processedColor !== undefined) {
r.push(red(processedColor));
g.push(green(processedColor));
b.push(blue(processedColor));
a.push(opacity(processedColor));
}
}
return { r, g, b, a };
};
export interface InterpolateHSV {
h: number[];
s: number[];
v: number[];
a: number[];
}
const getInterpolateHSV = (
colors: readonly (string | number)[]
): InterpolateHSV => {
'worklet';
const h = [];
const s = [];
const v = [];
const a = [];
for (let i = 0; i < colors.length; ++i) {
const color = colors[i];
const processedColor = processColor(color) as any;
if (typeof processedColor === 'number') {
const processedHSVColor = RGBtoHSV(
red(processedColor),
green(processedColor),
blue(processedColor)
);
h.push(processedHSVColor.h);
s.push(processedHSVColor.s);
v.push(processedHSVColor.v);
a.push(opacity(processedColor));
}
}
return { h, s, v, a };
};
/**
* Lets you map a value from a range of numbers to a range of colors using linear interpolation.
*
* @param value - A number from the `input` range that is going to be mapped to the color in the `output` range.
* @param inputRange - An array of numbers specifying the input range of the interpolation.
* @param outputRange - An array of output colors values (eg. "red", "#00FFCC", "rgba(255, 0, 0, 0.5)").
* @param colorSpace - The color space to use for interpolation. Defaults to 'RGB'.
* @param options - Additional options for interpolation - {@link InterpolationOptions}.
* @returns The color after interpolation from within the output range in rgba(r, g, b, a) format.
* @see https://docs.swmansion.com/react-native-reanimated/docs/utilities/interpolateColor
*/
export function interpolateColor(
value: number,
inputRange: readonly number[],
outputRange: readonly string[],
colorSpace?: 'RGB' | 'HSV',
options?: InterpolationOptions
): string;
export function interpolateColor(
value: number,
inputRange: readonly number[],
outputRange: readonly number[],
colorSpace?: 'RGB' | 'HSV',
options?: InterpolationOptions
): number;
export function interpolateColor(
value: number,
inputRange: readonly number[],
outputRange: readonly (string | number)[],
colorSpace: 'RGB' | 'HSV' = 'RGB',
options: InterpolationOptions = {}
): string | number {
'worklet';
if (colorSpace === 'HSV') {
return interpolateColorsHSV(
value,
inputRange,
getInterpolateHSV(outputRange),
options
);
} else if (colorSpace === 'RGB') {
return interpolateColorsRGB(
value,
inputRange,
getInterpolateRGB(outputRange),
options
);
}
throw new Error(
`[Reanimated] Invalid color space provided: ${
colorSpace as string
}. Supported values are: ['RGB', 'HSV'].`
);
}
export enum ColorSpace {
RGB = 0,
HSV = 1,
}
export interface InterpolateConfig {
inputRange: readonly number[];
outputRange: readonly (string | number)[];
colorSpace: ColorSpace;
cache: SharedValue<InterpolateRGB | InterpolateHSV | null>;
options: InterpolationOptions;
}
export function useInterpolateConfig(
inputRange: readonly number[],
outputRange: readonly (string | number)[],
colorSpace = ColorSpace.RGB,
options: InterpolationOptions = {}
): SharedValue<InterpolateConfig> {
return useSharedValue<InterpolateConfig>({
inputRange,
outputRange,
colorSpace,
cache: makeMutable<InterpolateRGB | InterpolateHSV | null>(null),
options,
});
}

View File

@@ -0,0 +1,231 @@
'use strict';
/**
* Extrapolation type.
*
* @param IDENTITY - Returns the provided value as is.
* @param CLAMP - Clamps the value to the edge of the output range.
* @param EXTEND - Predicts the values beyond the output range.
*/
export enum Extrapolation {
IDENTITY = 'identity',
CLAMP = 'clamp',
EXTEND = 'extend',
}
/**
* Represents the possible values for extrapolation as a string.
*/
type ExtrapolationAsString = 'identity' | 'clamp' | 'extend';
interface InterpolationNarrowedInput {
leftEdgeInput: number;
rightEdgeInput: number;
leftEdgeOutput: number;
rightEdgeOutput: number;
}
/**
* Allows to specify extrapolation for left and right edge of the interpolation.
*/
export interface ExtrapolationConfig {
extrapolateLeft?: Extrapolation | string;
extrapolateRight?: Extrapolation | string;
}
interface RequiredExtrapolationConfig {
extrapolateLeft: Extrapolation;
extrapolateRight: Extrapolation;
}
/**
* Configuration options for extrapolation.
*/
export type ExtrapolationType =
| ExtrapolationConfig
| Extrapolation
| ExtrapolationAsString
| undefined;
function getVal(
type: Extrapolation,
coef: number,
val: number,
leftEdgeOutput: number,
rightEdgeOutput: number,
x: number
): number {
'worklet';
switch (type) {
case Extrapolation.IDENTITY:
return x;
case Extrapolation.CLAMP:
if (coef * val < coef * leftEdgeOutput) {
return leftEdgeOutput;
}
return rightEdgeOutput;
case Extrapolation.EXTEND:
default:
return val;
}
}
function isExtrapolate(value: string): value is Extrapolation {
'worklet';
return (
/* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */
value === Extrapolation.EXTEND ||
value === Extrapolation.CLAMP ||
value === Extrapolation.IDENTITY
/* eslint-enable @typescript-eslint/no-unsafe-enum-comparison */
);
}
// validates extrapolations type
// if type is correct, converts it to ExtrapolationConfig
function validateType(type: ExtrapolationType): RequiredExtrapolationConfig {
'worklet';
// initialize extrapolationConfig with default extrapolation
const extrapolationConfig: RequiredExtrapolationConfig = {
extrapolateLeft: Extrapolation.EXTEND,
extrapolateRight: Extrapolation.EXTEND,
};
if (!type) {
return extrapolationConfig;
}
if (typeof type === 'string') {
if (!isExtrapolate(type)) {
throw new Error(
`[Reanimated] Unsupported value for "interpolate" \nSupported values: ["extend", "clamp", "identity", Extrapolatation.CLAMP, Extrapolatation.EXTEND, Extrapolatation.IDENTITY]\n Valid example:
interpolate(value, [inputRange], [outputRange], "clamp")`
);
}
extrapolationConfig.extrapolateLeft = type;
extrapolationConfig.extrapolateRight = type;
return extrapolationConfig;
}
// otherwise type is extrapolation config object
if (
(type.extrapolateLeft && !isExtrapolate(type.extrapolateLeft)) ||
(type.extrapolateRight && !isExtrapolate(type.extrapolateRight))
) {
throw new Error(
`[Reanimated] Unsupported value for "interpolate" \nSupported values: ["extend", "clamp", "identity", Extrapolatation.CLAMP, Extrapolatation.EXTEND, Extrapolatation.IDENTITY]\n Valid example:
interpolate(value, [inputRange], [outputRange], {
extrapolateLeft: Extrapolation.CLAMP,
extrapolateRight: Extrapolation.IDENTITY
}})`
);
}
Object.assign(extrapolationConfig, type);
return extrapolationConfig;
}
function internalInterpolate(
x: number,
narrowedInput: InterpolationNarrowedInput,
extrapolationConfig: RequiredExtrapolationConfig
) {
'worklet';
const { leftEdgeInput, rightEdgeInput, leftEdgeOutput, rightEdgeOutput } =
narrowedInput;
if (rightEdgeInput - leftEdgeInput === 0) return leftEdgeOutput;
const progress = (x - leftEdgeInput) / (rightEdgeInput - leftEdgeInput);
const val = leftEdgeOutput + progress * (rightEdgeOutput - leftEdgeOutput);
const coef = rightEdgeOutput >= leftEdgeOutput ? 1 : -1;
if (coef * val < coef * leftEdgeOutput) {
return getVal(
extrapolationConfig.extrapolateLeft,
coef,
val,
leftEdgeOutput,
rightEdgeOutput,
x
);
} else if (coef * val > coef * rightEdgeOutput) {
return getVal(
extrapolationConfig.extrapolateRight,
coef,
val,
leftEdgeOutput,
rightEdgeOutput,
x
);
}
return val;
}
/**
* Lets you map a value from one range to another using linear interpolation.
*
* @param value - A number from the `input` range that is going to be mapped to the `output` range.
* @param inputRange - An array of numbers specifying the input range of the interpolation.
* @param outputRange - An array of numbers specifying the output range of the interpolation.
* @param extrapolate - determines what happens when the `value` goes beyond the `input` range. Defaults to `Extrapolation.EXTEND` - {@link ExtrapolationType}.
* @returns A mapped value within the output range.
* @see https://docs.swmansion.com/react-native-reanimated/docs/utilities/interpolate
*/
export function interpolate(
x: number,
inputRange: readonly number[],
outputRange: readonly number[],
type?: ExtrapolationType
): number {
'worklet';
if (inputRange.length < 2 || outputRange.length < 2) {
throw new Error(
'[Reanimated] Interpolation input and output ranges should contain at least two values.'
);
}
const extrapolationConfig = validateType(type);
const length = inputRange.length;
const narrowedInput: InterpolationNarrowedInput = {
leftEdgeInput: inputRange[0],
rightEdgeInput: inputRange[1],
leftEdgeOutput: outputRange[0],
rightEdgeOutput: outputRange[1],
};
if (length > 2) {
if (x > inputRange[length - 1]) {
narrowedInput.leftEdgeInput = inputRange[length - 2];
narrowedInput.rightEdgeInput = inputRange[length - 1];
narrowedInput.leftEdgeOutput = outputRange[length - 2];
narrowedInput.rightEdgeOutput = outputRange[length - 1];
} else {
for (let i = 1; i < length; ++i) {
if (x <= inputRange[i]) {
narrowedInput.leftEdgeInput = inputRange[i - 1];
narrowedInput.rightEdgeInput = inputRange[i];
narrowedInput.leftEdgeOutput = outputRange[i - 1];
narrowedInput.rightEdgeOutput = outputRange[i];
break;
}
}
}
}
return internalInterpolate(x, narrowedInput, extrapolationConfig);
}
/**
* Lets you limit a value within a specified range.
*
* @param value - A number that will be returned as long as the provided value is in range between `min` and `max`.
* @param min - A number which will be returned when provided `value` is lower than `min`.
* @param max - A number which will be returned when provided `value` is higher than `max`.
* @returns A number between min and max bounds.
* @see https://docs.swmansion.com/react-native-reanimated/docs/utilities/clamp/
*/
export function clamp(value: number, min: number, max: number) {
'worklet';
return Math.min(Math.max(value, min), max);
}

View File

@@ -0,0 +1,10 @@
'use strict';
import type { SharedValue } from './commonTypes';
export function isSharedValue<T = unknown>(
value: unknown
): value is SharedValue<T> {
'worklet';
// We cannot use `in` operator here because `value` could be a HostObject and therefore we cast.
return (value as Record<string, unknown>)?._isReanimatedSharedValue === true;
}

View File

@@ -0,0 +1,251 @@
/* eslint-disable @typescript-eslint/no-namespace */
'use strict';
import type { ReactTestInstance } from 'react-test-renderer';
import type {
AnimatedComponentProps,
IAnimatedComponentInternal,
InitialComponentProps,
} from '../createAnimatedComponent/commonTypes';
import { isJest } from './PlatformChecker';
import type { DefaultStyle } from './hook/commonTypes';
declare global {
namespace jest {
interface Matchers<R> {
toHaveAnimatedStyle(
style: Record<string, unknown>[] | Record<string, unknown>,
config?: {
shouldMatchAllProps?: boolean;
}
): R;
}
}
}
const defaultFramerateConfig = {
fps: 60,
};
const getCurrentStyle = (component: TestComponent): DefaultStyle => {
const styleObject = component.props.style;
let currentStyle = {};
if (Array.isArray(styleObject)) {
styleObject.forEach((style) => {
currentStyle = {
...currentStyle,
...style,
};
});
} else {
currentStyle = {
...styleObject,
...component.props.jestAnimatedStyle?.value,
};
}
return currentStyle;
};
const checkEqual = <Value>(current: Value, expected: Value) => {
if (Array.isArray(expected)) {
if (!Array.isArray(current) || expected.length !== current.length) {
return false;
}
for (let i = 0; i < current.length; i++) {
if (!checkEqual(current[i], expected[i])) {
return false;
}
}
} else if (typeof current === 'object' && current) {
if (typeof expected !== 'object' || !expected) {
return false;
}
for (const property in expected) {
if (!checkEqual(current[property], expected[property])) {
return false;
}
}
} else {
return current === expected;
}
return true;
};
const findStyleDiff = (
current: DefaultStyle,
expected: DefaultStyle,
shouldMatchAllProps?: boolean
) => {
const diffs = [];
let isEqual = true;
let property: keyof DefaultStyle;
for (property in expected) {
if (!checkEqual(current[property], expected[property])) {
isEqual = false;
diffs.push({
property,
current: current[property],
expect: expected[property],
});
}
}
if (
shouldMatchAllProps &&
Object.keys(current).length !== Object.keys(expected).length
) {
isEqual = false;
// eslint-disable-next-line @typescript-eslint/no-shadow
let property: keyof DefaultStyle;
for (property in current) {
if (expected[property] === undefined) {
diffs.push({
property,
current: current[property],
expect: expected[property],
});
}
}
}
return { isEqual, diffs };
};
const compareStyle = (
component: TestComponent,
expectedStyle: DefaultStyle,
config: ToHaveAnimatedStyleConfig
) => {
if (!component.props.style) {
return { message: () => `Component doesn't have a style.`, pass: false };
}
const { shouldMatchAllProps } = config;
const currentStyle = getCurrentStyle(component);
const { isEqual, diffs } = findStyleDiff(
currentStyle,
expectedStyle,
shouldMatchAllProps
);
if (isEqual) {
return { message: () => 'ok', pass: true };
}
const currentStyleStr = JSON.stringify(currentStyle);
const expectedStyleStr = JSON.stringify(expectedStyle);
const differences = diffs
.map(
(diff) =>
`- '${diff.property}' should be ${JSON.stringify(
diff.expect
)}, but is ${JSON.stringify(diff.current)}`
)
.join('\n');
return {
message: () =>
`Expected: ${expectedStyleStr}\nReceived: ${currentStyleStr}\n\nDifferences:\n${differences}`,
pass: false,
};
};
let frameTime = Math.round(1000 / defaultFramerateConfig.fps);
const beforeTest = () => {
jest.useFakeTimers();
};
const afterTest = () => {
jest.runOnlyPendingTimers();
jest.useRealTimers();
};
export const withReanimatedTimer = (animationTest: () => void) => {
console.warn(
'This method is deprecated, you should define your own before and after test hooks to enable jest.useFakeTimers(). Check out the documentation for details on testing'
);
beforeTest();
animationTest();
afterTest();
};
export const advanceAnimationByTime = (time = frameTime) => {
console.warn(
'This method is deprecated, use jest.advanceTimersByTime directly'
);
jest.advanceTimersByTime(time);
jest.runOnlyPendingTimers();
};
export const advanceAnimationByFrame = (count: number) => {
console.warn(
'This method is deprecated, use jest.advanceTimersByTime directly'
);
jest.advanceTimersByTime(count * frameTime);
jest.runOnlyPendingTimers();
};
const requireFunction = isJest()
? require
: () => {
throw new Error(
'[Reanimated] `setUpTests` is available only in Jest environment.'
);
};
type ToHaveAnimatedStyleConfig = {
shouldMatchAllProps?: boolean;
};
export const setUpTests = (userFramerateConfig = {}) => {
let expect: jest.Expect = (global as typeof global & { expect: jest.Expect })
.expect;
if (expect === undefined) {
const expectModule = requireFunction('expect');
expect = expectModule;
// Starting from Jest 28, "expect" package uses named exports instead of default export.
// So, requiring "expect" package doesn't give direct access to "expect" function anymore.
// It gives access to the module object instead.
// We use this info to detect if the project uses Jest 28 or higher.
if (typeof expect === 'object') {
const jestGlobals = requireFunction('@jest/globals');
expect = jestGlobals.expect;
}
if (expect === undefined || expect.extend === undefined) {
expect = expectModule.default;
}
}
const framerateConfig = {
...defaultFramerateConfig,
...userFramerateConfig,
};
frameTime = Math.round(1000 / framerateConfig.fps);
expect.extend({
toHaveAnimatedStyle(
component: React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
IAnimatedComponentInternal,
expectedStyle: DefaultStyle,
config: ToHaveAnimatedStyleConfig = {}
) {
return compareStyle(component, expectedStyle, config);
},
});
};
type TestComponent = React.Component<
AnimatedComponentProps<InitialComponentProps> & {
jestAnimatedStyle?: { value: DefaultStyle };
}
>;
export const getAnimatedStyle = (component: ReactTestInstance) => {
return getCurrentStyle(
// This type assertion is needed to get type checking in the following
// functions since `ReactTestInstance` has its `props` defined as `any`.
component as unknown as TestComponent
);
};

View File

@@ -0,0 +1,24 @@
'use strict';
/*
* Stubbed for web, where we don't use this file;
*/
export function withReanimatedTimer() {
// NOOP
}
export function advanceAnimationByTime() {
// NOOP
}
export function advanceAnimationByFrame() {
// NOOP
}
export function setUpTests() {
// NOOP
}
export function getAnimatedStyle() {
// NOOP
}

View File

@@ -0,0 +1,317 @@
'use strict';
import {
isChromeDebugger,
isJest,
isWeb,
isWindowAvailable,
} from '../PlatformChecker';
import type { ShareableRef, Value3D, ValueRotation } from '../commonTypes';
import { SensorType } from '../commonTypes';
import type { WebSensor } from './WebSensor';
import { mockedRequestAnimationFrame } from '../mockedRequestAnimationFrame';
import type { WorkletRuntime } from '../runtimes';
// In Node.js environments (like when static rendering with Expo Router)
// requestAnimationFrame is unavailable, so we use our mock.
// It also has to be mocked for Jest purposes (see `initializeUIRuntime`).
const requestAnimationFrameImpl =
isJest() || !globalThis.requestAnimationFrame
? mockedRequestAnimationFrame
: globalThis.requestAnimationFrame;
export default class JSReanimated {
nextSensorId = 0;
sensors = new Map<number, WebSensor>();
platform?: Platform = undefined;
makeShareableClone<T>(): ShareableRef<T> {
throw new Error(
'[Reanimated] makeShareableClone should never be called in JSReanimated.'
);
}
scheduleOnUI<T>(worklet: ShareableRef<T>) {
// @ts-ignore web implementation has still not been updated after the rewrite, this will be addressed once the web implementation updates are ready
requestAnimationFrameImpl(worklet);
}
createWorkletRuntime(
_name: string,
_initializer: ShareableRef<() => void>
): WorkletRuntime {
throw new Error(
'[Reanimated] createWorkletRuntime is not available in JSReanimated.'
);
}
scheduleOnRuntime() {
throw new Error(
'[Reanimated] scheduleOnRuntime is not available in JSReanimated.'
);
}
registerEventHandler<T>(
_eventHandler: ShareableRef<T>,
_eventName: string,
_emitterReactTag: number
): number {
throw new Error(
'[Reanimated] registerEventHandler is not available in JSReanimated.'
);
}
unregisterEventHandler(_: number): void {
throw new Error(
'[Reanimated] unregisterEventHandler is not available in JSReanimated.'
);
}
enableLayoutAnimations() {
if (isWeb()) {
console.warn(
'[Reanimated] Layout Animations are not supported on web yet.'
);
} else if (isJest()) {
console.warn(
'[Reanimated] Layout Animations are no-ops when using Jest.'
);
} else if (isChromeDebugger()) {
console.warn(
'[Reanimated] Layout Animations are no-ops when using Chrome Debugger.'
);
} else {
console.warn(
'[Reanimated] Layout Animations are not supported on this configuration.'
);
}
}
configureLayoutAnimationBatch() {
// no-op
}
setShouldAnimateExitingForTag() {
// no-op
}
registerSensor(
sensorType: SensorType,
interval: number,
_iosReferenceFrame: number,
eventHandler: ShareableRef<(data: Value3D | ValueRotation) => void>
): number {
if (!isWindowAvailable()) {
// the window object is unavailable when building the server portion of a site that uses SSG
// this check is here to ensure that the server build won't fail
return -1;
}
if (this.platform === undefined) {
this.detectPlatform();
}
if (!(this.getSensorName(sensorType) in window)) {
// https://w3c.github.io/sensors/#secure-context
console.warn(
'[Reanimated] Sensor is not available.' +
(isWeb() && location.protocol !== 'https:'
? ' Make sure you use secure origin with `npx expo start --web --https`.'
: '') +
(this.platform === Platform.WEB_IOS
? ' For iOS web, you will also have to also grant permission in the browser: https://dev.to/li/how-to-requestpermission-for-devicemotion-and-deviceorientation-events-in-ios-13-46g2.'
: '')
);
return -1;
}
if (this.platform === undefined) {
this.detectPlatform();
}
const sensor: WebSensor = this.initializeSensor(sensorType, interval);
sensor.addEventListener(
'reading',
this.getSensorCallback(sensor, sensorType, eventHandler)
);
sensor.start();
this.sensors.set(this.nextSensorId, sensor);
return this.nextSensorId++;
}
getSensorCallback = (
sensor: WebSensor,
sensorType: SensorType,
eventHandler: ShareableRef<(data: Value3D | ValueRotation) => void>
) => {
switch (sensorType) {
case SensorType.ACCELEROMETER:
case SensorType.GRAVITY:
return () => {
let { x, y, z } = sensor;
// Web Android sensors have a different coordinate system than iOS
if (this.platform === Platform.WEB_ANDROID) {
[x, y, z] = [-x, -y, -z];
}
// TODO TYPESCRIPT on web ShareableRef is the value itself so we call it directly
(eventHandler as any)({ x, y, z, interfaceOrientation: 0 });
};
case SensorType.GYROSCOPE:
case SensorType.MAGNETIC_FIELD:
return () => {
const { x, y, z } = sensor;
// TODO TYPESCRIPT on web ShareableRef is the value itself so we call it directly
(eventHandler as any)({ x, y, z, interfaceOrientation: 0 });
};
case SensorType.ROTATION:
return () => {
let [qw, qx, qy, qz] = sensor.quaternion;
// Android sensors have a different coordinate system than iOS
if (this.platform === Platform.WEB_ANDROID) {
[qy, qz] = [qz, -qy];
}
// reference: https://stackoverflow.com/questions/5782658/extracting-yaw-from-a-quaternion
const yaw = -Math.atan2(
2.0 * (qy * qz + qw * qx),
qw * qw - qx * qx - qy * qy + qz * qz
);
const pitch = Math.sin(-2.0 * (qx * qz - qw * qy));
const roll = -Math.atan2(
2.0 * (qx * qy + qw * qz),
qw * qw + qx * qx - qy * qy - qz * qz
);
// TODO TYPESCRIPT on web ShareableRef is the value itself so we call it directly
(eventHandler as any)({
qw,
qx,
qy,
qz,
yaw,
pitch,
roll,
interfaceOrientation: 0,
});
};
}
};
unregisterSensor(id: number): void {
const sensor: WebSensor | undefined = this.sensors.get(id);
if (sensor !== undefined) {
sensor.stop();
this.sensors.delete(id);
}
}
subscribeForKeyboardEvents(_: ShareableRef<number>): number {
if (isWeb()) {
console.warn(
'[Reanimated] useAnimatedKeyboard is not available on web yet.'
);
} else if (isJest()) {
console.warn(
'[Reanimated] useAnimatedKeyboard is not available when using Jest.'
);
} else if (isChromeDebugger()) {
console.warn(
'[Reanimated] useAnimatedKeyboard is not available when using Chrome Debugger.'
);
} else {
console.warn(
'[Reanimated] useAnimatedKeyboard is not available on this configuration.'
);
}
return -1;
}
unsubscribeFromKeyboardEvents(_: number): void {
// noop
}
initializeSensor(sensorType: SensorType, interval: number): WebSensor {
const config =
interval <= 0
? { referenceFrame: 'device' }
: { frequency: 1000 / interval };
switch (sensorType) {
case SensorType.ACCELEROMETER:
return new window.Accelerometer(config);
case SensorType.GYROSCOPE:
return new window.Gyroscope(config);
case SensorType.GRAVITY:
return new window.GravitySensor(config);
case SensorType.MAGNETIC_FIELD:
return new window.Magnetometer(config);
case SensorType.ROTATION:
return new window.AbsoluteOrientationSensor(config);
}
}
getSensorName(sensorType: SensorType): string {
switch (sensorType) {
case SensorType.ACCELEROMETER:
return 'Accelerometer';
case SensorType.GRAVITY:
return 'GravitySensor';
case SensorType.GYROSCOPE:
return 'Gyroscope';
case SensorType.MAGNETIC_FIELD:
return 'Magnetometer';
case SensorType.ROTATION:
return 'AbsoluteOrientationSensor';
}
}
detectPlatform() {
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
if (userAgent === undefined) {
this.platform = Platform.UNKNOWN;
} else if (/iPad|iPhone|iPod/.test(userAgent)) {
this.platform = Platform.WEB_IOS;
} else if (/android/i.test(userAgent)) {
this.platform = Platform.WEB_ANDROID;
} else {
this.platform = Platform.WEB;
}
}
getViewProp<T>(
_viewTag: number,
_propName: string,
_component?: React.Component,
_callback?: (result: T) => void
): Promise<T> {
throw new Error(
'[Reanimated] getViewProp is not available in JSReanimated.'
);
}
configureProps() {
throw new Error(
'[Reanimated] configureProps is not available in JSReanimated.'
);
}
executeOnUIRuntimeSync<T, R>(_shareable: ShareableRef<T>): R {
throw new Error(
'[Reanimated] `executeOnUIRuntimeSync` is not available in JSReanimated.'
);
}
}
enum Platform {
WEB_IOS = 'web iOS',
WEB_ANDROID = 'web Android',
WEB = 'web',
UNKNOWN = 'unknown',
}
declare global {
interface Navigator {
userAgent?: string;
vendor?: string;
}
}

View File

@@ -0,0 +1,36 @@
'use strict';
export declare class WebSensor {
start: () => void;
stop: () => void;
addEventListener: (eventType: string, eventHandler: () => void) => void;
quaternion: [number, number, number, number];
x: number;
y: number;
z: number;
}
type configOptions =
| {
referenceFrame: string;
frequency?: undefined;
}
| {
frequency: number;
referenceFrame?: undefined;
};
interface Constructable<T> {
new (config: configOptions): T;
}
declare global {
interface Window {
Accelerometer: Constructable<WebSensor>;
GravitySensor: Constructable<WebSensor>;
Gyroscope: Constructable<WebSensor>;
Magnetometer: Constructable<WebSensor>;
AbsoluteOrientationSensor: Constructable<WebSensor>;
Sensor: Constructable<WebSensor>;
opera?: string;
}
}

View File

@@ -0,0 +1,191 @@
'use strict';
import JSReanimated from './JSReanimated';
import type { StyleProps } from '../commonTypes';
import type { AnimatedStyle } from '../helperTypes';
import { isWeb } from '../PlatformChecker';
import { PropsAllowlists } from '../../propsAllowlists';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let createReactDOMStyle: (style: any) => any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let createTransformValue: (transform: any) => any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let createTextShadowValue: (style: any) => void | string;
if (isWeb()) {
try {
createReactDOMStyle =
// eslint-disable-next-line @typescript-eslint/no-var-requires
require('react-native-web/dist/exports/StyleSheet/compiler/createReactDOMStyle').default;
} catch (e) {}
try {
// React Native Web 0.19+
createTransformValue =
// eslint-disable-next-line @typescript-eslint/no-var-requires
require('react-native-web/dist/exports/StyleSheet/preprocess').createTransformValue;
} catch (e) {}
try {
createTextShadowValue =
// eslint-disable-next-line @typescript-eslint/no-var-requires
require('react-native-web/dist/exports/StyleSheet/preprocess').createTextShadowValue;
} catch (e) {}
}
const reanimatedJS = new JSReanimated();
global._makeShareableClone = () => {
throw new Error(
'[Reanimated] _makeShareableClone should never be called in JSReanimated.'
);
};
global._scheduleOnJS = () => {
throw new Error(
'[Reanimated] _scheduleOnJS should never be called in JSReanimated.'
);
};
global._scheduleOnRuntime = () => {
throw new Error(
'[Reanimated] _scheduleOnRuntime should never be called in JSReanimated.'
);
};
interface JSReanimatedComponent {
previousStyle: StyleProps;
setNativeProps?: (style: StyleProps) => void;
style?: StyleProps;
props: Record<string, string | number>;
_touchableNode: {
setAttribute: (key: string, props: unknown) => void;
};
}
export interface ReanimatedHTMLElement extends HTMLElement {
previousStyle: StyleProps;
setNativeProps?: (style: StyleProps) => void;
props: Record<string, string | number>;
_touchableNode: {
setAttribute: (key: string, props: unknown) => void;
};
reanimatedDummy?: boolean;
removedAfterAnimation?: boolean;
}
export const _updatePropsJS = (
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
updates: StyleProps | AnimatedStyle<any>,
viewRef: { _component?: JSReanimatedComponent | ReanimatedHTMLElement },
isAnimatedProps?: boolean
): void => {
if (viewRef._component) {
const component = viewRef._component;
const [rawStyles] = Object.keys(updates).reduce(
(acc: [StyleProps, AnimatedStyle<any>], key) => {
const value = updates[key];
const index = typeof value === 'function' ? 1 : 0;
acc[index][key] = value;
return acc;
},
[{}, {}]
);
if (typeof component.setNativeProps === 'function') {
// This is the legacy way to update props on React Native Web <= 0.18.
// Also, some components (e.g. from react-native-svg) don't have styles
// and always provide setNativeProps function instead (even on React Native Web 0.19+).
setNativeProps(component, rawStyles, isAnimatedProps);
} else if (
createReactDOMStyle !== undefined &&
component.style !== undefined
) {
// React Native Web 0.19+ no longer provides setNativeProps function,
// so we need to update DOM nodes directly.
updatePropsDOM(component, rawStyles, isAnimatedProps);
} else if (Object.keys(component.props).length > 0) {
Object.keys(component.props).forEach((key) => {
if (!rawStyles[key]) {
return;
}
const dashedKey = key.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());
component._touchableNode.setAttribute(dashedKey, rawStyles[key]);
});
} else {
const componentName =
'className' in component ? component?.className : '';
console.warn(
`[Reanimated] It's not possible to manipulate the component ${componentName}`
);
}
}
};
const setNativeProps = (
component: JSReanimatedComponent | ReanimatedHTMLElement,
newProps: StyleProps,
isAnimatedProps?: boolean
): void => {
if (isAnimatedProps) {
const uiProps: Record<string, unknown> = {};
for (const key in newProps) {
if (isNativeProp(key)) {
uiProps[key] = newProps[key];
}
}
// Only update UI props directly on the component,
// other props can be updated as standard style props.
component.setNativeProps?.(uiProps);
}
const previousStyle = component.previousStyle ? component.previousStyle : {};
const currentStyle = { ...previousStyle, ...newProps };
component.previousStyle = currentStyle;
component.setNativeProps?.({ style: currentStyle });
};
const updatePropsDOM = (
component: JSReanimatedComponent | HTMLElement,
style: StyleProps,
isAnimatedProps?: boolean
): void => {
const previousStyle = (component as JSReanimatedComponent).previousStyle
? (component as JSReanimatedComponent).previousStyle
: {};
const currentStyle = { ...previousStyle, ...style };
(component as JSReanimatedComponent).previousStyle = currentStyle;
const domStyle = createReactDOMStyle(currentStyle);
if (Array.isArray(domStyle.transform) && createTransformValue !== undefined) {
domStyle.transform = createTransformValue(domStyle.transform);
}
if (
createTextShadowValue !== undefined &&
(domStyle.textShadowColor ||
domStyle.textShadowRadius ||
domStyle.textShadowOffset)
) {
domStyle.textShadow = createTextShadowValue({
textShadowColor: domStyle.textShadowColor,
textShadowOffset: domStyle.textShadowOffset,
textShadowRadius: domStyle.textShadowRadius,
});
}
for (const key in domStyle) {
if (isAnimatedProps) {
(component as HTMLElement).setAttribute(key, domStyle[key]);
} else {
(component.style as StyleProps)[key] = domStyle[key];
}
}
};
function isNativeProp(propName: string): boolean {
return !!PropsAllowlists.NATIVE_THREAD_PROPS_WHITELIST[propName];
}
export default reanimatedJS;

View File

@@ -0,0 +1,3 @@
'use strict';
declare module 'react-native-web/dist/exports/StyleSheet/compiler/createReactDOMStyle';
declare module 'react-native-web/dist/exports/StyleSheet/preprocess';

View File

@@ -0,0 +1,155 @@
'use strict';
import { withDelay } from '../../animation';
import type {
EntryExitAnimationFunction,
AnimationFunction,
LayoutAnimationFunction,
} from './commonTypes';
import { ReduceMotion } from '../../commonTypes';
import { getReduceMotionFromConfig } from '../../animation/util';
export class BaseAnimationBuilder {
durationV?: number;
delayV?: number;
reduceMotionV: ReduceMotion = ReduceMotion.System;
randomizeDelay = false;
callbackV?: (finished: boolean) => void;
static createInstance: <T extends typeof BaseAnimationBuilder>(
this: T
) => InstanceType<T>;
build = (): EntryExitAnimationFunction | LayoutAnimationFunction => {
throw new Error('[Reanimated] Unimplemented method in child class.');
};
/**
* Lets you adjust the animation duration. Can be chained alongside other [layout animation modifiers](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/glossary#layout-animation-modifier).
*
* @param durationMs - Length of the animation (in milliseconds).
*/
static duration<T extends typeof BaseAnimationBuilder>(
this: T,
durationMs: number
): InstanceType<T> {
const instance = this.createInstance();
return instance.duration(durationMs);
}
duration(durationMs: number): this {
this.durationV = durationMs;
return this;
}
/**
* Lets you adjust the delay before the animation starts (in milliseconds). Can be chained alongside other [layout animation modifiers](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/glossary#layout-animation-modifier).
*
* @param delayMs - Delay before the animation starts (in milliseconds).
*/
static delay<T extends typeof BaseAnimationBuilder>(
this: T,
delayMs: number
): InstanceType<T> {
const instance = this.createInstance();
return instance.delay(delayMs);
}
delay(delayMs: number): this {
this.delayV = delayMs;
return this;
}
/**
* The callback that will fire after the animation ends. Can be chained alongside other [layout animation modifiers](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/glossary#layout-animation-modifier).
*
* @param callback - Callback that will fire after the animation ends.
*/
static withCallback<T extends typeof BaseAnimationBuilder>(
this: T,
callback: (finished: boolean) => void
): InstanceType<T> {
const instance = this.createInstance();
return instance.withCallback(callback);
}
withCallback(callback: (finished: boolean) => void): this {
this.callbackV = callback;
return this;
}
/**
* Lets you adjust the behavior when the device's reduced motion accessibility setting is turned on. Can be chained alongside other [layout animation modifiers](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/glossary#layout-animation-modifier).
*
* @param reduceMotion - Determines how the animation responds to the device's reduced motion accessibility setting. Default to `ReduceMotion.System` - {@link ReduceMotion}.
*/
static reduceMotion<T extends typeof BaseAnimationBuilder>(
this: T,
reduceMotion: ReduceMotion
): InstanceType<T> {
const instance = this.createInstance();
return instance.reduceMotion(reduceMotion);
}
reduceMotion(reduceMotionV: ReduceMotion): this {
this.reduceMotionV = reduceMotionV;
return this;
}
// 300ms is the default animation duration. If any animation has different default has to override this method.
static getDuration(): number {
return 300;
}
getDuration(): number {
return this.durationV ?? 300;
}
/**
* @deprecated Use `.delay()` with `Math.random()` instead
*/
static randomDelay<T extends typeof BaseAnimationBuilder>(
this: T
): InstanceType<T> {
const instance = this.createInstance();
return instance.randomDelay();
}
randomDelay(): this {
this.randomizeDelay = true;
return this;
}
// when randomizeDelay is set to true, randomize delay between 0 and provided value (or 1000ms if delay is not provided)
getDelay(): number {
return this.randomizeDelay
? Math.random() * (this.delayV ?? 1000)
: this.delayV ?? 0;
}
getReduceMotion(): ReduceMotion {
return this.reduceMotionV;
}
getDelayFunction(): AnimationFunction {
const isDelayProvided = this.randomizeDelay || this.delayV;
const reduceMotion = this.getReduceMotion();
return isDelayProvided
? (delay, animation) => {
'worklet';
return withDelay(delay, animation, reduceMotion);
}
: (_, animation) => {
'worklet';
animation.reduceMotion = getReduceMotionFromConfig(reduceMotion);
return animation;
};
}
static build<T extends typeof BaseAnimationBuilder>(
this: T
): EntryExitAnimationFunction | LayoutAnimationFunction {
const instance = this.createInstance();
return instance.build();
}
}

View File

@@ -0,0 +1,280 @@
'use strict';
import { withTiming, withSpring } from '../../animation';
import type {
AnimationFunction,
BaseBuilderAnimationConfig,
LayoutAnimationAndConfig,
} from './commonTypes';
import type { EasingFunction } from '../../Easing';
import { BaseAnimationBuilder } from './BaseAnimationBuilder';
import type { StyleProps } from '../../commonTypes';
import { assertEasingIsWorklet } from '../../animation/util';
export class ComplexAnimationBuilder extends BaseAnimationBuilder {
easingV?: EasingFunction;
rotateV?: string;
type?: AnimationFunction;
dampingV?: number;
dampingRatioV?: number;
massV?: number;
stiffnessV?: number;
overshootClampingV?: number;
restDisplacementThresholdV?: number;
restSpeedThresholdV?: number;
initialValues?: StyleProps;
static createInstance: <T extends typeof BaseAnimationBuilder>(
this: T
) => InstanceType<T>;
/**
* Lets you change the easing curve of the animation. Can be chained alongside other [layout animation modifiers](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/glossary#layout-animation-modifier).
*
* @param easingFunction - An easing function which defines the animation curve.
*/
static easing<T extends typeof ComplexAnimationBuilder>(
this: T,
easingFunction: EasingFunction
) {
const instance = this.createInstance();
return instance.easing(easingFunction);
}
easing(easingFunction: EasingFunction): this {
if (__DEV__) {
assertEasingIsWorklet(easingFunction);
}
this.easingV = easingFunction;
return this;
}
/**
* Lets you rotate the element. Can be chained alongside other [layout animation modifiers](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/glossary#layout-animation-modifier).
*
* @param degree - The rotation degree.
*/
static rotate<T extends typeof ComplexAnimationBuilder>(
this: T,
degree: string
) {
const instance = this.createInstance();
return instance.rotate(degree);
}
rotate(degree: string): this {
this.rotateV = degree;
return this;
}
/**
* Enables the spring-based animation configuration. Can be chained alongside other [layout animation modifiers](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/glossary#layout-animation-modifier).
*
* @param duration - An optional duration of the spring animation (in milliseconds).
*/
static springify<T extends typeof ComplexAnimationBuilder>(
this: T,
duration?: number
): ComplexAnimationBuilder {
const instance = this.createInstance();
return instance.springify(duration);
}
springify(duration?: number): this {
this.durationV = duration;
this.type = withSpring as AnimationFunction;
return this;
}
/**
* Lets you adjust the spring animation damping ratio. Can be chained alongside other [layout animation modifiers](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/glossary#layout-animation-modifier).
*
* @param dampingRatio - How damped the spring is.
*/
static dampingRatio<T extends typeof ComplexAnimationBuilder>(
this: T,
dampingRatio: number
) {
const instance = this.createInstance();
return instance.dampingRatio(dampingRatio);
}
dampingRatio(value: number): this {
this.dampingRatioV = value;
return this;
}
/**
* Lets you adjust the spring animation damping. Can be chained alongside other [layout animation modifiers](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/glossary#layout-animation-modifier).
*
* @param value - Decides how quickly a spring stops moving. Higher damping means the spring will come to rest faster.
*/
static damping<T extends typeof ComplexAnimationBuilder>(
this: T,
damping: number
) {
const instance = this.createInstance();
return instance.damping(damping);
}
damping(damping: number): this {
this.dampingV = damping;
return this;
}
/**
* Lets you adjust the spring animation mass. Can be chained alongside other [layout animation modifiers](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/glossary#layout-animation-modifier).
*
* @param mass - The weight of the spring. Reducing this value makes the animation faster.
*/
static mass<T extends typeof ComplexAnimationBuilder>(this: T, mass: number) {
const instance = this.createInstance();
return instance.mass(mass);
}
mass(mass: number): this {
this.massV = mass;
return this;
}
/**
* Lets you adjust the stiffness of the spring animation. Can be chained alongside other [layout animation modifiers](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/glossary#layout-animation-modifier).
*
* @param stiffness - How bouncy the spring is.
*/
static stiffness<T extends typeof ComplexAnimationBuilder>(
this: T,
stiffness: number
) {
const instance = this.createInstance();
return instance.stiffness(stiffness);
}
stiffness(stiffness: number): this {
this.stiffnessV = stiffness;
return this;
}
/**
* Lets you adjust overshoot clamping of the spring. Can be chained alongside other [layout animation modifiers](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/glossary#layout-animation-modifier).
*
* @param overshootClamping - Whether a spring can bounce over the final position.
*/
static overshootClamping<T extends typeof ComplexAnimationBuilder>(
this: T,
overshootClamping: number
) {
const instance = this.createInstance();
return instance.overshootClamping(overshootClamping);
}
overshootClamping(overshootClamping: number): this {
this.overshootClampingV = overshootClamping;
return this;
}
/**
* Lets you adjust the rest displacement threshold of the spring animation. Can be chained alongside other [layout animation modifiers](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/glossary#layout-animation-modifier).
*
* @param restDisplacementThreshold - The displacement below which the spring will snap to the designated position without further oscillations.
*/
static restDisplacementThreshold<T extends typeof ComplexAnimationBuilder>(
this: T,
restDisplacementThreshold: number
) {
const instance = this.createInstance();
return instance.restDisplacementThreshold(restDisplacementThreshold);
}
restDisplacementThreshold(restDisplacementThreshold: number) {
this.restDisplacementThresholdV = restDisplacementThreshold;
return this;
}
/**
* Lets you adjust the rest speed threshold of the spring animation. Can be chained alongside other [layout animation modifiers](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/glossary#layout-animation-modifier).
*
* @param restSpeedThreshold - The speed in pixels per second from which the spring will snap to the designated position without further oscillations.
*/
static restSpeedThreshold<T extends typeof ComplexAnimationBuilder>(
this: T,
restSpeedThreshold: number
) {
const instance = this.createInstance();
return instance.restSpeedThreshold(restSpeedThreshold);
}
restSpeedThreshold(restSpeedThreshold: number): this {
this.restSpeedThresholdV = restSpeedThreshold;
return this;
}
/**
* Lets you override the initial config of the animation
*
* @param values - An object containing the styles to override.
*/
static withInitialValues<T extends typeof ComplexAnimationBuilder>(
this: T,
values: StyleProps
) {
const instance = this.createInstance();
return instance.withInitialValues(values);
}
withInitialValues(values: StyleProps): this {
this.initialValues = values;
return this;
}
getAnimationAndConfig(): LayoutAnimationAndConfig {
const duration = this.durationV;
const easing = this.easingV;
const rotate = this.rotateV;
const type = this.type ? this.type : (withTiming as AnimationFunction);
const damping = this.dampingV;
const dampingRatio = this.dampingRatioV;
const mass = this.massV;
const stiffness = this.stiffnessV;
const overshootClamping = this.overshootClampingV;
const restDisplacementThreshold = this.restDisplacementThresholdV;
const restSpeedThreshold = this.restSpeedThresholdV;
const animation = type;
const config: BaseBuilderAnimationConfig = {};
function maybeSetConfigValue<Key extends keyof BaseBuilderAnimationConfig>(
value: BaseBuilderAnimationConfig[Key],
variableName: Key
) {
if (value) {
config[variableName] = value;
}
}
if (type === withTiming) {
maybeSetConfigValue(easing, 'easing');
}
(
[
{ variableName: 'damping', value: damping },
{ variableName: 'dampingRatio', value: dampingRatio },
{ variableName: 'mass', value: mass },
{ variableName: 'stiffness', value: stiffness },
{ variableName: 'overshootClamping', value: overshootClamping },
{
variableName: 'restDisplacementThreshold',
value: restDisplacementThreshold,
},
{ variableName: 'restSpeedThreshold', value: restSpeedThreshold },
{ variableName: 'duration', value: duration },
{ variableName: 'rotate', value: rotate },
] as const
).forEach(({ value, variableName }) =>
maybeSetConfigValue(value, variableName)
);
return [animation, config];
}
}

View File

@@ -0,0 +1,313 @@
'use strict';
import type { EasingFunction } from '../../Easing';
import { Easing } from '../../Easing';
import { withDelay, withSequence, withTiming } from '../../animation';
import type {
AnimationFunction,
EntryExitAnimationFunction,
IEntryExitAnimationBuilder,
KeyframeProps,
StylePropsWithArrayTransform,
} from './commonTypes';
import type { StyleProps } from '../../commonTypes';
import type { TransformArrayItem } from '../../helperTypes';
import { ReduceMotion } from '../../commonTypes';
import {
assertEasingIsWorklet,
getReduceMotionFromConfig,
} from '../../animation/util';
interface KeyframePoint {
duration: number;
value: number | string;
easing?: EasingFunction;
}
interface ParsedKeyframesDefinition {
initialValues: StyleProps;
keyframes: Record<string, KeyframePoint[]>;
}
class InnerKeyframe implements IEntryExitAnimationBuilder {
durationV?: number;
delayV?: number;
reduceMotionV: ReduceMotion = ReduceMotion.System;
callbackV?: (finished: boolean) => void;
definitions: Record<string, KeyframeProps>;
/*
Keyframe definition should be passed in the constructor as the map
which keys are between range 0 - 100 (%) and correspond to the point in the animation progress.
*/
constructor(definitions: Record<string, KeyframeProps>) {
this.definitions = definitions;
}
private parseDefinitions(): ParsedKeyframesDefinition {
/*
Each style property contain an array with all their key points:
value, duration of transition to that value, and optional easing function (defaults to Linear)
*/
const parsedKeyframes: Record<string, KeyframePoint[]> = {};
/*
Parsing keyframes 'from' and 'to'.
*/
if (this.definitions.from) {
if (this.definitions['0']) {
throw new Error(
"[Reanimated] You cannot provide both keyframe 0 and 'from' as they both specified initial values."
);
}
this.definitions['0'] = this.definitions.from;
delete this.definitions.from;
}
if (this.definitions.to) {
if (this.definitions['100']) {
throw new Error(
"[Reanimated] You cannot provide both keyframe 100 and 'to' as they both specified values at the end of the animation."
);
}
this.definitions['100'] = this.definitions.to;
delete this.definitions.to;
}
/*
One of the assumptions is that keyframe 0 is required to properly set initial values.
Every other keyframe should contain properties from the set provided as initial values.
*/
if (!this.definitions['0']) {
throw new Error(
"[Reanimated] Please provide 0 or 'from' keyframe with initial state of your object."
);
}
const initialValues: StyleProps = this.definitions['0'] as StyleProps;
/*
Initialize parsedKeyframes for properties provided in initial keyframe
*/
Object.keys(initialValues).forEach((styleProp: string) => {
if (styleProp === 'transform') {
if (!Array.isArray(initialValues.transform)) {
return;
}
initialValues.transform.forEach((transformStyle, index) => {
Object.keys(transformStyle).forEach((transformProp: string) => {
parsedKeyframes[makeKeyframeKey(index, transformProp)] = [];
});
});
} else {
parsedKeyframes[styleProp] = [];
}
});
const duration: number = this.durationV ? this.durationV : 500;
const animationKeyPoints: Array<string> = Array.from(
Object.keys(this.definitions)
);
const getAnimationDuration = (
key: string,
currentKeyPoint: number
): number => {
const maxDuration = (currentKeyPoint / 100) * duration;
const currentDuration = parsedKeyframes[key].reduce(
(acc: number, value: KeyframePoint) => acc + value.duration,
0
);
return maxDuration - currentDuration;
};
/*
Other keyframes can't contain properties that were not specified in initial keyframe.
*/
const addKeyPoint = ({
key,
value,
currentKeyPoint,
easing,
}: {
key: string;
value: string | number;
currentKeyPoint: number;
easing?: EasingFunction;
}): void => {
if (!(key in parsedKeyframes)) {
throw new Error(
"[Reanimated] Keyframe can contain only that set of properties that were provide with initial values (keyframe 0 or 'from')"
);
}
if (__DEV__ && easing) {
assertEasingIsWorklet(easing);
}
parsedKeyframes[key].push({
duration: getAnimationDuration(key, currentKeyPoint),
value,
easing,
});
};
animationKeyPoints
.filter((value: string) => parseInt(value) !== 0)
.sort((a: string, b: string) => parseInt(a) - parseInt(b))
.forEach((keyPoint: string) => {
if (parseInt(keyPoint) < 0 || parseInt(keyPoint) > 100) {
throw new Error(
'[Reanimated] Keyframe should be in between range 0 - 100.'
);
}
const keyframe: KeyframeProps = this.definitions[keyPoint];
const easing = keyframe.easing;
delete keyframe.easing;
const addKeyPointWith = (key: string, value: string | number) =>
addKeyPoint({
key,
value,
currentKeyPoint: parseInt(keyPoint),
easing,
});
Object.keys(keyframe).forEach((key: string) => {
if (key === 'transform') {
if (!Array.isArray(keyframe.transform)) {
return;
}
keyframe.transform.forEach((transformStyle, index) => {
Object.keys(transformStyle).forEach((transformProp: string) => {
addKeyPointWith(
makeKeyframeKey(index, transformProp),
transformStyle[
transformProp as keyof typeof transformStyle
] as number | string // Here we assume that user has passed props of proper type.
// I don't think it's worthwhile to check if he passed i.e. `Animated.Node`.
);
});
});
} else {
addKeyPointWith(key, keyframe[key]);
}
});
});
return { initialValues, keyframes: parsedKeyframes };
}
duration(durationMs: number): InnerKeyframe {
this.durationV = durationMs;
return this;
}
delay(delayMs: number): InnerKeyframe {
this.delayV = delayMs;
return this;
}
withCallback(callback: (finsihed: boolean) => void): InnerKeyframe {
this.callbackV = callback;
return this;
}
reduceMotion(reduceMotionV: ReduceMotion): this {
this.reduceMotionV = reduceMotionV;
return this;
}
private getDelayFunction(): AnimationFunction {
const delay = this.delayV;
const reduceMotion = this.reduceMotionV;
return delay
? // eslint-disable-next-line @typescript-eslint/no-shadow
(delay, animation) => {
'worklet';
return withDelay(delay, animation, reduceMotion);
}
: (_, animation) => {
'worklet';
animation.reduceMotion = getReduceMotionFromConfig(reduceMotion);
return animation;
};
}
build = (): EntryExitAnimationFunction => {
const delay = this.delayV;
const delayFunction = this.getDelayFunction();
const { keyframes, initialValues } = this.parseDefinitions();
const callback = this.callbackV;
return () => {
'worklet';
const animations: StylePropsWithArrayTransform = {};
/*
For each style property, an animations sequence is created that corresponds with its key points.
Transform style properties require special handling because of their nested structure.
*/
const addAnimation = (key: string) => {
const keyframePoints = keyframes[key];
// in case if property was only passed as initial value
if (keyframePoints.length === 0) {
return;
}
const animation = delayFunction(
delay,
keyframePoints.length === 1
? withTiming(keyframePoints[0].value, {
duration: keyframePoints[0].duration,
easing: keyframePoints[0].easing
? keyframePoints[0].easing
: Easing.linear,
})
: withSequence(
...keyframePoints.map((keyframePoint: KeyframePoint) =>
withTiming(keyframePoint.value, {
duration: keyframePoint.duration,
easing: keyframePoint.easing
? keyframePoint.easing
: Easing.linear,
})
)
)
);
if (key.includes('transform')) {
if (!('transform' in animations)) {
animations.transform = [];
}
animations.transform!.push(<TransformArrayItem>{
[key.split(':')[1]]: animation,
});
} else {
animations[key] = animation;
}
};
Object.keys(initialValues).forEach((key: string) => {
if (key.includes('transform')) {
initialValues[key].forEach(
(transformProp: Record<string, number | string>, index: number) => {
Object.keys(transformProp).forEach((transformPropKey: string) => {
addAnimation(makeKeyframeKey(index, transformPropKey));
});
}
);
} else {
addAnimation(key);
}
});
return {
animations,
initialValues,
callback,
};
};
};
}
function makeKeyframeKey(index: number, transformProp: string) {
'worklet';
return `${index}_transform:${transformProp}`;
}
// TODO TYPESCRIPT This is a temporary type to get rid of .d.ts file.
export declare class ReanimatedKeyframe {
constructor(definitions: Record<string, KeyframeProps>);
duration(durationMs: number): ReanimatedKeyframe;
delay(delayMs: number): ReanimatedKeyframe;
reduceMotion(reduceMotionV: ReduceMotion): ReanimatedKeyframe;
withCallback(callback: (finished: boolean) => void): ReanimatedKeyframe;
}
// TODO TYPESCRIPT This temporary cast is to get rid of .d.ts file.
export const Keyframe = InnerKeyframe as unknown as typeof ReanimatedKeyframe;

View File

@@ -0,0 +1,169 @@
'use strict';
import type { TransformArrayItem } from '../../helperTypes';
import type { EasingFunction } from '../../Easing';
import type { ShareableRef, StyleProps } from '../../commonTypes';
export type LayoutAnimationsOptions =
| 'originX'
| 'originY'
| 'width'
| 'height'
| 'borderRadius'
| 'globalOriginX'
| 'globalOriginY';
type CurrentLayoutAnimationsValues = {
[K in LayoutAnimationsOptions as `current${Capitalize<string & K>}`]: number;
};
type TargetLayoutAnimationsValues = {
[K in LayoutAnimationsOptions as `target${Capitalize<string & K>}`]: number;
};
interface WindowDimensions {
windowWidth: number;
windowHeight: number;
}
export interface KeyframeProps extends StyleProps {
easing?: EasingFunction;
}
export type LayoutAnimation = {
initialValues: StyleProps;
animations: StyleProps;
callback?: (finished: boolean) => void;
};
export type AnimationFunction = (a?: any, b?: any, c?: any) => any; // this is just a temporary mock
export type EntryAnimationsValues = TargetLayoutAnimationsValues &
WindowDimensions;
export type ExitAnimationsValues = CurrentLayoutAnimationsValues &
WindowDimensions;
export type EntryExitAnimationFunction =
| ((targetValues: EntryAnimationsValues) => LayoutAnimation)
| ((targetValues: ExitAnimationsValues) => LayoutAnimation)
| (() => LayoutAnimation);
export type AnimationConfigFunction<T> = (targetValues: T) => LayoutAnimation;
export type LayoutAnimationsValues = CurrentLayoutAnimationsValues &
TargetLayoutAnimationsValues &
WindowDimensions;
export interface SharedTransitionAnimationsValues
extends LayoutAnimationsValues {
currentTransformMatrix: number[];
targetTransformMatrix: number[];
}
export type SharedTransitionAnimationsFunction = (
values: SharedTransitionAnimationsValues
) => LayoutAnimation;
export enum LayoutAnimationType {
ENTERING = 1,
EXITING = 2,
LAYOUT = 3,
SHARED_ELEMENT_TRANSITION = 4,
SHARED_ELEMENT_TRANSITION_PROGRESS = 5,
}
export type LayoutAnimationFunction = (
targetValues: LayoutAnimationsValues
) => LayoutAnimation;
export type LayoutAnimationStartFunction = (
tag: number,
type: LayoutAnimationType,
yogaValues: Partial<SharedTransitionAnimationsValues>,
config: LayoutAnimationFunction
) => void;
export interface ILayoutAnimationBuilder {
build: () => LayoutAnimationFunction;
}
export interface BaseLayoutAnimationConfig {
duration?: number;
easing?: EasingFunction;
type?: AnimationFunction;
damping?: number;
dampingRatio?: number;
mass?: number;
stiffness?: number;
overshootClamping?: number;
restDisplacementThreshold?: number;
restSpeedThreshold?: number;
}
export interface BaseBuilderAnimationConfig extends BaseLayoutAnimationConfig {
rotate?: number | string;
}
export type LayoutAnimationAndConfig = [
AnimationFunction,
BaseBuilderAnimationConfig
];
export interface IEntryExitAnimationBuilder {
build: () => EntryExitAnimationFunction;
}
export interface IEntryAnimationBuilder {
build: () => AnimationConfigFunction<EntryAnimationsValues>;
}
export interface IExitAnimationBuilder {
build: () => AnimationConfigFunction<ExitAnimationsValues>;
}
export type ProgressAnimationCallback = (
viewTag: number,
progress: number
) => void;
export type ProgressAnimation = (
viewTag: number,
values: SharedTransitionAnimationsValues,
progress: number
) => void;
export type CustomProgressAnimation = (
values: SharedTransitionAnimationsValues,
progress: number
) => StyleProps;
/**
* Used to configure the `.defaultTransitionType()` shared transition modifier.
* @experimental
*/
export enum SharedTransitionType {
ANIMATION = 'animation',
PROGRESS_ANIMATION = 'progressAnimation',
}
export type EntryExitAnimationsValues =
| EntryAnimationsValues
| ExitAnimationsValues;
export type StylePropsWithArrayTransform = StyleProps & {
transform?: TransformArrayItem[];
};
export interface LayoutAnimationBatchItem {
viewTag: number;
type: LayoutAnimationType;
config:
| ShareableRef<
| Keyframe
| LayoutAnimationFunction
| SharedTransitionAnimationsFunction
| ProgressAnimationCallback
>
| undefined;
sharedTransitionTag?: string;
}

Some files were not shown because too many files have changed in this diff Show More