chore: update map

This commit is contained in:
2026-02-19 08:55:45 +01:00
parent 3308adf1ea
commit 900c54c20b
1845 changed files with 3677 additions and 84269 deletions
+76 -6
View File
@@ -1,11 +1,81 @@
'use strict';
export { ReanimatedFlatList as FlatList } from './component/FlatList';
export { AnimatedImage as Image } from './component/Image';
export { AnimatedScrollView as ScrollView } from './component/ScrollView';
export { AnimatedText as Text } from './component/Text';
export { AnimatedView as View } from './component/View';
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';
export { createAnimatedComponent } from './createAnimatedComponent';
/**
* @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>;
-168
View File
@@ -1,168 +0,0 @@
'use strict';
import { ReanimatedError } from './common';
/**
* 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 ReanimatedError('Bezier x values must be in [0, 1] range.');
}
if (mX1 === mY1 && mX2 === mY2) {
return LinearEasing;
}
const sampleValues = new Array(kSplineTableSize);
// Precompute samples table
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);
};
}
-702
View File
@@ -1,702 +0,0 @@
'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 */
interface RGB {
r: number;
g: number;
b: number;
}
interface HSV {
h: number;
s: number;
v: number;
}
const NUMBER: string = '[-+]?\\d*\\.?\\d+';
const PERCENTAGE = NUMBER + '%';
function call(...args: (RegExp | string)[]) {
return '\\(\\s*(' + args.join(')\\s*,?\\s*(') + ')\\s*\\)';
}
function callWithSlashSeparator(...args: (RegExp | string)[]) {
return (
'\\(\\s*(' +
args.slice(0, args.length - 1).join(')\\s*,?\\s*(') +
')\\s*/\\s*(' +
args[args.length - 1] +
')\\s*\\)'
);
}
function commaSeparatedCall(...args: (RegExp | string)[]) {
return '\\(\\s*(' + args.join(')\\s*,\\s*(') + ')\\s*\\)';
}
const MATCHERS = {
rgb: new RegExp('rgb' + call(NUMBER, NUMBER, NUMBER)),
rgba: new RegExp(
'rgba(' +
commaSeparatedCall(NUMBER, NUMBER, NUMBER, NUMBER) +
'|' +
callWithSlashSeparator(NUMBER, NUMBER, NUMBER, NUMBER) +
')'
),
hsl: new RegExp('hsl' + call(NUMBER, PERCENTAGE, PERCENTAGE)),
hsla: new RegExp(
'hsla(' +
commaSeparatedCall(NUMBER, PERCENTAGE, PERCENTAGE, NUMBER) +
'|' +
callWithSlashSeparator(NUMBER, PERCENTAGE, PERCENTAGE, NUMBER) +
')'
),
hwb: new RegExp('hwb' + call(NUMBER, PERCENTAGE, PERCENTAGE)),
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 hwbToRgb(h: number, w: number, b: number): number {
'worklet';
if (w + b >= 1) {
const gray = Math.round((w * 255) / (w + b));
return (gray << 24) | (gray << 16) | (gray << 8);
}
const red = hue2rgb(0, 1, h + 1 / 3) * (1 - w - b) + w;
const green = hue2rgb(0, 1, h) * (1 - w - b) + w;
const blue = hue2rgb(0, 1, h - 1 / 3) * (1 - w - b) + w;
return (
(Math.round(red * 255) << 24) |
(Math.round(green * 255) << 16) |
(Math.round(blue * 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;
}
export function clampRGBA(RGBA: ParsedColorArray): void {
'worklet';
for (let i = 0; i < 4; i++) {
RGBA[i] = Math.max(0, Math.min(RGBA[i], 1));
}
}
const names: Record<string, number | undefined> = {
transparent: undefined,
/* spell-checker: disable */
// 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,
/* spell-checker: enable */
};
// copied from react-native/Libraries/Components/View/ReactNativeStyleAttributes
export const ColorProperties = [
'backgroundColor',
'borderBottomColor',
'borderColor',
'borderLeftColor',
'borderRightColor',
'borderTopColor',
'borderStartColor',
'borderEndColor',
'borderBlockColor',
'borderBlockEndColor',
'borderBlockStartColor',
'color',
'outlineColor',
'placeholderTextColor',
'shadowColor',
'textDecorationColor',
'tintColor',
'textShadowColor',
'overlayColor',
// SVG color properties
'fill',
'floodColor',
'lightingColor',
'stopColor',
'stroke',
];
export function normalizeColor(color: unknown): number | null | undefined {
'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 (color in names) {
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))) {
// rgba(R G B / A) notation
if (match[6] !== undefined) {
return (
((parse255(match[6]) << 24) | // r
(parse255(match[7]) << 16) | // g
(parse255(match[8]) << 8) | // b
parse1(match[9])) >>> // a
0
);
}
// rgba(R, G, B, A) notation
return (
((parse255(match[2]) << 24) | // r
(parse255(match[3]) << 16) | // g
(parse255(match[4]) << 8) | // b
parse1(match[5])) >>> // 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))) {
// hsla(H S L / A) notation
if (match[6] !== undefined) {
return (
(hslToRgb(
parse360(match[6]), // h
parsePercentage(match[7]), // s
parsePercentage(match[8]) // l
) |
parse1(match[9])) >>> // a
0
);
}
// hsla(H, S, L, A) notation
return (
(hslToRgb(
parse360(match[2]), // h
parsePercentage(match[3]), // s
parsePercentage(match[4]) // l
) |
parse1(match[5])) >>> // a
0
);
}
if ((match = MATCHERS.hwb.exec(color))) {
return (
(hwbToRgb(
parse360(match[1]), // h
parsePercentage(match[2]), // w
parsePercentage(match[3]) // b
) |
0x000000ff) >>> // 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;
};
export const rgbaColor = (
r: number,
g: number,
b: number,
alpha = 1
): number | string => {
'worklet';
// Round alpha to 3 decimal places to avoid floating point precision issues
const safeAlpha = Math.round(alpha * 1000) / 1000;
return `rgba(${r}, ${g}, ${b}, ${safeAlpha})`;
};
/**
* @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);
};
export function processColorInitially(
color: unknown
): number | null | undefined {
'worklet';
if (color === null || color === undefined) {
return color;
}
let colorNumber: number;
if (typeof color === 'number') {
colorNumber = color;
} else {
const normalizedColor = normalizeColor(color);
if (typeof normalizedColor !== 'number') {
return normalizedColor;
}
colorNumber = normalizedColor;
}
return ((colorNumber << 24) | (colorNumber >>> 8)) >>> 0; // alpha rgb
}
export function isColor(value: unknown): boolean {
'worklet';
if (typeof value !== 'string') {
return false;
}
return processColorInitially(value) != null;
}
export type ParsedColorArray = [number, number, number, number];
export function convertToRGBA(color: unknown): ParsedColorArray {
'worklet';
const processedColor = processColorInitially(color)!; // alpha rgb;
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';
const alpha = RGBA[3] < 0.001 ? 0 : RGBA[3];
return `rgba(${Math.round(RGBA[0] * 255)}, ${Math.round(
RGBA[1] * 255
)}, ${Math.round(RGBA[2] * 255)}, ${alpha})`;
}
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;
}
+84 -38
View File
@@ -1,41 +1,87 @@
'use strict';
import { executeOnUIRuntimeSync } from 'react-native-worklets';
import type { LoggerConfig } from './common';
import {
getLoggerConfig,
SHOULD_BE_USE_WEB,
updateLoggerConfig,
} from './common';
/** @deprecated This function is a no-op in Reanimated 4. */
export function addWhitelistedNativeProps(
_props: Record<string, boolean>
): void {
// Do nothing. This is just for backward compatibility.
}
/** @deprecated This function is a no-op in Reanimated 4. */
export function addWhitelistedUIProps(_props: Record<string, boolean>): void {
// Do nothing. This is just for backward compatibility.
}
/**
* Updates Reanimated logger config with the user-provided configuration. Will
* affect Reanimated code executed after call to this function so it should be
* called before any Reanimated code is executed to take effect. Each call to
* this function will override the previous configuration (it's recommended to
* call it only once).
*
* @param config - The new logger configuration to apply.
*/
export function configureReanimatedLogger(config: LoggerConfig) {
// Get the current config from the React runtime (to have a single source of truth)
const currentConfig = getLoggerConfig();
// Update the configuration object in the React runtime
updateLoggerConfig(currentConfig, config);
// Register the updated configuration in the UI runtime
if (!SHOULD_BE_USE_WEB) {
executeOnUIRuntimeSync(updateLoggerConfig)(currentConfig, config);
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.`
);
}
}
}
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();
-311
View File
@@ -1,311 +0,0 @@
'use strict';
import { Bezier } from './Bezier';
import type { EasingFunction } from './commonTypes';
/**
* 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 EasingFunctionFactory = { factory: () => EasingFunction };
/**
* 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
): EasingFunctionFactory {
'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 EasingNameSymbol = Symbol('easingName');
for (const [easingName, easing] of Object.entries(EasingObject)) {
Object.defineProperty(easing, EasingNameSymbol, {
value: easingName,
configurable: false,
enumerable: false,
writable: false,
});
}
export const Easing = EasingObject;
@@ -1,23 +0,0 @@
'use strict';
import { logger } from './common';
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 {
logger.warn(
'`createAnimatedPropAdapter` is no longer necessary in Reanimated 4 and will be removed in next version. Please remove this call from your code and pass the adapter function directly.'
);
return adapter;
}
@@ -1,289 +0,0 @@
/* eslint-disable @typescript-eslint/no-empty-function */
'use strict';
import type {
IWorkletsModule,
SerializableRef,
WorkletFunction,
} from 'react-native-worklets';
import { executeOnUIRuntimeSync, WorkletsModule } from 'react-native-worklets';
import {
ReanimatedError,
registerReanimatedError,
SHOULD_BE_USE_WEB,
} from '../common';
import type {
LayoutAnimationBatchItem,
ShadowNodeWrapper,
StyleProps,
Value3D,
ValueRotation,
WrapperRef,
} from '../commonTypes';
import type {
CSSAnimationUpdates,
NormalizedCSSAnimationKeyframesConfig,
NormalizedCSSTransitionConfig,
} from '../css/native';
import { getShadowNodeWrapperFromRef } from '../fabricUtils';
import { checkCppVersion } from '../platform-specific/checkCppVersion';
import { jsVersion } from '../platform-specific/jsVersion';
import { assertWorkletsVersion } from '../platform-specific/workletsVersion';
import { ReanimatedTurboModule } from '../specs';
import type {
IReanimatedModule,
ReanimatedModuleProxy,
} from './reanimatedModuleProxy';
export function createNativeReanimatedModule(): IReanimatedModule {
return new NativeReanimatedModule();
}
function assertSingleReanimatedInstance() {
if (
global._REANIMATED_VERSION_JS !== undefined &&
global._REANIMATED_VERSION_JS !== jsVersion
) {
throw new ReanimatedError(
`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}.`
);
}
}
class NativeReanimatedModule implements IReanimatedModule {
/**
* We keep the instance of `WorkletsModule` here to keep correct coupling of
* the modules and initialization order.
*/
// eslint-disable-next-line no-unused-private-class-members
#workletsModule: IWorkletsModule;
#reanimatedModuleProxy: ReanimatedModuleProxy;
constructor() {
this.#workletsModule = WorkletsModule;
// These checks have to split since version checking depend on the execution order
if (__DEV__) {
assertSingleReanimatedInstance();
assertWorkletsVersion();
}
global._REANIMATED_VERSION_JS = jsVersion;
if (global.__reanimatedModuleProxy === undefined && ReanimatedTurboModule) {
if (!ReanimatedTurboModule.installTurboModule()) {
// This path means that React Native has failed on reload.
// We don't want to throw any errors to not mislead the users
// that the problem is related to Reanimated.
// We install a DummyReanimatedModuleProxy instead.
this.#reanimatedModuleProxy = new DummyReanimatedModuleProxy();
return;
}
}
if (global.__reanimatedModuleProxy === undefined) {
throw new ReanimatedError(
`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__ && !globalThis.RN$Bridgeless && !SHOULD_BE_USE_WEB) {
throw new ReanimatedError(
'Reanimated 4 supports only the React Native New Architecture and web.'
);
}
if (__DEV__) {
checkCppVersion();
}
this.#reanimatedModuleProxy = global.__reanimatedModuleProxy;
executeOnUIRuntimeSync(function initializeUI() {
'worklet';
registerReanimatedError();
})();
}
registerSensor(
sensorType: number,
interval: number,
iosReferenceFrame: number,
handler: SerializableRef<(data: Value3D | ValueRotation) => void>
) {
return this.#reanimatedModuleProxy.registerSensor(
sensorType,
interval,
iosReferenceFrame,
handler
);
}
unregisterSensor(sensorId: number) {
return this.#reanimatedModuleProxy.unregisterSensor(sensorId);
}
registerEventHandler<T>(
eventHandler: SerializableRef<T>,
eventName: string,
emitterReactTag: number
) {
return this.#reanimatedModuleProxy.registerEventHandler(
eventHandler,
eventName,
emitterReactTag
);
}
unregisterEventHandler(id: number) {
return this.#reanimatedModuleProxy.unregisterEventHandler(id);
}
getViewProp<T>(
viewTag: number,
propName: string,
component: WrapperRef, // required on Fabric
callback?: (result: T) => void
) {
const shadowNodeWrapper = getShadowNodeWrapperFromRef(component);
return this.#reanimatedModuleProxy.getViewProp(
shadowNodeWrapper,
propName,
callback
);
}
configureLayoutAnimationBatch(
layoutAnimationsBatch: LayoutAnimationBatchItem[]
) {
this.#reanimatedModuleProxy.configureLayoutAnimationBatch(
layoutAnimationsBatch
);
}
setShouldAnimateExitingForTag(viewTag: number, shouldAnimate: boolean) {
this.#reanimatedModuleProxy.setShouldAnimateExitingForTag(
viewTag,
shouldAnimate
);
}
getStaticFeatureFlag(name: string): boolean {
return this.#reanimatedModuleProxy.getStaticFeatureFlag(name);
}
setDynamicFeatureFlag(name: string, value: boolean) {
this.#reanimatedModuleProxy.setDynamicFeatureFlag(name, value);
}
subscribeForKeyboardEvents(
handler: SerializableRef<WorkletFunction>,
isStatusBarTranslucent: boolean,
isNavigationBarTranslucent: boolean
) {
return this.#reanimatedModuleProxy.subscribeForKeyboardEvents(
handler,
isStatusBarTranslucent,
isNavigationBarTranslucent
);
}
unsubscribeFromKeyboardEvents(listenerId: number) {
this.#reanimatedModuleProxy.unsubscribeFromKeyboardEvents(listenerId);
}
setViewStyle(viewTag: number, style: StyleProps) {
this.#reanimatedModuleProxy.setViewStyle(viewTag, style);
}
markNodeAsRemovable(shadowNodeWrapper: ShadowNodeWrapper) {
this.#reanimatedModuleProxy.markNodeAsRemovable(shadowNodeWrapper);
}
unmarkNodeAsRemovable(viewTag: number) {
this.#reanimatedModuleProxy.unmarkNodeAsRemovable(viewTag);
}
registerCSSKeyframes(
animationName: string,
viewName: string,
keyframesConfig: NormalizedCSSAnimationKeyframesConfig
) {
this.#reanimatedModuleProxy.registerCSSKeyframes(
animationName,
viewName,
keyframesConfig
);
}
unregisterCSSKeyframes(animationName: string, viewName: string) {
this.#reanimatedModuleProxy.unregisterCSSKeyframes(animationName, viewName);
}
applyCSSAnimations(
shadowNodeWrapper: ShadowNodeWrapper,
animationUpdates: CSSAnimationUpdates
) {
this.#reanimatedModuleProxy.applyCSSAnimations(
shadowNodeWrapper,
animationUpdates
);
}
unregisterCSSAnimations(viewTag: number) {
this.#reanimatedModuleProxy.unregisterCSSAnimations(viewTag);
}
registerCSSTransition(
shadowNodeWrapper: ShadowNodeWrapper,
transitionConfig: NormalizedCSSTransitionConfig
) {
this.#reanimatedModuleProxy.registerCSSTransition(
shadowNodeWrapper,
transitionConfig
);
}
updateCSSTransition(
viewTag: number,
configUpdates: Partial<NormalizedCSSTransitionConfig>
) {
this.#reanimatedModuleProxy.updateCSSTransition(viewTag, configUpdates);
}
unregisterCSSTransition(viewTag: number) {
this.#reanimatedModuleProxy.unregisterCSSTransition(viewTag);
}
}
class DummyReanimatedModuleProxy implements ReanimatedModuleProxy {
configureLayoutAnimationBatch(): void {}
setShouldAnimateExitingForTag(): void {}
getStaticFeatureFlag(): boolean {
return false;
}
setDynamicFeatureFlag(): void {}
subscribeForKeyboardEvents(): number {
return -1;
}
unsubscribeFromKeyboardEvents(): void {}
setViewStyle(): void {}
markNodeAsRemovable(): void {}
unmarkNodeAsRemovable(): void {}
registerCSSKeyframes(): void {}
unregisterCSSKeyframes(): void {}
applyCSSAnimations(): void {}
registerCSSAnimations(): void {}
updateCSSAnimations(): void {}
unregisterCSSAnimations(): void {}
registerCSSTransition(): void {}
updateCSSTransition(): void {}
unregisterCSSTransition(): void {}
registerSensor(): number {
return -1;
}
unregisterSensor(): void {}
registerEventHandler(): number {
return -1;
}
unregisterEventHandler(): void {}
getViewProp() {
return null!;
}
}
@@ -1,7 +0,0 @@
'use strict';
export { ReanimatedModule } from './reanimatedModuleInstance';
export type {
IReanimatedModule,
ReanimatedModuleProxy,
} from './reanimatedModuleProxy';
@@ -1,9 +0,0 @@
'use strict';
// this file was created to prevent NativeReanimated from being included in the web bundle
import { createJSReanimatedModule } from './js-reanimated';
export const ReanimatedModule = createJSReanimatedModule();
export type {
IReanimatedModule,
ReanimatedModuleProxy,
} from './reanimatedModuleProxy';
@@ -1,360 +0,0 @@
'use strict';
import type {
IWorkletsModule,
SerializableRef,
WorkletFunction,
} from 'react-native-worklets';
import { WorkletsModule } from 'react-native-worklets';
import {
IS_JEST,
IS_WEB,
IS_WINDOW_AVAILABLE,
logger,
ReanimatedError,
} from '../../common';
import type {
ShadowNodeWrapper,
StyleProps,
Value3D,
ValueRotation,
WrapperRef,
} from '../../commonTypes';
import { SensorType } from '../../commonTypes';
import type {
CSSAnimationUpdates,
NormalizedCSSAnimationKeyframesConfig,
NormalizedCSSTransitionConfig,
} from '../../css/native';
import { assertWorkletsVersion } from '../../platform-specific/workletsVersion';
import type { IReanimatedModule } from '../reanimatedModuleProxy';
import type { WebSensor } from './WebSensor';
export function createJSReanimatedModule(): IReanimatedModule {
return new JSReanimated();
}
class JSReanimated implements IReanimatedModule {
/**
* We keep the instance of `WorkletsModule` here to keep correct coupling of
* the modules and initialization order.
*/
// eslint-disable-next-line no-unused-private-class-members
#workletsModule: IWorkletsModule = WorkletsModule;
nextSensorId = 0;
sensors = new Map<number, WebSensor>();
platform?: Platform = undefined;
constructor() {
if (__DEV__) {
assertWorkletsVersion();
}
}
registerEventHandler<T>(
_eventHandler: SerializableRef<T>,
_eventName: string,
_emitterReactTag: number
): number {
throw new ReanimatedError(
'registerEventHandler is not available in JSReanimated.'
);
}
unregisterEventHandler(_: number): void {
throw new ReanimatedError(
'unregisterEventHandler is not available in JSReanimated.'
);
}
configureLayoutAnimationBatch() {
// no-op
}
setShouldAnimateExitingForTag() {
// no-op
}
registerSensor(
sensorType: SensorType,
interval: number,
_iosReferenceFrame: number,
eventHandler: SerializableRef<(data: Value3D | ValueRotation) => void>
): number {
if (!IS_WINDOW_AVAILABLE) {
// 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
logger.warn(
'Sensor is not available.' +
(IS_WEB && 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: SerializableRef<(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 SerializableRef 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 SerializableRef is the value itself so we call it directly
(eventHandler as any)({ x, y, z, interfaceOrientation: 0 });
};
case SensorType.ROTATION:
return () => {
const [qw, qx] = sensor.quaternion;
let [, , 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 SerializableRef 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(_: SerializableRef<WorkletFunction>): number {
if (IS_WEB) {
logger.warn('useAnimatedKeyboard is not available on web yet.');
} else if (IS_JEST) {
logger.warn('useAnimatedKeyboard is not available when using Jest.');
} else {
logger.warn(
'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?: WrapperRef | null,
_callback?: (result: T) => void
): Promise<T> {
throw new ReanimatedError('getViewProp is not available in JSReanimated.');
}
getStaticFeatureFlag(): boolean {
// mock implementation
return false;
}
setDynamicFeatureFlag(): void {
// noop
}
setViewStyle(_viewTag: number, _style: StyleProps): void {
throw new ReanimatedError('setViewStyle is not available in JSReanimated.');
}
markNodeAsRemovable(_shadowNodeWrapper: ShadowNodeWrapper): void {
throw new ReanimatedError(
'markNodeAsRemovable is not available in JSReanimated.'
);
}
unmarkNodeAsRemovable(_viewTag: number): void {
throw new ReanimatedError(
'unmarkNodeAsRemovable is not available in JSReanimated.'
);
}
registerCSSKeyframes(
_animationName: string,
_viewName: string,
_keyframesConfig: NormalizedCSSAnimationKeyframesConfig
): void {
throw new ReanimatedError(
'`registerCSSKeyframes` is not available in JSReanimated.'
);
}
unregisterCSSKeyframes(_animationName: string, _viewName: string): void {
throw new ReanimatedError(
'`unregisterCSSKeyframes` is not available in JSReanimated.'
);
}
applyCSSAnimations(
_shadowNodeWrapper: ShadowNodeWrapper,
_animationUpdates: CSSAnimationUpdates
) {
throw new ReanimatedError(
'`applyCSSAnimations` is not available in JSReanimated.'
);
}
unregisterCSSAnimations(_viewTag: number): void {
throw new ReanimatedError(
'`unregisterCSSAnimations` is not available in JSReanimated.'
);
}
registerCSSTransition(
_shadowNodeWrapper: ShadowNodeWrapper,
_transitionConfig: NormalizedCSSTransitionConfig
): void {
throw new ReanimatedError(
'`registerCSSTransition` is not available in JSReanimated.'
);
}
updateCSSTransition(
_viewTag: number,
_settingsUpdates: Partial<NormalizedCSSTransitionConfig>
): void {
throw new ReanimatedError(
'`updateCSSTransition` is not available in JSReanimated.'
);
}
unregisterCSSTransition(_viewTag: number): void {
throw new ReanimatedError(
'`unregisterCSSTransition` is not available in JSReanimated.'
);
}
}
// Lack of this export breaks TypeScript generation since
// an enum transpiles into JavaScript code.
/** @knipIgnore */
export enum Platform {
WEB_IOS = 'web iOS',
WEB_ANDROID = 'web Android',
WEB = 'web',
UNKNOWN = 'unknown',
}
declare global {
interface Navigator {
userAgent: string;
vendor: string;
}
}
@@ -1,36 +0,0 @@
'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;
}
}
@@ -1,148 +0,0 @@
'use strict';
import { logger } from '../../common';
import type { AnimatedStyle, StyleProps } from '../../commonTypes';
import type { PropUpdates } from '../../createAnimatedComponent/commonTypes';
import {
createReactDOMStyle,
createTextShadowValue,
createTransformValue,
} from './webUtils';
export { createJSReanimatedModule } from './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;
};
isDummy?: boolean;
dummyClone?: ReanimatedHTMLElement;
removedAfterAnimation?: boolean;
}
// TODO: Move these functions outside of index file.
export const _updatePropsJS = (
updates: PropUpdates,
viewRef: (JSReanimatedComponent | ReanimatedHTMLElement) & {
getAnimatableRef?: () => JSReanimatedComponent | ReanimatedHTMLElement;
},
isAnimatedProps?: boolean
): void => {
if (viewRef) {
const component = viewRef.getAnimatableRef
? viewRef.getAnimatableRef()
: viewRef;
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 : '';
logger.warn(
`It's not possible to manipulate the component ${componentName}`
);
}
}
};
const setNativeProps = (
component: JSReanimatedComponent | ReanimatedHTMLElement,
newProps: StyleProps,
isAnimatedProps?: boolean
): void => {
if (isAnimatedProps) {
// Only update UI props directly on the component,
// other props can be updated as standard style props.
component.setNativeProps?.(newProps);
}
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) {
// We need to explicitly set the 'text' property on input component because React Native's
// internal _valueTracker (https://github.com/facebook/react/blob/main/packages/react-dom-bindings/src/client/inputValueTracking.js)
// prevents updates when only modifying attributes.
if ((component as HTMLElement).nodeName === 'INPUT' && key === 'text') {
(component as HTMLInputElement).value = domStyle[key] as string;
} else {
(component as HTMLElement).setAttribute(key, domStyle[key]);
}
} else {
(component.style as StyleProps)[key] = domStyle[key];
}
}
};
@@ -1,3 +0,0 @@
'use strict';
declare module 'react-native-web/dist/exports/StyleSheet/compiler/createReactDOMStyle';
declare module 'react-native-web/dist/exports/StyleSheet/preprocess';
@@ -1,8 +0,0 @@
'use strict';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export let createReactDOMStyle: (style: any) => any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export let createTransformValue: (transform: any) => any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export let createTextShadowValue: (style: any) => void | string;
@@ -1,27 +0,0 @@
'use strict';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export let createReactDOMStyle: (style: any) => any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export let createTransformValue: (transform: any) => any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export let createTextShadowValue: (style: any) => void | string;
try {
createReactDOMStyle =
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
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, @typescript-eslint/no-require-imports
require('react-native-web/dist/exports/StyleSheet/preprocess').createTransformValue;
} catch (_e) {}
try {
createTextShadowValue =
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
require('react-native-web/dist/exports/StyleSheet/preprocess').createTextShadowValue;
} catch (_e) {}
@@ -1,9 +0,0 @@
'use strict';
import { SHOULD_BE_USE_WEB } from '../common';
import { createJSReanimatedModule } from './js-reanimated';
import { createNativeReanimatedModule } from './NativeReanimated';
export const ReanimatedModule = SHOULD_BE_USE_WEB
? createJSReanimatedModule()
: createNativeReanimatedModule();
@@ -1,5 +0,0 @@
'use strict';
import { createJSReanimatedModule } from './js-reanimated';
export const ReanimatedModule = createJSReanimatedModule();
@@ -1,103 +0,0 @@
'use strict';
import type { SerializableRef, WorkletFunction } from 'react-native-worklets';
import type {
LayoutAnimationBatchItem,
ShadowNodeWrapper,
StyleProps,
Value3D,
ValueRotation,
WrapperRef,
} from '../commonTypes';
import type {
CSSAnimationUpdates,
NormalizedCSSAnimationKeyframesConfig,
NormalizedCSSTransitionConfig,
} from '../css/native';
/** Type of `__reanimatedModuleProxy` injected with JSI. */
export interface ReanimatedModuleProxy {
registerEventHandler<T>(
eventHandler: SerializableRef<T>,
eventName: string,
emitterReactTag: number
): number;
unregisterEventHandler(id: number): void;
getViewProp<T>(
viewTagOrShadowNodeWrapper: number | ShadowNodeWrapper,
propName: string,
callback?: (result: T) => void
): Promise<T>;
registerSensor(
sensorType: number,
interval: number,
iosReferenceFrame: number,
handler: SerializableRef<(data: Value3D | ValueRotation) => void>
): number;
unregisterSensor(sensorId: number): void;
getStaticFeatureFlag(name: string): boolean;
setDynamicFeatureFlag(name: string, value: boolean): void;
subscribeForKeyboardEvents(
handler: SerializableRef<WorkletFunction>,
isStatusBarTranslucent: boolean,
isNavigationBarTranslucent: boolean
): number;
unsubscribeFromKeyboardEvents(listenerId: number): void;
configureLayoutAnimationBatch(
layoutAnimationsBatch: LayoutAnimationBatchItem[]
): void;
setShouldAnimateExitingForTag(viewTag: number, shouldAnimate: boolean): void;
setViewStyle(viewTag: number, style: StyleProps): void;
markNodeAsRemovable(shadowNodeWrapper: ShadowNodeWrapper): void;
unmarkNodeAsRemovable(viewTag: number): void;
registerCSSKeyframes(
animationName: string,
viewName: string,
keyframesConfig: NormalizedCSSAnimationKeyframesConfig
): void;
unregisterCSSKeyframes(animationName: string, viewName: string): void;
applyCSSAnimations(
shadowNodeWrapper: ShadowNodeWrapper,
animationUpdates: CSSAnimationUpdates
): void;
unregisterCSSAnimations(viewTag: number): void;
registerCSSTransition(
shadowNodeWrapper: ShadowNodeWrapper,
transitionConfig: NormalizedCSSTransitionConfig
): void;
updateCSSTransition(
viewTag: number,
settingsUpdates: Partial<NormalizedCSSTransitionConfig>
): void;
unregisterCSSTransition(viewTag: number): void;
}
export interface IReanimatedModule
extends Omit<ReanimatedModuleProxy, 'getViewProp'> {
getViewProp<TValue>(
viewTag: number,
propName: string,
component: WrapperRef | null,
callback?: (result: TValue) => void
): Promise<TValue>;
}
@@ -1,25 +0,0 @@
'use strict';
import { IS_WEB, IS_WINDOW_AVAILABLE } from './common';
import { makeMutable } from './mutables';
type localGlobal = typeof global & Record<string, unknown>;
export function isReducedMotionEnabledInSystem() {
return IS_WEB
? IS_WINDOW_AVAILABLE
? // @ts-ignore Fallback if `window` is undefined.
window.matchMedia('(prefers-reduced-motion: reduce)').matches
: false
: !!(global as localGlobal)._REANIMATED_IS_REDUCED_MOTION;
}
const IS_REDUCED_MOTION_ENABLED_IN_SYSTEM = isReducedMotionEnabledInSystem();
export const ReducedMotionManager = {
jsValue: IS_REDUCED_MOTION_ENABLED_IN_SYSTEM,
uiValue: makeMutable(IS_REDUCED_MOTION_ENABLED_IN_SYSTEM),
setEnabled(value: boolean) {
ReducedMotionManager.jsValue = value;
ReducedMotionManager.uiValue.value = value;
},
};
-83
View File
@@ -1,83 +0,0 @@
'use strict';
import type { SerializableRef, WorkletFunction } from 'react-native-worklets';
import type {
SensorConfig,
SharedValue,
Value3D,
ValueRotation,
} from './commonTypes';
import { SensorType } from './commonTypes';
import { makeMutable } from './mutables';
import { ReanimatedModule } from './ReanimatedModule';
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: SerializableRef<(data: Value3D | ValueRotation) => void>
) {
const config = this.config;
const sensorType = this.sensorType;
this.sensorId = ReanimatedModule.registerSensor(
sensorType,
config.interval === 'auto' ? -1 : config.interval,
config.iosReferenceFrame,
eventHandler as SerializableRef<WorkletFunction>
);
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) {
ReanimatedModule.unregisterSensor(this.sensorId);
}
this.sensorId = null;
}
}
@@ -1,73 +0,0 @@
'use strict';
import type { SerializableRef } from 'react-native-worklets';
import type {
SensorConfig,
SensorType,
SharedValue,
Value3D,
ValueRotation,
} 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: SerializableRef<(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();
}
}
}
}
}
@@ -1,76 +0,0 @@
'use strict';
import { createSerializable } from 'react-native-worklets';
import { SHOULD_BE_USE_WEB } from './common';
import type {
LayoutAnimationBatchItem,
LayoutAnimationFunction,
LayoutAnimationType,
} from './commonTypes';
import { configureLayoutAnimationBatch } from './core';
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) {
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}
* or {@link Keyframe}. Passing `undefined` will remove the animation.
* @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,
isUnmounting?: boolean
) => void;
if (SHOULD_BE_USE_WEB) {
updateLayoutAnimations = () => {
// no-op
};
} else {
const updateLayoutAnimationsManager = createUpdateManager();
updateLayoutAnimations = (viewTag, type, config, isUnmounting) =>
updateLayoutAnimationsManager.update(
{
viewTag,
type,
config: config ? createSerializable(config) : undefined,
},
isUnmounting
);
}
@@ -1,55 +0,0 @@
'use strict';
import type { SharedValue, StyleUpdaterContainer } from './commonTypes';
import { makeMutable } from './core';
import type { Descriptor } from './hook/commonTypes';
export interface ViewDescriptorsSet {
shareableViewDescriptors: SharedValue<Descriptor[]>;
add: (item: Descriptor, updaterContainer?: StyleUpdaterContainer) => void;
remove: (viewTag: number) => void;
has: (viewTag: number) => boolean;
}
export function makeViewDescriptorsSet(): ViewDescriptorsSet {
const shareableViewDescriptors = makeMutable<Descriptor[]>([]);
const viewTags = new Set<number>();
const data: ViewDescriptorsSet = {
shareableViewDescriptors,
add: (item: Descriptor, updaterContainer?: StyleUpdaterContainer) => {
viewTags.add(item.tag as number);
const updater = updaterContainer?.current;
shareableViewDescriptors.modify((descriptors) => {
'worklet';
const index = descriptors.findIndex(
(descriptor) => descriptor.tag === item.tag
);
if (index !== -1) {
descriptors[index] = item;
} else {
descriptors.push(item);
}
updater?.(true);
return descriptors;
}, false);
},
remove: (viewTag: number) => {
viewTags.delete(viewTag);
shareableViewDescriptors.modify((descriptors) => {
'worklet';
const index = descriptors.findIndex(
(descriptor) => descriptor.tag === viewTag
);
if (index !== -1) {
descriptors.splice(index, 1);
}
return descriptors;
}, false);
},
has: (viewTag: number) => viewTags.has(viewTag),
};
return data;
}
@@ -1,141 +0,0 @@
'use strict';
import type { NativeSyntheticEvent } from 'react-native';
import { SHOULD_BE_USE_WEB } from './common';
import { registerEventHandler, unregisterEventHandler } from './core';
import type {
EventPayload,
IWorkletEventHandler,
ReanimatedEvent,
} from './hook/commonTypes';
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;
@@ -1,134 +0,0 @@
'use strict';
import { logger } from '../common';
import type {
AnimatableValue,
Animation,
AnimationObject,
ReduceMotion,
Timestamp,
} from '../commonTypes';
import type { ClampAnimation } from './commonTypes';
import {
defineAnimation,
getReduceMotionForAnimation,
recognizePrefixSuffix,
} from './util';
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) {
logger.warn(
"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
) {
logger.warn(
'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;
@@ -1,59 +0,0 @@
'use strict';
import type {
AnimatableValue,
AnimatedStyle,
Animation,
AnimationCallback,
AnimationObject,
StyleProps,
Timestamp,
} from '../commonTypes';
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;
}
@@ -1,130 +0,0 @@
'use strict';
import { ReanimatedError } from '../../common';
import type {
Animation,
AnimationCallback,
Timestamp,
} from '../../commonTypes';
import { defineAnimation, getReduceMotionForAnimation } from '../util';
import { rigidDecay } from './rigidDecay';
import { rubberBandDecay } from './rubberBandDecay';
import type {
DecayAnimation,
DecayConfig,
DefaultDecayConfig,
InnerDecayAnimation,
} from './utils';
import { isValidRubberBandConfig } from './utils';
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 ReanimatedError(
`\`config.clamp\` must be an array but is ${typeof config.clamp}.`
);
}
if (config.clamp.length !== 2) {
throw new ReanimatedError(
`\`clamp array\` must contain 2 items but is given ${
config.clamp.length as number
}.`
);
}
}
if (config.velocityFactor <= 0) {
throw new ReanimatedError(
`\`config.velocityFactor\` must be greater then 0 but is ${config.velocityFactor}.`
);
}
if (config.rubberBandEffect && !config.clamp) {
throw new ReanimatedError(
'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 {
const initialVelocity = config.velocity;
animation.current = value;
animation.lastTimestamp = now;
animation.startTimestamp = now;
animation.initialVelocity = initialVelocity;
animation.velocity = initialVelocity;
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];
}
}
}
// To ensure the animation is correctly initialized and starts as expected
// we need to set its current value to undefined.
// Setting current to 0 breaks the animation.
return {
onFrame: decay,
onStart,
callback,
velocity: config.velocity ?? 0,
initialVelocity: 0,
current: undefined,
lastTimestamp: 0,
startTimestamp: 0,
reduceMotion: getReduceMotionForAnimation(config.reduceMotion),
} as DecayAnimation;
});
} as unknown as withDecayType;
@@ -1,4 +0,0 @@
'use strict';
export type { WithDecayConfig } from './decay';
export { withDecay } from './decay';
export type { DecayAnimation } from './utils';
@@ -1,34 +0,0 @@
'use strict';
import type { DefaultDecayConfig, InnerDecayAnimation } from './utils';
import { SLOPE_FACTOR, VELOCITY_EPS } 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;
}
@@ -1,46 +0,0 @@
'use strict';
import type { InnerDecayAnimation, RubberBandDecayConfig } 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;
}
@@ -1,85 +0,0 @@
'use strict';
import { IS_WEB } from '../../common';
import type {
AnimatableValue,
Animation,
AnimationObject,
ReduceMotion,
RequiredKeys,
Timestamp,
} from '../../commonTypes';
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 | undefined;
}
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
);
}
@@ -1,117 +0,0 @@
'use strict';
import type {
AnimatableValue,
Animation,
AnimationObject,
ReduceMotion,
Timestamp,
} from '../commonTypes';
import type { DelayAnimation } from './commonTypes';
import { defineAnimation, getReduceMotionForAnimation } from './util';
// 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;
@@ -1,29 +0,0 @@
'use strict';
export { withClamp } from './clamp';
export type {
DelayAnimation,
RepeatAnimation,
SequenceAnimation,
StyleLayoutAnimation,
} from './commonTypes';
export type { DecayAnimation, WithDecayConfig } from './decay';
export { withDecay } from './decay';
export { withDelay } from './delay';
export { withRepeat } from './repeat';
export { withSequence } from './sequence';
export type { SpringAnimation, WithSpringConfig } from './spring';
export {
GentleSpringConfig,
GentleSpringConfigWithDuration,
Reanimated3DefaultSpringConfig,
Reanimated3DefaultSpringConfigWithDuration,
SnappySpringConfig,
SnappySpringConfigWithDuration,
WigglySpringConfig,
WigglySpringConfigWithDuration,
withSpring,
} from './spring';
export { withStyleAnimation } from './styleAnimation';
export type { TimingAnimation, WithTimingConfig } from './timing';
export { withTiming } from './timing';
export { cancelAnimation, defineAnimation, initialUpdaterRun } from './util';
@@ -1,144 +0,0 @@
'use strict';
import type {
AnimatableValue,
Animation,
AnimationCallback,
AnimationObject,
ReduceMotion,
Timestamp,
} from '../commonTypes';
import type { RepeatAnimation } from './commonTypes';
import { defineAnimation, getReduceMotionForAnimation } from './util';
// 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;
@@ -1,171 +0,0 @@
'use strict';
import { logger } from '../common';
import type {
AnimatableValue,
Animation,
AnimationObject,
ReduceMotion,
Timestamp,
} from '../commonTypes';
import type { NextAnimation, SequenceAnimation } from './commonTypes';
import { defineAnimation, getReduceMotionForAnimation } from './util';
/**
* 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;
} else {
_animations.unshift(
_reduceMotionOrFirstAnimation as NextAnimation<AnimationObject>
);
}
}
if (_animations.length === 0) {
logger.warn('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
) {
if (typeof animations[index].callback === 'function') {
animations[index].callback?.(true);
}
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;
}
);
}
@@ -1,5 +0,0 @@
'use strict';
export * from './spring';
export * from './springConfigs';
export * from './springUtils';
@@ -1,254 +0,0 @@
'use strict';
import type {
AnimatableValue,
Animation,
AnimationCallback,
Timestamp,
} from '../../commonTypes';
import { defineAnimation, getReduceMotionForAnimation } from '../util';
import type { SpringConfig } from './springConfigs';
import {
GentleSpringConfig,
GentleSpringConfigWithDuration,
} from './springConfigs';
import type {
DefaultSpringConfig,
InnerSpringAnimation,
SpringAnimation,
SpringConfigInner,
} from './springUtils';
import {
calculateNewStiffnessToMatchDuration,
checkIfConfigIsValid,
criticallyDampedSpringCalculations,
getEnergy,
initialCalculations,
isAnimationTerminatingCalculation,
safeMergeConfigs,
scaleZetaToMatchClamps,
underDampedSpringCalculations,
} 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}.
* Defaults to {@link GentleSpringConfig}. You can use other predefined spring
* configurations, such as {@link WigglySpringConfig},
* {@link SnappySpringConfig}, {@link Reanimated3DefaultSpringConfig} or create
* your own.
* @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 = {
...GentleSpringConfig,
...GentleSpringConfigWithDuration,
overshootClamping: false,
energyThreshold: 6e-9,
velocity: 0,
reduceMotion: undefined,
clamp: undefined,
} as const;
const config: DefaultSpringConfig & SpringConfigInner = safeMergeConfigs<
DefaultSpringConfig & SpringConfigInner
>(
{
...defaultConfig,
useDuration: !!(userConfig?.duration || userConfig?.dampingRatio),
skipAnimation: false,
},
userConfig
);
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, current } = animation;
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 as number;
const x0 = current - toValue;
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;
if (isAnimationTerminatingCalculation(animation, config)) {
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;
let stiffness = config.stiffness;
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 as number)
: value - (animation.toValue as number);
animation.startValue = x0;
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;
stiffness = calculateNewStiffnessToMatchDuration(
x0,
config,
animation.velocity
);
config.stiffness = stiffness;
}
const { zeta, omega0, omega1 } = initialCalculations(stiffness, config);
animation.zeta = zeta;
animation.omega0 = omega0;
animation.omega1 = omega1;
if (config.clamp !== undefined) {
animation.zeta = scaleZetaToMatchClamps(animation, config.clamp);
}
}
const initialEnergy = getEnergy(
x0,
config.velocity,
config.stiffness,
config.mass
);
animation.initialEnergy = initialEnergy;
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,
initialEnergy: 0,
reduceMotion: getReduceMotionForAnimation(config.reduceMotion),
} as SpringAnimation;
});
}) as withSpringType;
@@ -1,98 +0,0 @@
'use strict';
import type { ReduceMotion } from '../../commonTypes';
export const Reanimated3DefaultSpringConfig = {
damping: 10,
mass: 1,
stiffness: 100,
} as const satisfies SpringConfig;
export const Reanimated3DefaultSpringConfigWithDuration = {
duration: 1333,
dampingRatio: 0.5,
} as const satisfies SpringConfig;
export const WigglySpringConfig = {
damping: 90,
mass: 4,
stiffness: 900,
} as const satisfies SpringConfig;
export const WigglySpringConfigWithDuration = {
duration: 550,
dampingRatio: 0.75,
} as const satisfies SpringConfig;
export const GentleSpringConfig = {
damping: 120,
mass: 4,
stiffness: 900,
} as const satisfies SpringConfig;
export const GentleSpringConfigWithDuration = {
duration: 550,
dampingRatio: 1,
} as const satisfies SpringConfig;
export const SnappySpringConfig = {
damping: 110,
mass: 4,
stiffness: 900,
overshootClamping: true,
} as const satisfies SpringConfig;
export const SnappySpringConfigWithDuration = {
duration: 550,
dampingRatio: 0.92,
overshootClamping: true,
} as const satisfies SpringConfig;
/**
* Spring animation configuration.
*
* @param mass - The weight of the spring. Reducing this value makes the
* animation faster. Defaults to 4.
* @param damping - How quickly a spring slows down. Higher damping means the
* spring will come to rest faster. Defaults to 120.
* @param stiffness - How bouncy the spring is. Defaults to 900.
* @param duration - Perceptual duration of the animation in milliseconds.
* Actual duration is 1.5 times the value of perceptual duration. Defaults to
* 550ms if `dampingRatio` is provided.
* @param dampingRatio - How damped the spring is. Value `1` means the spring is
* critically damped, value `<1` means the spring is underdamped and value
* `>1` means the spring is overdamped. Defaults to 1 if `duration` is
* provided.
* @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 energyThreshold - Relative energy threshold below which the spring
* will snap to `toValue` without further oscillations. Defaults to 6e-9.
* @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 = {
mass?: number;
overshootClamping?: boolean;
energyThreshold?: number;
velocity?: number;
reduceMotion?: ReduceMotion;
} & (
| {
stiffness?: number;
damping?: number;
duration?: never;
dampingRatio?: never;
clamp?: never;
}
| {
stiffness?: never;
damping?: never;
duration?: number;
dampingRatio?: number;
clamp?: { min?: number; max?: number };
}
);
@@ -1,393 +0,0 @@
'use strict';
import { logger } from '../../common';
import type { AnimatableValue, Animation, Timestamp } from '../../commonTypes';
import type { SpringConfig } from './springConfigs';
// 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;
initialEnergy: 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', 'mass', 'energyThreshold'] 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 !== '') {
logger.warn('Invalid spring config' + errorMessage);
}
return errorMessage === '';
}
export function safeMergeConfigs<TConfig extends object>(
defaults: TConfig,
userConfig?: Partial<TConfig>
): TConfig {
'worklet';
if (!userConfig) {
return defaults;
}
const filtered = Object.fromEntries(
Object.entries(userConfig).filter(([, v]) => v !== undefined)
) as Partial<TConfig>;
return {
...defaults,
...filtered,
};
}
function bisectRoot({
min,
max,
func,
precision,
maxIterations = 20,
}: {
min: number;
max: number;
func: (x: number) => number;
precision: number;
maxIterations?: number;
}) {
'worklet';
const direction = func(max) >= func(min) ? 1 : -1;
let idx = maxIterations;
let current = (max + min) / 2;
while (Math.abs(func(current)) > precision && idx > 0) {
idx -= 1;
if (func(current) * direction < 0) {
min = current;
} else {
max = current;
}
current = (min + max) / 2;
}
return current;
}
export function initialCalculations(
stiffness = 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 { mass: m, 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(stiffness / m);
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 (startValue === 0) {
return zeta;
}
const [firstBound, secondBound] =
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) / startValue)
: undefined;
const relativeExtremum2 =
firstBound !== undefined
? Math.abs((firstBound - 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);
}
export function getEnergy(
displacement: number,
velocity: number,
stiffness: number,
mass: number
) {
'worklet';
const potentialEnergy = 0.5 * stiffness * displacement ** 2;
const kineticEnergy = 0.5 * mass * velocity ** 2;
return potentialEnergy + kineticEnergy;
}
/** Runs before initial */
export function calculateNewStiffnessToMatchDuration(
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
*/
const {
dampingRatio: zeta,
energyThreshold: threshold,
mass: m,
duration: targetDuration,
} = config;
const energyDiffForStiffness = (stiffness: number) => {
'worklet';
const perceptualCoefficient = 1.5;
const MILLISECONDS_IN_SECOND = 1000;
const settlingDuration =
(targetDuration * perceptualCoefficient) / MILLISECONDS_IN_SECOND;
const omega0 = Math.sqrt(stiffness / m) * zeta;
const xtk =
(x0 + (v0 + x0 * omega0) * settlingDuration) *
Math.exp(-omega0 * settlingDuration);
const vtk =
(x0 + (v0 + x0 * omega0) * settlingDuration) *
Math.exp(-omega0 * settlingDuration) *
-omega0 +
(v0 + x0 * omega0) * Math.exp(-omega0 * settlingDuration);
const e0 = getEnergy(x0, v0, stiffness, m);
const etk = getEnergy(xtk, vtk, stiffness, m);
const energyFraction = etk / e0;
return energyFraction - threshold;
};
const precision = config.energyThreshold * 1e-3; // Experimentally seems to be good enough.
// Bisection turns out to be much faster than Newton's method in our case
return bisectRoot({
min: Number.EPSILON,
max: 8e3 /* Stiffness for 8ms animation doesn't exceed 2e3, we add some safety margin on top of that. */,
func: energyDiffForStiffness,
precision,
maxIterations: 100,
});
}
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 * -omega0 * (x0 + (v0 + omega0 * x0) * t) +
criticallyDampedEnvelope * (v0 + omega0 * x0);
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 } = animation;
const { zeta, t, omega0, omega1, x0, v0 } = precalculatedValues;
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 & SpringConfigInner
): boolean {
'worklet';
const { toValue, velocity, startValue, current, initialEnergy } = animation;
if (config.overshootClamping) {
const leftBound = startValue >= 0 ? toValue : toValue + startValue;
const rightBound = leftBound + Math.abs(startValue);
if (current < leftBound || current > rightBound) {
return true;
}
}
const currentEnergy = getEnergy(
toValue - current,
velocity,
config.stiffness,
config.mass
);
return (
initialEnergy === 0 ||
currentEnergy / initialEnergy <= config.energyThreshold
);
}
@@ -1,276 +0,0 @@
'use strict';
import { ColorProperties } from '../Colors';
import { logger, processColor } from '../common';
import type {
AnimatableValue,
AnimatedStyle,
Animation,
AnimationObject,
NestedObject,
NestedObjectValues,
Timestamp,
} from '../commonTypes';
import type { StyleLayoutAnimation } from './commonTypes';
import { withTiming } from './timing';
import { defineAnimation, isValidLayoutAnimationProp } from './util';
// 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 (__DEV__) {
if (prevVal === undefined) {
logger.warn(
`Initial values for animation are missing for property ${currentEntry.path.join(
'.'
)}`
);
}
const propName = currentEntry.path[0];
if (
typeof propName === 'string' &&
!isValidLayoutAnimationProp(propName.trim())
) {
logger.warn(
`'${propName}' property is not officially supported for layout animations. It may not work as expected.`
);
}
}
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;
});
}
@@ -1,159 +0,0 @@
'use strict';
import type {
AnimatableValue,
Animation,
AnimationCallback,
EasingFunction,
ReduceMotion,
Timestamp,
} from '../commonTypes';
import type { EasingFunctionFactory } from '../Easing';
import { Easing } from '../Easing';
import {
assertEasingIsWorklet,
defineAnimation,
getReduceMotionForAnimation,
} from './util';
/**
* 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;
@@ -1,422 +0,0 @@
'use strict';
import { ReanimatedError } from '../../common';
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))
);
}
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;
}
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 ReanimatedError(
`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),
};
}
export function decomposeMatrix(
unknownTypeMatrix: AffineMatrixFlat | AffineMatrix
): TransformMatrixDecomposition {
'worklet';
const matrix = maybeFlattenMatrix(unknownTypeMatrix);
// normalize matrix
if (matrix[15] === 0) {
throw new ReanimatedError('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,
};
}
@@ -1,610 +0,0 @@
/* eslint-disable @typescript-eslint/no-shadow */
'use strict';
import {
createSerializable,
isWorkletFunction,
runOnUI,
RuntimeKind,
serializableMappingCache,
} from 'react-native-worklets';
import type { ParsedColorArray } from '../Colors';
import {
clampRGBA,
convertToRGBA,
isColor,
rgbaArrayToRGBAColor,
toGammaSpace,
toLinearSpace,
} from '../Colors';
import { logger, ReanimatedError, SHOULD_BE_USE_WEB } from '../common';
import type {
AnimatableValue,
AnimatableValueObject,
Animation,
AnimationObject,
EasingFunction,
SharedValue,
Timestamp,
} from '../commonTypes';
import { ReduceMotion } from '../commonTypes';
import type { EasingFunctionFactory } from '../Easing';
import { ReducedMotionManager } from '../ReducedMotion';
import type { HigherOrderAnimation, StyleLayoutAnimation } from './commonTypes';
import type {
AffineMatrix,
AffineMatrixFlat,
} from './transformationMatrix/matrixUtils';
import {
addMatrices,
decomposeMatrixIntoMatricesAndAngles,
flatten,
getRotationMatrix,
isAffineMatrixFlat,
multiplyMatrices,
scaleMatrix,
subtractMatrices,
} from './transformationMatrix/matrixUtils';
/**
* This variable has to be an object, because it can't be changed for the
* worklets if it's a primitive value. We also have to bind it to a separate
* object to prevent from freezing it in development.
*/
const IN_STYLE_UPDATER = { current: false };
const IN_STYLE_UPDATER_UI = createSerializable({ current: false });
serializableMappingCache.set(IN_STYLE_UPDATER, IN_STYLE_UPDATER_UI);
const LAYOUT_ANIMATION_SUPPORTED_PROPS = {
originX: true,
originY: true,
width: true,
height: true,
borderRadius: true,
globalOriginX: true,
globalOriginY: true,
opacity: true,
transform: true,
backgroundColor: true,
};
type LayoutAnimationProp = keyof typeof LAYOUT_ANIMATION_SUPPORTED_PROPS;
export function isValidLayoutAnimationProp(prop: string) {
'worklet';
return (prop as LayoutAnimationProp) in LAYOUT_ANIMATION_SUPPORTED_PROPS;
}
if (__DEV__ && ReducedMotionManager.jsValue) {
logger.warn(
`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 (globalThis.__RUNTIME_KIND !== RuntimeKind.ReactNative) {
// 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 ReanimatedError(
'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.current = true;
const result = updater();
IN_STYLE_UPDATER.current = 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 ReanimatedError("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.
*/
const isReduceMotionOnUI = ReducedMotionManager.uiValue;
export function getReduceMotionFromConfig(config?: ReduceMotion) {
'worklet';
return !config || config === ReduceMotion.System
? isReduceMotionOnUI.value
: 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, @typescript-eslint/no-base-to-string
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.unroundedCurrent = res;
// We need to clamp the res values to make sure they are in the correct RGBA range
clampRGBA(res as ParsedColorArray);
animation.current = rgbaArrayToRGBAColor(
toGammaSpace(res as ParsedColorArray)
);
};
const colorOnFrame = (
animation: Animation<AnimationObject>,
timestamp: Timestamp
): boolean => {
const res: Array<number> = [];
let finished = true;
// We must restore nonscale current to ever end the animation.
animation.current = animation.nonscaledCurrent;
tab.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;
res.push(animation[i].current);
});
// We need to clamp the res values to make sure they are in the correct RGBA range
clampRGBA(res as ParsedColorArray);
animation.nonscaledCurrent = res;
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 (
globalThis.__RUNTIME_KIND === RuntimeKind.ReactNative &&
IN_STYLE_UPDATER.current
) {
return starting as unknown as T;
}
const create = () => {
'worklet';
const animation = factory();
decorateAnimation<U>(animation as unknown as U);
return animation;
};
if (
globalThis.__RUNTIME_KIND !== RuntimeKind.ReactNative ||
SHOULD_BE_USE_WEB
) {
return create();
}
create.__isAnimationDefinition = true;
// @ts-expect-error it's fine
return create;
}
function cancelAnimationNative<TValue>(sharedValue: SharedValue<TValue>): void {
'worklet';
// setting the current value cancels the animation if one is currently running
if (globalThis.__RUNTIME_KIND !== RuntimeKind.ReactNative) {
sharedValue.value = sharedValue.value; // eslint-disable-line no-self-assign
} else {
runOnUI(() => {
'worklet';
sharedValue.value = sharedValue.value; // eslint-disable-line no-self-assign
})();
}
}
function cancelAnimationWeb<TValue>(sharedValue: SharedValue<TValue>): void {
// setting the current value cancels the animation if one is currently running
sharedValue.value = sharedValue.value; // eslint-disable-line no-self-assign
}
/**
* Lets you cancel a running animation paired to a shared value. The
* cancellation is asynchronous.
*
* @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 const cancelAnimation = SHOULD_BE_USE_WEB
? cancelAnimationWeb
: cancelAnimationNative;
+19 -59
View File
@@ -1,14 +1,12 @@
'use strict';
import { logger } from './common';
import type {
ILayoutAnimationBuilder,
LayoutAnimationFunction,
LayoutAnimationValues,
StyleProps,
} from './commonTypes';
import type { NestedArray } from './createAnimatedComponent/commonTypes';
LayoutAnimationsValues,
} from './reanimated2/layoutReanimation';
import type { StyleProps } from './reanimated2/commonTypes';
const mockTargetValues: LayoutAnimationValues = {
const mockTargetValues: LayoutAnimationsValues = {
targetOriginX: 0,
targetOriginY: 0,
targetWidth: 0,
@@ -27,57 +25,12 @@ const mockTargetValues: LayoutAnimationValues = {
currentBorderRadius: 0,
};
function getCommonProperties(
layoutStyle: StyleProps,
componentStyle: NestedArray<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) {
logger.warn(
`${
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,
style: StyleProps | undefined,
displayName: string
): LayoutAnimationFunction | Keyframe {
const isAnimationBuilder = (
@@ -88,17 +41,24 @@ export function maybeBuild(
if (isAnimationBuilder(layoutAnimationOrBuilder)) {
const animationFactory = layoutAnimationOrBuilder.build();
const layoutAnimation = animationFactory(mockTargetValues);
const animatedStyle = layoutAnimation.animations;
if (__DEV__ && style) {
const layoutAnimation = animationFactory(mockTargetValues);
maybeReportOverwrittenProperties(
layoutAnimation.animations,
style,
displayName
const getCommonProperties = (obj1: object, obj2: object) =>
Object.keys(obj1).filter((key) =>
Object.prototype.hasOwnProperty.call(obj2, key)
);
const commonProperties = getCommonProperties(animatedStyle, style || {});
if (commonProperties.length > 0) {
console.warn(
`[Reanimated] ${
commonProperties.length === 1 ? 'Property' : 'Properties: '
} "${commonProperties}" of ${displayName} may be overwritten with layout animation. Please create a wrapper with the layout animation you want to apply.`
);
}
return animationFactory;
return layoutAnimationOrBuilder.build();
} else {
return layoutAnimationOrBuilder;
}
@@ -1,22 +0,0 @@
'use strict';
import { Platform } from 'react-native';
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 const IS_ANDROID: boolean = Platform.OS === 'android';
/** @knipIgnore */
export const IS_IOS: boolean = Platform.OS === 'ios';
export const IS_WEB: boolean = Platform.OS === 'web';
export const IS_JEST: boolean = !!process.env.JEST_WORKER_ID;
/** @knipIgnore */
export const IS_WINDOWS: boolean = Platform.OS === 'windows';
export const IS_WINDOW_AVAILABLE: boolean = isWindowAvailable();
export const SHOULD_BE_USE_WEB = IS_JEST || IS_WEB || IS_WINDOWS;
@@ -1,35 +0,0 @@
/* eslint-disable reanimated/use-reanimated-error */
'use strict';
import { RuntimeKind } from 'react-native-worklets';
function ReanimatedErrorConstructor(message: string): ReanimatedError {
'worklet';
const prefix = '[Reanimated]';
const errorInstance = new Error(message ? `${prefix} ${message}` : prefix);
errorInstance.name = 'ReanimatedError';
return errorInstance as ReanimatedError;
}
/**
* Registers ReanimatedError in the global scope. Register only for Worklet
* runtimes.
*/
export function registerReanimatedError() {
'worklet';
if (globalThis.__RUNTIME_KIND !== RuntimeKind.ReactNative) {
globalThis.ReanimatedError =
ReanimatedErrorConstructor as IReanimatedErrorConstructor;
}
}
export const ReanimatedError =
ReanimatedErrorConstructor as IReanimatedErrorConstructor;
export interface IReanimatedErrorConstructor extends Error {
new (message?: string): ReanimatedError;
(message?: string): ReanimatedError;
readonly prototype: ReanimatedError;
}
export type ReanimatedError = Error & { name: 'Reanimated' }; // signed type
@@ -1,7 +0,0 @@
'use strict';
export * from './constants';
export * from './errors';
export * from './logger';
export * from './processors';
export type * from './types';
export * from './utils';
@@ -1,124 +0,0 @@
/* eslint-disable reanimated/use-logger */
'use strict';
const PREFIX = '[Reanimated]';
const DOCS_URL =
'https://docs.swmansion.com/react-native-reanimated/docs/debugging/logger-configuration';
const DOCS_REFERENCE = `If you don't want to see this message, you can disable the \`strict\` mode. Refer to:\n${DOCS_URL} for more details.`;
export enum ReanimatedLogLevel {
warn = 1,
error = 2,
}
type LogData = {
level: ReanimatedLogLevel;
message: string;
};
type LogFunction = (data: LogData) => void;
export type LoggerConfig = {
level?: ReanimatedLogLevel;
strict?: boolean;
};
export type LoggerConfigInternal = {
logFunction: LogFunction;
} & Required<LoggerConfig>;
function logToConsole(data: LogData) {
'worklet';
switch (data.level) {
case ReanimatedLogLevel.warn:
console.warn(data.message);
break;
case ReanimatedLogLevel.error:
console.error(data.message);
break;
}
}
const DEFAULT_LOGGER_CONFIG: LoggerConfigInternal = {
logFunction: logToConsole,
level: ReanimatedLogLevel.warn,
strict: true,
};
/**
* Current logger config getter.
*
* @returns The current logger configuration object.
*/
export function getLoggerConfig() {
'worklet';
if (!global.__reanimatedLoggerConfig) {
global.__reanimatedLoggerConfig = DEFAULT_LOGGER_CONFIG;
}
return global.__reanimatedLoggerConfig;
}
/**
* Updates logger configuration.
*
* @param currentConfig - The current logger configuration object.
* @param options - The new logger configuration to apply.
*
* - Level: The minimum log level to display.
* - Strict: Whether to log warnings and errors that are not strict. Defaults to
* false.
*/
export function updateLoggerConfig(
currentConfig: LoggerConfigInternal,
options?: Partial<LoggerConfig>
) {
'worklet';
global.__reanimatedLoggerConfig = {
...currentConfig,
// Don't reuse previous level and strict values from the current config
level: options?.level ?? DEFAULT_LOGGER_CONFIG.level,
strict: options?.strict ?? DEFAULT_LOGGER_CONFIG.strict,
};
}
type LogOptions = {
strict?: boolean;
};
function handleLog(
level: ReanimatedLogLevel,
message: string,
options: LogOptions
) {
'worklet';
const config = getLoggerConfig();
if (
// Don't log if the log is marked as strict-only and the config doesn't
// enable strict logging
(options.strict && !config.strict) ||
// Don't log if the log level is below the minimum configured level
level < config.level
) {
return;
}
if (options.strict) {
message += `\n\n${DOCS_REFERENCE}`;
}
config.logFunction({
level,
message: `${PREFIX} ${message}`,
});
}
export const logger = {
warn(message: string, options: LogOptions = {}) {
'worklet';
handleLog(ReanimatedLogLevel.warn, message, options);
},
error(message: string, options: LogOptions = {}) {
'worklet';
handleLog(ReanimatedLogLevel.error, message, options);
},
};
@@ -1,35 +0,0 @@
'use strict';
'worklet';
import { ColorProperties, processColorInitially } from '../../Colors';
import type { StyleProps } from '../../commonTypes';
import { IS_ANDROID } from '../constants';
export function processColor(color: unknown): number | null | undefined {
let normalizedColor = processColorInitially(color);
if (typeof normalizedColor !== 'number') {
return normalizedColor;
}
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) {
for (const key in props) {
if (ColorProperties.includes(key)) {
if (Array.isArray(props[key])) {
props[key] = props[key].map((color: unknown) => processColor(color));
} else {
props[key] = processColor(props[key]);
}
}
}
}
@@ -1,4 +0,0 @@
'use strict';
export * from './colors';
export * from './shadows';
export * from './transformOrigin';
@@ -1,108 +0,0 @@
'use strict';
'worklet';
import type { BoxShadowValue } from 'react-native';
import { IS_ANDROID } from '../constants';
import { ReanimatedError } from '../errors';
import type { ValueProcessor } from '../types';
import { maybeAddSuffix, parseBoxShadowString } from '../utils';
import { processColor } from './colors';
const ERROR_MESSAGES = {
notArrayObject: (value: object) =>
`Box shadow value must be a string or an array of shadow objects (e.g. [{ offsetX, offsetY, color }]). Received: ${JSON.stringify(value)}.`,
invalidColor: (color: string, boxShadow: string) =>
`Invalid color "${color}" in box shadow "${boxShadow}".`,
};
export type ProcessedBoxShadowValue = {
offsetX: number;
offsetY: number;
blurRadius?: number;
color?: number;
spreadDistance?: number;
inset?: boolean;
};
const parseBlurRadius = (value: string) => {
if (IS_ANDROID) {
// Android crashes when blurRadius is smaller than 1
return Math.max(parseFloat(value), 1);
}
return parseFloat(value);
};
export const processBoxShadowNative: ValueProcessor<
ReadonlyArray<BoxShadowValue> | string,
ProcessedBoxShadowValue[]
> = (value) => {
if (value === 'none') {
return;
}
const parsedShadow =
typeof value === 'string' ? parseBoxShadowString(value) : value;
if (!Array.isArray(parsedShadow)) {
throw new ReanimatedError(ERROR_MESSAGES.notArrayObject(parsedShadow));
}
return parsedShadow.map<ProcessedBoxShadowValue>((shadow) => {
const {
color = '#000',
offsetX = 0,
offsetY = 0,
spreadDistance = 0,
blurRadius = 0,
...rest
} = shadow;
const processedColor = processColor(color);
if (processedColor === null) {
throw new ReanimatedError(
ERROR_MESSAGES.invalidColor(color, JSON.stringify(shadow))
);
}
return {
...rest,
blurRadius: parseBlurRadius(blurRadius as string),
color: processedColor,
offsetX: parseFloat(offsetX as string),
offsetY: parseFloat(offsetY as string),
spreadDistance: parseFloat(spreadDistance as string),
};
});
};
export const processBoxShadowWeb: ValueProcessor<
string | ReadonlyArray<BoxShadowValue>,
string
> = (value) => {
const parsedShadow =
typeof value === 'string' ? parseBoxShadowString(value) : value;
return parsedShadow
.map(
({
offsetX,
offsetY,
color = '#000',
blurRadius = '',
spreadDistance = '',
inset = '',
}) =>
[
maybeAddSuffix(offsetX, 'px'),
maybeAddSuffix(offsetY, 'px'),
maybeAddSuffix(blurRadius, 'px'),
maybeAddSuffix(spreadDistance, 'px'),
color,
inset ? 'inset' : '',
]
.filter(Boolean)
.join(' ')
)
.join(', ');
};
@@ -1,149 +0,0 @@
'use strict';
'worklet';
import { ReanimatedError } from '../errors';
import type { TransformOrigin, ValueProcessor } from '../types';
type Axis = 'x' | 'y' | 'z';
type ConvertedValue = `${number}%` | number;
type KeywordConversions = Record<string, ConvertedValue>;
type CustomParse = (value: string) => ConvertedValue | null;
const HORIZONTAL_CONVERSIONS = {
left: 0,
center: '50%',
right: '100%',
} satisfies KeywordConversions;
const VERTICAL_CONVERSIONS = {
top: 0,
center: '50%',
bottom: '100%',
} satisfies KeywordConversions;
function getAllowedValues(axis: Axis, isArray: boolean): string {
const allowed: string[] = [];
if (isArray) {
allowed.push('numbers');
} else {
allowed.push('numbers with px unit');
}
allowed.push('percentages');
let keywords: string[] = [];
switch (axis) {
case 'x':
keywords = Object.keys(HORIZONTAL_CONVERSIONS);
break;
case 'y':
keywords = Object.keys(VERTICAL_CONVERSIONS);
break;
}
if (keywords.length) {
allowed.push(`keywords (${keywords.join(', ')})`);
}
// Add "or" before the last item
allowed[allowed.length - 1] = `or ${allowed[allowed.length - 1]}`;
return allowed.join(', ');
}
export const ERROR_MESSAGES = {
invalidTransformOrigin: (value: TransformOrigin) =>
`Invalid transformOrigin: ${JSON.stringify(value)}. Expected 1-3 values.`,
invalidValue: (
value: string | number,
axis: Axis,
origin: TransformOrigin,
isArray: boolean
) =>
`Invalid value "${value}" for the ${axis}-axis in transformOrigin ${JSON.stringify(
origin
)}. Allowed values: ${getAllowedValues(axis, isArray)}.`,
};
function maybeSwapComponents(components: (string | number)[]) {
if (
components[0] in VERTICAL_CONVERSIONS &&
(components[1] === undefined || components[1] in HORIZONTAL_CONVERSIONS)
) {
[components[0], components[1]] = [components[1], components[0]];
}
}
function parseValue(
value: string | number,
allowPercentages: boolean,
customParse: CustomParse,
getError: () => string,
keywordConversions?: KeywordConversions
) {
if (typeof value === 'number') {
return value;
}
if (keywordConversions && value in keywordConversions) {
return keywordConversions[value];
}
if (allowPercentages && value.endsWith('%')) {
const num = parseFloat(value);
if (num === 0) {
return 0;
}
if (!isNaN(num)) {
return `${num}%`;
}
}
const parsed = customParse(value);
if (parsed === null) {
throw new ReanimatedError(getError());
}
return parsed;
}
function parsePx(component: string) {
if (component.endsWith('px') || component === '0') {
const num = parseFloat(component);
if (!isNaN(num)) {
return num;
}
}
return null;
}
export const processTransformOrigin: ValueProcessor<TransformOrigin> = (
value
) => {
const isArray = Array.isArray(value);
const components = isArray ? value : value.split(/\s+/);
const customParse = isArray ? () => null : parsePx;
if (components.length < 1 || components.length > 3) {
throw new ReanimatedError(ERROR_MESSAGES.invalidTransformOrigin(value));
}
maybeSwapComponents(components);
return [
parseValue(
components[0] ?? '50%',
true,
customParse,
() => ERROR_MESSAGES.invalidValue(components[0], 'x', value, isArray),
HORIZONTAL_CONVERSIONS
),
parseValue(
components[1] ?? '50%',
true,
customParse,
() => ERROR_MESSAGES.invalidValue(components[1], 'y', value, isArray),
VERTICAL_CONVERSIONS
),
parseValue(components[2] ?? 0, false, customParse, () =>
ERROR_MESSAGES.invalidValue(components[2], 'z', value, isArray)
),
];
};
@@ -1,12 +0,0 @@
'use strict';
export type Maybe<T> = T | null | undefined;
export type ValueProcessor<V, R = V> = (value: V) => Maybe<R>;
export type TransformOrigin = string | Array<string | number>;
export type NormalizedTransformOrigin = [
`${number}%` | number,
`${number}%` | number,
number,
];
@@ -1,6 +0,0 @@
'use strict';
'worklet';
export const isLength = (value: string) => {
return value.endsWith('px') || !isNaN(Number(value));
};
@@ -1,4 +0,0 @@
'use strict';
export * from './guards';
export * from './parsers';
export * from './suffix';
@@ -1,45 +0,0 @@
'use strict';
'worklet';
import type { BoxShadowValue } from 'react-native';
import { isLength } from '../utils/guards';
const LENGTH_MAPPINGS = [
'offsetX',
'offsetY',
'blurRadius',
'spreadDistance',
] as const;
const SHADOW_PARTS_REGEX = /(?:[^\s()]+|\([^()]*\))+/g;
const SHADOW_SPLIT_REGEX = /(?:[^,()]+|\([^)]*\))+(?=\s*,|$)/g;
export function parseBoxShadowString(value: string) {
if (value === 'none') {
return [];
}
const shadows = value.match(SHADOW_SPLIT_REGEX) || [];
return shadows.map<BoxShadowValue>((shadow) => {
const result: BoxShadowValue = {
offsetX: 0,
offsetY: 0,
};
let foundLengthsCount = 0;
const parts = shadow.match(SHADOW_PARTS_REGEX) || [];
parts.forEach((part) => {
if (isLength(part)) {
result[LENGTH_MAPPINGS[foundLengthsCount++]] = part;
} else if (part === 'inset') {
result.inset = true;
} else {
result.color = part.trim();
}
});
return result;
});
}
@@ -1,9 +0,0 @@
'use strict';
export function hasSuffix(value: unknown): value is string {
return typeof value === 'string' && isNaN(parseInt(value[value.length - 1]));
}
export function maybeAddSuffix(value: unknown, suffix: string) {
return hasSuffix(value) ? value : `${String(value)}${suffix}`;
}
-482
View File
@@ -1,482 +0,0 @@
'use strict';
import type { ComponentRef, RefObject } from 'react';
import type {
ImageStyle,
NativeMethods,
ScrollResponderMixin,
ScrollViewComponent,
TextStyle,
TransformsStyle,
View,
ViewStyle,
} from 'react-native';
import type { SerializableRef, WorkletFunction } from 'react-native-worklets';
import type { Maybe } from './common/types';
import type { CSSAnimationProperties, CSSTransitionProperties } from './css';
import type { AnyRecord } from './css/types';
import type { EasingFunctionFactory } from './Easing';
type LayoutAnimationOptions =
| 'originX'
| 'originY'
| 'width'
| 'height'
| 'borderRadius'
| 'globalOriginX'
| 'globalOriginY';
type CurrentLayoutAnimationValues = {
[K in LayoutAnimationOptions as `current${Capitalize<string & K>}`]: number;
};
type TargetLayoutAnimationValues = {
[K in LayoutAnimationOptions as `target${Capitalize<string & K>}`]: number;
};
interface WindowDimensions {
windowWidth: number;
windowHeight: number;
}
export interface KeyframeProps extends StyleProps {
easing?: EasingFunction | EasingFunctionFactory;
}
type FirstFrame =
| {
0: KeyframeProps & { easing?: never };
from?: never;
}
| {
0?: never;
from: KeyframeProps & { easing?: never };
};
type LastFrame =
| { 100?: KeyframeProps; to?: never }
| { 100?: never; to: KeyframeProps };
export type ValidKeyframeProps = FirstFrame &
LastFrame &
Record<number, KeyframeProps>;
export type MaybeInvalidKeyframeProps = Record<number, KeyframeProps> & {
to?: KeyframeProps;
from?: KeyframeProps;
};
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 = TargetLayoutAnimationValues &
WindowDimensions;
export type ExitAnimationsValues = CurrentLayoutAnimationValues &
WindowDimensions;
export type EntryExitAnimationFunction =
| ((targetValues: EntryAnimationsValues) => LayoutAnimation)
| ((targetValues: ExitAnimationsValues) => LayoutAnimation)
| (() => LayoutAnimation);
export type AnimationConfigFunction<T> = (targetValues: T) => LayoutAnimation;
export type LayoutAnimationValues = CurrentLayoutAnimationValues &
TargetLayoutAnimationValues &
WindowDimensions;
export enum LayoutAnimationType {
ENTERING = 1,
EXITING = 2,
LAYOUT = 3,
}
export type LayoutAnimationFunction = (
targetValues: LayoutAnimationValues
) => LayoutAnimation;
export type LayoutAnimationStartFunction = (
tag: number,
type: LayoutAnimationType,
yogaValues: Partial<LayoutAnimationValues>,
config: (arg: Partial<LayoutAnimationValues>) => LayoutAnimation
) => void;
export interface ILayoutAnimationBuilder {
build: () => LayoutAnimationFunction;
}
export interface BaseLayoutAnimationConfig {
duration?: number;
easing?: EasingFunction | EasingFunctionFactory;
type?: AnimationFunction;
damping?: number;
dampingRatio?: number;
mass?: number;
stiffness?: number;
overshootClamping?: number;
energyThreshold?: 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>;
}
/**
* Used to configure the `.defaultTransitionType()` shared transition modifier.
*
* @experimental
*/
export type EntryExitAnimationsValues =
| EntryAnimationsValues
| ExitAnimationsValues;
export type StylePropsWithArrayTransform = StyleProps & {
transform?: TransformArrayItem[];
};
export interface LayoutAnimationBatchItem {
viewTag: number;
type: LayoutAnimationType;
config: SerializableRef<Keyframe | LayoutAnimationFunction> | undefined;
}
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;
get(): Value;
set(value: Value | ((value: Value) => Value)): void;
addListener: (listenerID: number, listener: (value: Value) => void) => void;
removeListener: (listenerID: number) => void;
modify: (
modifier?: <T extends Value>(value: T) => T,
forceUpdate?: boolean
) => void;
}
/**
* Due to pattern of `MaybeSharedValue` type present in `AnimatedProps`
* (`AnimatedStyle`), contravariance breaks types for animated styles etc.
* Instead of refactoring the code with small chances of success, we just
* disable contravariance for `SharedValue` in this problematic case.
*/
type SharedValueDisableContravariance<Value = unknown> = Omit<
SharedValue<Value>,
'set'
>;
export interface Mutable<Value = unknown> extends SharedValue<Value> {
_isReanimatedSharedValue: true;
_animation?: AnimationObject<Value> | null; // only in Native
/**
* `_value` prop should only be accessed by the `valueSetter` implementation
* which may make the decision about updating the mutable value depending on
* the provided new value. All other places should only attempt to modify the
* mutable by assigning to `value` prop directly or by calling the `set`
* method.
*/
_value: Value;
/**
* Defined only when enabled with a feature flag
* `USE_SYNCHRONIZABLE_FOR_MUTABLES`.
*/
setDirty?: (dirty: boolean) => void;
}
export type MapperRawInputs = unknown[];
export type MapperOutputs = SharedValue[];
export type MapperRegistry = {
start: (
mapperID: number,
worklet: (forceUpdate?: boolean) => void,
inputs: MapperRawInputs,
outputs?: MapperOutputs
) => void;
stop: (mapperID: number) => void;
};
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 = {
__nativeStateShadowNodeWrapper: 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;
isNavigationBarTranslucentAndroid?: 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',
}
export type EasingFunction = (t: number) => number;
export type TransformArrayItem = Extract<
TransformsStyle['transform'],
Array<unknown>
>[number];
type MaybeSharedValue<Value> =
| Value
| (Value extends AnimatableValue
? SharedValueDisableContravariance<Value>
: never);
type MaybeSharedValueRecursive<Value> = Value extends readonly (infer Item)[]
?
| SharedValueDisableContravariance<Item[]>
| (MaybeSharedValueRecursive<Item> | Item)[]
: Value extends object
?
| SharedValueDisableContravariance<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 dependencies on it being generic that it's not feasible at the moment.
export type AnimatedStyle<Style = DefaultStyle> =
| (Style & Partial<CSSAnimationProperties> & Partial<CSSTransitionProperties>) // TODO - maybe add css animation config somewhere else
| MaybeSharedValueRecursive<Style>;
export type AnimatedTransform = MaybeSharedValueRecursive<
TransformsStyle['transform']
>;
export type StyleUpdaterContainer = RefObject<
((forceUpdate: boolean) => void) | undefined
>;
type NativeScrollRef = Maybe<
(
| ComponentRef<typeof View>
| ComponentRef<typeof ScrollViewComponent>
| NativeMethods
) & {
__internalInstanceHandle?: AnyRecord;
}
>;
type InstanceMethods = {
getScrollResponder?: () => Maybe<
(ScrollResponderMixin | React.JSX.Element) & {
getNativeScrollRef?: () => NativeScrollRef;
}
>;
getNativeScrollRef?: () => NativeScrollRef;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getScrollableNode?: () => any;
__internalInstanceHandle?: AnyRecord;
};
export type WrapperRef = (React.Component & InstanceMethods) | InstanceMethods;
@@ -1,165 +0,0 @@
'use strict';
import type { RefObject } from 'react';
import { useMemo, useRef } from 'react';
import type {
FlatListProps,
LayoutChangeEvent,
StyleProp,
ViewStyle,
} from 'react-native';
import { FlatList } from 'react-native';
import type { AnimatedStyle, ILayoutAnimationBuilder } from '../commonTypes';
import { createAnimatedComponent } from '../createAnimatedComponent';
import type { AnimatedProps } from '../helperTypes';
import { LayoutAnimationConfig } from './LayoutAnimationConfig';
import { AnimatedView } from './View';
const AnimatedFlatList = createAnimatedComponent(FlatList);
interface CellRendererComponentProps<ItemT = any> {
index: number;
item: ItemT;
onLayout?: ((event: LayoutChangeEvent) => void) | undefined;
children: React.ReactNode;
style?: StyleProp<AnimatedStyle<ViewStyle>>;
}
const createCellRendererComponent = (
itemLayoutAnimationRef?: RefObject<ILayoutAnimationBuilder | undefined>,
cellRendererComponentStyleRef?: RefObject<
ReanimatedFlatListPropsWithLayout<any>['CellRendererComponentStyle']
>
) => {
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,
typeof cellRendererComponentStyleRef?.current === 'function'
? cellRendererComponentStyleRef?.current({
index: props.index,
item: props.item,
})
: cellRendererComponentStyleRef?.current,
]}>
{props.children}
</AnimatedView>
);
};
return CellRendererComponent;
};
interface ReanimatedFlatListPropsWithLayout<T>
extends AnimatedProps<FlatListProps<T>> {
/**
* Lets you pass layout animation directly to the FlatList item. Works only
* with a single-column `Animated.FlatList`, `numColumns` property cannot be
* greater than 1.
*/
itemLayoutAnimation?: ILayoutAnimationBuilder;
/**
* Lets you skip entering and exiting animations of FlatList items when on
* FlatList mount or unmount.
*/
skipEnteringExitingAnimations?: boolean;
/** Property `CellRendererComponent` is not supported in `Animated.FlatList`. */
CellRendererComponent?: never;
/**
* Either animated view styles or a function that receives the item to be
* rendered and its index and returns animated view styles.
*/
CellRendererComponentStyle?:
| StyleProp<AnimatedStyle<StyleProp<ViewStyle>>>
| (({
item,
index,
}: {
item: T;
index: number;
}) => StyleProp<AnimatedStyle<StyleProp<ViewStyle>>>)
| undefined;
}
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 FlatListRender = function <Item = any>(
props: ReanimatedFlatListPropsWithLayout<Item>,
ref: React.Ref<FlatList>
) {
const {
itemLayoutAnimation,
skipEnteringExitingAnimations,
CellRendererComponentStyle,
...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 cellRendererComponentStyleRef = useRef(CellRendererComponentStyle);
cellRendererComponentStyleRef.current = CellRendererComponentStyle;
const CellRendererComponent = useMemo(
() =>
createCellRendererComponent(
itemLayoutAnimationRef,
cellRendererComponentStyleRef
),
[]
);
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 = FlatListRender 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.Ref<FlatList>;
}
) => React.ReactElement;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ReanimatedFlatList<T = any> = typeof AnimatedFlatList &
AnimatedFlatListComplement<T>;
@@ -1,14 +0,0 @@
'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;
@@ -1,86 +0,0 @@
'use strict';
import type { ReactNode } from 'react';
import { Children, Component, createContext, useEffect, useRef } from 'react';
import { setShouldAnimateExitingForTag } from '../core';
import { findNodeHandle } from '../platformFunctions/findNodeHandle';
export const SkipEnteringContext =
createContext<React.RefObject<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 value={skipValueRef}>
{props.children}
</SkipEnteringContext>
);
}
// 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>
);
}
}
@@ -1,210 +0,0 @@
'use strict';
import { useEffect, useRef } from 'react';
import { StyleSheet, TextInput, View } from 'react-native';
import { createAnimatedComponent } from '../createAnimatedComponent';
import type { FrameInfo } from '../frameCallback';
import { useAnimatedProps, useFrameCallback, useSharedValue } from '../hook';
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 = 20;
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 completeBufferRoutine(
buffer: CircularBuffer,
timestamp: number
): number {
'worklet';
timestamp = Math.round(timestamp);
const droppedTimestamp = buffer.push(timestamp) ?? timestamp;
const measuredRangeDuration = timestamp - droppedTimestamp;
return getFps(measuredRangeDuration / buffer.count);
}
function JsPerformance({ smoothingFrames }: { smoothingFrames: number }) {
const jsFps = useSharedValue<string | null>(null);
const totalRenderTime = useSharedValue(0);
const circularBuffer = useRef<CircularBuffer>(
createCircularDoublesBuffer(smoothingFrames)
);
useEffect(() => {
loopAnimationFrame((_, timestamp) => {
timestamp = Math.round(timestamp);
const currentFps = completeBufferRoutine(
circularBuffer.current,
timestamp
);
// JS fps have to be measured every 2nd frame,
// thus 2x multiplication has to occur here
jsFps.value = (currentFps * 2).toFixed(0);
});
}, [jsFps, totalRenderTime]);
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({ smoothingFrames }: { smoothingFrames: number }) {
const uiFps = useSharedValue<string | null>(null);
const circularBuffer = useSharedValue<CircularBuffer | null>(null);
useFrameCallback(({ timestamp }: FrameInfo) => {
if (circularBuffer.value === null) {
circularBuffer.value = createCircularDoublesBuffer(smoothingFrames);
}
timestamp = Math.round(timestamp);
const currentFps = completeBufferRoutine(circularBuffer.value, timestamp);
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 type PerformanceMonitorProps = {
/**
* Sets amount of previous frames used for smoothing at highest expectedFps.
*
* Automatically scales down at lower frame rates.
*
* Affects jumpiness of the FPS measurements value.
*/
smoothingFrames?: number;
};
/**
* A component that lets you measure fps values on JS and UI threads on both the
* Paper and Fabric architectures.
*
* @param smoothingFrames - Determines amount of saved frames which will be used
* for fps value smoothing.
*/
export function PerformanceMonitor({
smoothingFrames = DEFAULT_BUFFER_SIZE,
}: PerformanceMonitorProps) {
return (
<View style={styles.monitor}>
<JsPerformance smoothingFrames={smoothingFrames} />
<UiPerformance smoothingFrames={smoothingFrames} />
</View>
);
}
const styles = StyleSheet.create({
monitor: {
flexDirection: 'row',
position: 'absolute',
backgroundColor: '#0006',
zIndex: 1000,
},
header: {
fontSize: 14,
color: '#ffff',
paddingHorizontal: 5,
},
text: {
fontSize: 13,
fontVariant: ['tabular-nums'],
color: '#ffff',
fontFamily: 'monospace',
paddingHorizontal: 3,
},
container: {
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
flexWrap: 'wrap',
},
});
@@ -1,47 +0,0 @@
'use strict';
import { useEffect } from 'react';
import { logger } from '../common';
import { ReduceMotion } from '../commonTypes';
import {
isReducedMotionEnabledInSystem,
ReducedMotionManager,
} from '../ReducedMotion';
/**
* A component that lets you overwrite default reduce motion behavior globally
* in your application.
*
* @param mode - Determines default reduce motion behavior globally in your
* application. Configured with {@link ReduceMotion} enum.
* @see https://docs.swmansion.com/react-native-reanimated/docs/device/ReducedMotionConfig
*/
export function ReducedMotionConfig({ mode }: { mode: ReduceMotion }) {
useEffect(() => {
if (!__DEV__) {
return;
}
logger.warn(`Reduced motion setting is overwritten with mode '${mode}'.`);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
const wasEnabled = ReducedMotionManager.jsValue;
switch (mode) {
case ReduceMotion.System:
ReducedMotionManager.setEnabled(isReducedMotionEnabledInSystem());
break;
case ReduceMotion.Always:
ReducedMotionManager.setEnabled(true);
break;
case ReduceMotion.Never:
ReducedMotionManager.setEnabled(false);
break;
}
return () => {
ReducedMotionManager.setEnabled(wasEnabled);
};
}, [mode]);
return null;
}
@@ -1,55 +0,0 @@
'use strict';
import type { Ref } from 'react';
import React from 'react';
import type { ScrollViewProps } from 'react-native';
import { ScrollView } from 'react-native';
import type { SharedValue } from '../commonTypes';
import { createAnimatedComponent } from '../createAnimatedComponent';
import type { AnimatedProps } from '../helperTypes';
import type { AnimatedRef } from '../hook';
import { useAnimatedRef, useScrollOffset } from '../hook';
export interface AnimatedScrollViewProps
extends AnimatedProps<ScrollViewProps> {
scrollViewOffset?: SharedValue<number>;
ref?: Ref<AnimatedScrollView> | null;
}
// 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 function AnimatedScrollView({
scrollViewOffset,
ref,
...restProps
}: AnimatedScrollViewProps) {
const animatedRef =
ref === null
? // eslint-disable-next-line react-hooks/rules-of-hooks
useAnimatedRef<ScrollView>()
: (ref as AnimatedRef<ScrollView>);
if (scrollViewOffset) {
// eslint-disable-next-line react-hooks/rules-of-hooks
useScrollOffset(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;
@@ -1,14 +0,0 @@
'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;
@@ -1,14 +0,0 @@
'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;
-200
View File
@@ -1,200 +0,0 @@
'use strict';
import {
controlEdgeToEdgeValues,
isEdgeToEdge,
} from 'react-native-is-edge-to-edge';
import type { WorkletFunction } from 'react-native-worklets';
import { createSerializable } from 'react-native-worklets';
import { logger, ReanimatedError } from './common';
import type {
AnimatedKeyboardOptions,
LayoutAnimationBatchItem,
SensorConfig,
SensorType,
SharedValue,
Value3D,
ValueRotation,
WrapperRef,
} from './commonTypes';
import { ReanimatedModule } from './ReanimatedModule';
import { SensorContainer } from './SensorContainer';
export { startMapper, stopMapper } from './mappers';
export { makeMutable } from './mutables';
const EDGE_TO_EDGE = isEdgeToEdge();
/**
* @deprecated Please use the exported variable `reanimatedVersion` instead.
* @returns `false` in Reanimated 4, `true` in Reanimated 3, doesn't exist in
* Reanimated 2 or 1
*/
export const isReanimated3 = () => {
logger.warn(
'The `isReanimated3` function is deprecated. Please use the exported variable `reanimatedVersion` instead.'
);
return false;
};
// Superseded by check in `/src/threads.ts`.
// Used by `react-navigation` to detect if using Reanimated 2 or 3.
/**
* @deprecated Please use the exported variable `reanimatedVersion` instead.
* @returns `false` in Reanimated 4, `true` in Reanimated 3, doesn't exist in
* Reanimated 2 or 1
*/
export const isConfigured = isReanimated3;
export function getViewProp<T>(
viewTag: number,
propName: string,
component?: WrapperRef | null // required on Fabric
): Promise<T> {
if (!component) {
throw new ReanimatedError(
'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 ReanimatedModule.getViewProp(
viewTag,
propName,
component,
(result: T) => {
if (typeof result === 'string' && result.slice(0, 6) === 'error:') {
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
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';
// TODO: Fix this and don't call `__flushAnimationFrame` here.
global.__frameTimestamp = eventTimestamp;
eventHandler(event);
global.__flushAnimationFrame(eventTimestamp);
global.__frameTimestamp = undefined;
}
return ReanimatedModule.registerEventHandler(
createSerializable(handleAndFlushAnimationFrame as WorkletFunction),
eventName,
emitterReactTag
);
}
export function unregisterEventHandler(id: number): void {
return ReanimatedModule.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';
// TODO: Fix this and don't call `__flushAnimationFrame` here.
const now = global._getAnimationTimestamp();
global.__frameTimestamp = now;
eventHandler(state, height);
global.__flushAnimationFrame(now);
global.__frameTimestamp = undefined;
}
if (__DEV__) {
controlEdgeToEdgeValues({
isStatusBarTranslucentAndroid: options.isStatusBarTranslucentAndroid,
isNavigationBarTranslucentAndroid:
options.isNavigationBarTranslucentAndroid,
});
}
return ReanimatedModule.subscribeForKeyboardEvents(
createSerializable(handleAndFlushAnimationFrame as WorkletFunction),
EDGE_TO_EDGE || (options.isStatusBarTranslucentAndroid ?? false),
EDGE_TO_EDGE || (options.isNavigationBarTranslucentAndroid ?? false)
);
}
export function unsubscribeFromKeyboardEvents(listenerId: number): void {
return ReanimatedModule.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,
createSerializable(eventHandler as WorkletFunction)
);
}
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);
}
/**
* @deprecated This function no longer has any effect in Reanimated and will be
* removed in the future.
*/
export function enableLayoutAnimations(
_flag: boolean,
_isCallByUser = true
): void {
logger.warn(
'`enableLayoutAnimations` is deprecated and will be removed in the future.'
);
}
export function configureLayoutAnimationBatch(
layoutAnimationsBatch: LayoutAnimationBatchItem[]
): void {
ReanimatedModule.configureLayoutAnimationBatch(layoutAnimationsBatch);
}
export function setShouldAnimateExitingForTag(
viewTag: number | HTMLElement,
shouldAnimate: boolean
) {
ReanimatedModule.setShouldAnimateExitingForTag(
viewTag as number,
shouldAnimate
);
}
@@ -1,460 +0,0 @@
'use strict';
import '../layoutReanimation/animationsManager';
import type React from 'react';
import { maybeBuild } from '../animationBuilder';
import { IS_JEST, IS_WEB, logger } from '../common';
import type { StyleProps } from '../commonTypes';
import { LayoutAnimationType } from '../commonTypes';
import { SkipEnteringContext } from '../component/LayoutAnimationConfig';
import ReanimatedAnimatedComponent from '../css/component/AnimatedComponent';
import type { AnimatedStyleHandle } from '../hook/commonTypes';
import {
configureWebLayoutAnimations,
getReducedMotionFromConfig,
saveSnapshot,
startWebLayoutAnimation,
tryActivateLayoutTransition,
} from '../layoutReanimation/web';
import type { CustomConfig } from '../layoutReanimation/web/config';
import { addHTMLMutationObserver } from '../layoutReanimation/web/domUtils';
import type { ReanimatedHTMLElement } from '../ReanimatedModule/js-reanimated';
import { updateLayoutAnimations } from '../UpdateLayoutAnimations';
import type {
AnimatedComponentProps,
AnimatedComponentRef,
AnimatedProps,
AnyComponent,
IAnimatedComponentInternal,
INativeEventsManager,
InitialComponentProps,
LayoutAnimationOrBuilder,
NestedArray,
} from './commonTypes';
import { InlinePropManager } from './InlinePropManager';
import jsPropsUpdater from './JSPropsUpdater';
import { NativeEventsManager } from './NativeEventsManager';
import { PropsFilter } from './PropsFilter';
import { filterStyles, flattenArray } from './utils';
let id = 0;
if (IS_WEB) {
configureWebLayoutAnimations();
}
export type Options<P> = {
setNativeProps?: (ref: AnimatedComponentRef, props: P) => void;
jsProps?: string[];
};
export default class AnimatedComponent
extends ReanimatedAnimatedComponent<
AnimatedComponentProps<InitialComponentProps>
>
implements IAnimatedComponentInternal
{
_options?: Options<InitialComponentProps>;
_displayName: string;
_animatedStyles: StyleProps[] = [];
_prevAnimatedStyles: StyleProps[] = [];
_animatedProps: Partial<AnimatedComponentProps<AnimatedProps>>[] = [];
_prevAnimatedProps: Partial<AnimatedComponentProps<AnimatedProps>>[] = [];
_isFirstRender = true;
jestInlineStyle: NestedArray<StyleProps> | undefined;
jestAnimatedStyle: { value: StyleProps } = { value: {} };
jestAnimatedProps: { value: AnimatedProps } = { value: {} };
_InlinePropManager = new InlinePropManager();
_PropsFilter = new PropsFilter();
_NativeEventsManager?: INativeEventsManager;
static contextType = SkipEnteringContext;
context!: React.ContextType<typeof SkipEnteringContext>;
reanimatedID = id++;
constructor(
ChildComponent: AnyComponent,
props: AnimatedComponentProps<InitialComponentProps>,
displayName: string,
options?: Options<InitialComponentProps>
) {
super(ChildComponent, props);
this._options = options;
this._displayName = displayName;
if (IS_JEST) {
this.jestAnimatedStyle = { value: {} };
this.jestAnimatedProps = { value: {} };
}
const skipEntering = this.context?.current;
if (!skipEntering) {
this._configureLayoutAnimation(
LayoutAnimationType.ENTERING,
this.props.entering
);
}
}
componentDidMount() {
super.componentDidMount();
if (!IS_WEB) {
// It exists only on native platforms. We initialize it here because the ref to the animated component is available only post-mount
this._NativeEventsManager = new NativeEventsManager(this, this._options);
}
this._NativeEventsManager?.attachEvents();
this._updateAnimatedStylesAndProps();
this._InlinePropManager.attachInlineProps(this, this._getViewInfo());
if (this._options?.jsProps?.length) {
jsPropsUpdater.registerComponent(this, this._options.jsProps);
}
this._configureLayoutAnimation(
LayoutAnimationType.LAYOUT,
this.props.layout
);
this._configureLayoutAnimation(
LayoutAnimationType.EXITING,
this.props.exiting
);
if (IS_WEB && this._componentDOMRef) {
const element = this._componentDOMRef as ReanimatedHTMLElement;
const dummyClone = element.dummyClone;
// If the element was cloned (because of the exiting animation), we need bring it
// back to the DOM
while (dummyClone?.firstChild) {
element.appendChild(dummyClone.firstChild);
}
delete element.dummyClone;
if (this.props.exiting) {
saveSnapshot(element);
}
if (
!this.props.entering ||
getReducedMotionFromConfig(this.props.entering as CustomConfig)
) {
this._isFirstRender = false;
return;
}
const skipEntering = this.context?.current;
if (!skipEntering) {
startWebLayoutAnimation(
this.props,
element,
LayoutAnimationType.ENTERING
);
} else if (element.style) {
element.style.visibility = 'initial';
}
}
this._isFirstRender = false;
}
componentWillUnmount() {
super.componentWillUnmount();
this._NativeEventsManager?.detachEvents();
this._detachStyles();
this._InlinePropManager.detachInlineProps();
if (this._options?.jsProps?.length) {
jsPropsUpdater.unregisterComponent(this);
}
const exiting = this.props.exiting;
if (
IS_WEB &&
this._componentDOMRef &&
exiting &&
!getReducedMotionFromConfig(exiting as CustomConfig)
) {
addHTMLMutationObserver();
startWebLayoutAnimation(
this.props,
this._componentDOMRef as ReanimatedHTMLElement,
LayoutAnimationType.EXITING
);
}
}
_detachStyles() {
const viewTag = this.getComponentViewTag();
if (viewTag !== -1) {
for (const style of this._animatedStyles) {
style.viewDescriptors.remove(viewTag);
}
if (this.props.animatedProps?.viewDescriptors) {
this.props.animatedProps.viewDescriptors.remove(viewTag);
}
}
}
setNativeProps(props: StyleProps) {
if (this._options?.setNativeProps) {
this._options.setNativeProps(
this._componentRef as AnimatedComponentRef,
props
);
} else {
(this._componentRef as AnimatedComponentRef)?.setNativeProps?.(props);
}
}
_handleAnimatedStylesUpdate(
prevStyles: StyleProps[],
currentStyles: StyleProps[],
jestAnimatedStyleOrProps: { value: StyleProps }
) {
const { viewTag, shadowNodeWrapper } = this._getViewInfo();
const newStyles = new Set<StyleProps>(currentStyles);
const isStyleAttached = (style: StyleProps) =>
style.viewDescriptors.has(viewTag);
// remove old styles
if (prevStyles) {
// in most of the cases, views have only a single animated style and it remains unchanged
const hasOneSameStyle =
currentStyles.length === 1 &&
prevStyles.length === 1 &&
currentStyles[0] === prevStyles[0];
if (hasOneSameStyle && isStyleAttached(prevStyles[0])) {
return;
}
// otherwise, remove each style that is not present in new styles
for (const prevStyle of prevStyles) {
const isPresent = currentStyles.some((style) => {
if (style === prevStyle && isStyleAttached(style)) {
newStyles.delete(style);
return true;
}
return false;
});
if (!isPresent) {
prevStyle.viewDescriptors.remove(viewTag);
}
}
}
newStyles.forEach((style) => {
style.viewDescriptors.add(
{
tag: viewTag,
shadowNodeWrapper,
},
style.styleUpdaterContainer
);
if (IS_JEST) {
/**
* We need to connect Jest's TestObject instance whose contains just
* props object with the updateProps() function where we update the
* properties of the component. We can't update props object directly
* because TestObject contains a copy of props - look at render
* function: const props = this._filterNonAnimatedProps(this.props);
*/
Object.assign(jestAnimatedStyleOrProps.value, style.initial.value);
style.jestAnimatedValues.current = jestAnimatedStyleOrProps;
}
});
}
_updateAnimatedStylesAndProps() {
this._handleAnimatedStylesUpdate(
this._prevAnimatedStyles,
this._animatedStyles,
this.jestAnimatedStyle
);
this._handleAnimatedStylesUpdate(
this._prevAnimatedProps,
this._animatedProps,
this.jestAnimatedProps
);
}
componentDidUpdate(
prevProps: AnimatedComponentProps<InitialComponentProps>,
_prevState: Readonly<unknown>,
snapshot: DOMRect | null
) {
this._configureLayoutAnimation(
LayoutAnimationType.LAYOUT,
this.props.layout,
prevProps.layout
);
this._configureLayoutAnimation(
LayoutAnimationType.EXITING,
this.props.exiting,
prevProps.exiting
);
this._NativeEventsManager?.updateEvents(prevProps);
this._updateAnimatedStylesAndProps();
this._InlinePropManager.attachInlineProps(this, this._getViewInfo());
if (IS_WEB && this.props.exiting && this._componentDOMRef) {
saveSnapshot(this._componentDOMRef);
}
if (
IS_WEB &&
snapshot &&
this.props.layout &&
!getReducedMotionFromConfig(this.props.layout as CustomConfig)
) {
tryActivateLayoutTransition(
this.props,
this._componentDOMRef as ReanimatedHTMLElement,
snapshot
);
}
}
_updateStyles(props: AnimatedComponentProps<InitialComponentProps>): void {
const filteredStyles = filterStyles(flattenArray(props.style ?? []));
this._prevAnimatedStyles = this._animatedStyles;
this._animatedStyles = filteredStyles.animatedStyles;
const filteredAnimatedProps = filterStyles(
flattenArray(props.animatedProps ?? [])
);
this._prevAnimatedProps = this._animatedProps;
this._animatedProps = filteredAnimatedProps.animatedStyles;
if (filteredAnimatedProps.cssStyle) {
if (__DEV__ && filteredStyles.cssStyle) {
logger.warn(
'AnimatedComponent: CSS properties cannot be used in style and animatedProps at the same time. Using properties from the style object.'
);
this._cssStyle = filteredStyles.cssStyle;
return;
}
// Add all remaining props to cssStyle object
// (e.g. SVG components are styled via top level props, not via style object)
const mergedProps = {
...props,
...filteredAnimatedProps.cssStyle,
};
delete mergedProps.style;
delete mergedProps.animatedProps;
this._cssStyle = mergedProps;
} else {
this._cssStyle = filteredStyles.cssStyle ?? {};
}
}
_configureLayoutAnimation(
type: LayoutAnimationType,
currentConfig: LayoutAnimationOrBuilder | undefined,
previousConfig?: LayoutAnimationOrBuilder
) {
if (IS_WEB || currentConfig === previousConfig) {
return;
}
updateLayoutAnimations(
type === LayoutAnimationType.ENTERING
? this.reanimatedID
: this.getComponentViewTag(),
type,
currentConfig &&
maybeBuild(
currentConfig,
type === LayoutAnimationType.LAYOUT
? undefined /* We don't have to warn user if style has common properties with animation for LAYOUT */
: this.props?.style,
this._displayName
)
);
}
// This is a component lifecycle method from React, therefore we are not calling it directly.
// It is called before the component gets rerendered. This way we can access components' position before it changed
// and later on, in componentDidUpdate, calculate translation for layout transition.
getSnapshotBeforeUpdate() {
if (
IS_WEB &&
this.props.layout &&
this._componentDOMRef?.getBoundingClientRect
) {
return this._componentDOMRef.getBoundingClientRect();
}
// `getSnapshotBeforeUpdate` has to return value which is not `undefined`.
return null;
}
render() {
const filteredProps = this._PropsFilter.filterNonAnimatedProps(this);
if (IS_JEST) {
filteredProps.jestAnimatedStyle = this.jestAnimatedStyle;
filteredProps.jestAnimatedProps = this.jestAnimatedProps;
}
// Layout animations on web are set inside `componentDidMount` method, which is called after first render.
// Because of that we can encounter a situation in which component is visible for a short amount of time, and later on animation triggers.
// I've tested that on various browsers and devices and it did not happen to me. To be sure that it won't happen to someone else,
// I've decided to hide component at first render. Its visibility is reset in `componentDidMount`.
if (
this._isFirstRender &&
IS_WEB &&
filteredProps.entering &&
!getReducedMotionFromConfig(filteredProps.entering as CustomConfig)
) {
filteredProps.style = Array.isArray(filteredProps.style)
? filteredProps.style.concat([{ visibility: 'hidden' }])
: {
...(filteredProps.style ?? {}),
visibility: 'hidden', // Hide component until `componentDidMount` triggers
};
}
const skipEntering = this.context?.current;
const nativeID = skipEntering ? undefined : `${this.reanimatedID}`;
const jestProps = IS_JEST
? {
jestInlineStyle:
this.props.style && filterOutAnimatedStyles(this.props.style),
jestAnimatedStyle: this.jestAnimatedStyle,
jestAnimatedProps: this.jestAnimatedProps,
}
: {};
return super.render({
nativeID,
...filteredProps,
...jestProps,
});
}
}
function filterOutAnimatedStyles(
style: NestedArray<StyleProps | AnimatedStyleHandle | null | undefined>
): NestedArray<StyleProps | null | undefined> {
if (!style) {
return style;
}
if (!Array.isArray(style)) {
return style?.viewDescriptors ? {} : style;
}
return style
.filter(
(styleElement) => !(styleElement && 'viewDescriptors' in styleElement)
)
.map((styleElement) => {
if (Array.isArray(styleElement)) {
return filterOutAnimatedStyles(styleElement);
}
return styleElement;
});
}
@@ -1,17 +1,24 @@
'use strict';
import type { StyleProps } from '../commonTypes';
import { isSharedValue } from '../isSharedValue';
import { startMapper, stopMapper } from '../mappers';
import { updateProps } from '../updateProps';
import type { ViewDescriptorsSet } from '../ViewDescriptorsSet';
import { makeViewDescriptorsSet } from '../ViewDescriptorsSet';
import type { StyleProps } from '../reanimated2';
import type {
IAnimatedComponentInternal,
AnimatedComponentProps,
AnimatedComponentType,
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)) {
@@ -30,30 +37,29 @@ function inlinePropsHasChanged(
}
for (const key of Object.keys(styles1)) {
if (styles1[key] !== styles2[key]) {
return true;
}
if (styles1[key] !== styles2[key]) return true;
}
return false;
}
function getInlinePropsUpdate(styleValue: StyleProps): unknown {
function getInlinePropsUpdate(inlineProps: Record<string, unknown>) {
'worklet';
if (isSharedValue(styleValue)) {
return styleValue.value;
}
if (Array.isArray(styleValue)) {
return styleValue.map(getInlinePropsUpdate);
}
if (styleValue && typeof styleValue === 'object') {
const update: Record<string, unknown> = {};
for (const [key, value] of Object.entries(styleValue)) {
update[key] = getInlinePropsUpdate(value);
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;
}
return styleValue;
return update;
}
function extractSharedValuesMapFromProps(
@@ -71,14 +77,14 @@ function extractSharedValuesMapFromProps(
if (!style) {
return;
}
for (const [styleKey, styleValue] of Object.entries(style)) {
for (const [key, styleValue] of Object.entries(style)) {
if (isSharedValue(styleValue)) {
inlineProps[styleKey] = styleValue;
inlineProps[key] = styleValue;
} else if (
styleKey === 'transform' &&
key === 'transform' &&
isInlineStyleTransform(styleValue)
) {
inlineProps[styleKey] = styleValue;
inlineProps[key] = styleValue;
}
}
});
@@ -108,7 +114,7 @@ export function getInlineStyle(
isFirstRender: boolean
) {
if (isFirstRender) {
return getInlinePropsUpdate(style) as Record<string, unknown>;
return getInlinePropsUpdate(style);
}
const newStyle: StyleProps = {};
for (const [key, styleValue] of Object.entries(style)) {
@@ -128,7 +134,8 @@ export class InlinePropManager implements IInlinePropManager {
_inlineProps: StyleProps = {};
public attachInlineProps(
animatedComponent: AnimatedComponentType,
animatedComponent: React.Component<unknown, unknown> &
IAnimatedComponentInternal,
viewInfo: ViewInfo
) {
const newInlineProps: Record<string, unknown> =
@@ -139,20 +146,31 @@ export class InlinePropManager implements IInlinePropManager {
if (!this._inlinePropsViewDescriptors) {
this._inlinePropsViewDescriptors = makeViewDescriptorsSet();
const { viewTag, shadowNodeWrapper } = viewInfo;
const { viewTag, viewName, shadowNodeWrapper, viewConfig } = viewInfo;
if (Object.keys(newInlineProps).length && viewConfig) {
adaptViewConfig(viewConfig);
}
this._inlinePropsViewDescriptors.add({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
tag: viewTag as number,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
name: viewName!,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
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);
updateProps(shareableViewDescriptors, update, maybeViewRef);
};
this._inlineProps = newInlineProps;
if (this._inlinePropsMapperId) {
@@ -1,55 +1,124 @@
'use strict';
import { runOnUI } from 'react-native-worklets';
import { SHOULD_BE_USE_WEB } from '../common';
import {
NativeEventEmitter,
NativeModules,
findNodeHandle,
} from 'react-native';
import { shouldBeUseWeb } from '../reanimated2/PlatformChecker';
import type { StyleProps } from '../reanimated2';
import { runOnJS, runOnUIImmediately } from '../reanimated2/threads';
import type {
AnimatedComponentProps,
AnimatedComponentType,
IAnimatedComponentInternal,
IJSPropsUpdater,
InitialComponentProps,
JSPropsOperation,
} from './commonTypes';
class JSPropsUpdaterNative implements IJSPropsUpdater {
private static _tagToComponentMapping = new Map<
number,
AnimatedComponentType
>();
interface ListenerData {
viewTag: number;
props: StyleProps;
}
public registerComponent(
animatedComponent: AnimatedComponentType,
jsProps: string[]
const SHOULD_BE_USE_WEB = shouldBeUseWeb();
class JSPropsUpdaterPaper implements IJSPropsUpdater {
private static _tagToComponentMapping = new Map();
private _reanimatedEventEmitter: NativeEventEmitter;
constructor() {
this._reanimatedEventEmitter = new NativeEventEmitter(
NativeModules.ReanimatedModule
);
}
public addOnJSPropsChangeListener(
animatedComponent: React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
IAnimatedComponentInternal
) {
const viewTag = animatedComponent.getComponentViewTag();
JSPropsUpdaterNative._tagToComponentMapping.set(viewTag, animatedComponent);
runOnUI(() => {
global._tagToJSPropNamesMapping[viewTag] = Object.fromEntries(
jsProps.map((propName) => [propName, true])
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 unregisterComponent(animatedComponent: AnimatedComponentType) {
const viewTag = animatedComponent.getComponentViewTag();
JSPropsUpdaterNative._tagToComponentMapping.delete(viewTag);
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'
);
}
}
}
runOnUI(() => {
delete global._tagToJSPropNamesMapping[viewTag];
})();
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 updateProps(operations: JSPropsOperation[]) {
operations.forEach(({ tag, updates }) => {
const component = JSPropsUpdaterNative._tagToComponentMapping.get(tag);
component?.setNativeProps(updates);
});
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 registerComponent(
public addOnJSPropsChangeListener(
_animatedComponent: React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
@@ -58,7 +127,7 @@ class JSPropsUpdaterWeb implements IJSPropsUpdater {
// noop
}
public unregisterComponent(
public removeOnJSPropsChangeListener(
_animatedComponent: React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
@@ -66,23 +135,20 @@ class JSPropsUpdaterWeb implements IJSPropsUpdater {
) {
// noop
}
public updateProps(_operations: JSPropsOperation[]) {
// noop
}
}
type JSPropsUpdaterOptions =
| typeof JSPropsUpdaterWeb
| typeof JSPropsUpdaterNative;
| typeof JSPropsUpdaterFabric
| typeof JSPropsUpdaterPaper;
let JSPropsUpdater: JSPropsUpdaterOptions;
if (SHOULD_BE_USE_WEB) {
JSPropsUpdater = JSPropsUpdaterWeb;
} else if (global._IS_FABRIC) {
JSPropsUpdater = JSPropsUpdaterFabric;
} else {
JSPropsUpdater = JSPropsUpdaterNative;
JSPropsUpdater = JSPropsUpdaterPaper;
}
const jsPropsUpdater = new JSPropsUpdater();
export default jsPropsUpdater;
export default JSPropsUpdater;
@@ -1,150 +0,0 @@
'use strict';
import { findNodeHandle } from '../platformFunctions/findNodeHandle';
import { WorkletEventHandler } from '../WorkletEventHandler';
import type {
AnimatedComponentProps,
AnimatedComponentRef,
INativeEventsManager,
InitialComponentProps,
ManagedAnimatedComponent,
} from './commonTypes';
import { has } from './utils';
export class NativeEventsManager implements INativeEventsManager {
readonly #managedComponent: ManagedAnimatedComponent;
readonly #componentOptions?: ComponentOptions;
#eventViewTag = -1;
constructor(component: ManagedAnimatedComponent, options?: ComponentOptions) {
this.#managedComponent = component;
this.#componentOptions = options;
this.#eventViewTag = this.getEventViewTag();
}
public attachEvents() {
executeForEachEventHandler(this.#managedComponent.props, (key, handler) => {
handler.registerForEvents(this.#eventViewTag, key);
});
}
public detachEvents() {
executeForEachEventHandler(
this.#managedComponent.props,
(_key, handler) => {
handler.unregisterFromEvents(this.#eventViewTag);
}
);
}
public updateEvents(
prevProps: AnimatedComponentProps<InitialComponentProps>
) {
const computedEventTag = this.getEventViewTag(true);
// If the event view tag changes, we need to completely re-mount all events
if (this.#eventViewTag !== computedEventTag) {
// Remove all bindings from previous props that ran on the old viewTag
executeForEachEventHandler(prevProps, (_key, handler) => {
handler.unregisterFromEvents(this.#eventViewTag);
});
// We don't need to unregister from current (new) props, because their events weren't registered yet
// Replace the view tag
this.#eventViewTag = computedEventTag;
// Attach the events with a new viewTag
this.attachEvents();
return;
}
executeForEachEventHandler(prevProps, (key, prevHandler) => {
const newProp = this.#managedComponent.props[key];
if (!newProp) {
// Prop got deleted
prevHandler.unregisterFromEvents(this.#eventViewTag);
} else if (
isWorkletEventHandler(newProp) &&
newProp.workletEventHandler !== prevHandler
) {
// Prop got changed
prevHandler.unregisterFromEvents(this.#eventViewTag);
newProp.workletEventHandler.registerForEvents(this.#eventViewTag);
}
});
executeForEachEventHandler(this.#managedComponent.props, (key, handler) => {
if (!prevProps[key]) {
// Prop got added
handler.registerForEvents(this.#eventViewTag);
}
});
}
private getEventViewTag(componentUpdate: boolean = false) {
// Get the tag for registering events - since the event emitting view can be nested inside the main component
const componentAnimatedRef = this.#managedComponent
._componentRef as AnimatedComponentRef & { __nativeTag?: number };
if (componentAnimatedRef?.getScrollableNode) {
/*
In most cases, getScrollableNode() returns a view tag, and findNodeHandle is not required.
However, to cover more exotic list cases, we will continue to use findNodeHandle
for consistency. For numerical values, findNodeHandle should return the value immediately,
as documented here: https://github.com/facebook/react/blob/91061073d57783c061889ac6720ef1ab7f0c2149/packages/react-native-renderer/src/ReactNativePublicCompat.js#L113
*/
const scrollableNode = componentAnimatedRef.getScrollableNode();
if (typeof scrollableNode === 'number') {
return scrollableNode;
}
return findNodeHandle(scrollableNode) ?? -1;
}
if (this.#componentOptions?.setNativeProps) {
// This case ensures backward compatibility with components that
// have their own setNativeProps method passed as an option.
return findNodeHandle(this.#managedComponent) ?? -1;
}
if (!componentUpdate) {
// On the first render of a component, we may already receive a resolved view tag.
return this.#managedComponent.getComponentViewTag();
}
if (componentAnimatedRef?.__nativeTag) {
return componentAnimatedRef.__nativeTag ?? -1;
}
/*
When a component is updated, a child could potentially change and have a different
view tag. This can occur with a GestureDetector component.
*/
return findNodeHandle(componentAnimatedRef) ?? -1;
}
}
function isWorkletEventHandler(
prop: unknown
): prop is WorkletEventHandlerHolder {
return (
has('workletEventHandler', prop) &&
prop.workletEventHandler instanceof WorkletEventHandler
);
}
function executeForEachEventHandler(
props: AnimatedComponentProps<InitialComponentProps>,
callback: (
key: string,
handler: InstanceType<typeof WorkletEventHandler>
) => void
) {
for (const key in props) {
const prop = props[key];
if (isWorkletEventHandler(prop)) {
callback(key, prop.workletEventHandler);
}
}
}
type ComponentOptions = {
setNativeProps?: (
ref: AnimatedComponentRef,
props: InitialComponentProps
) => void;
};
type WorkletEventHandlerHolder = {
workletEventHandler: InstanceType<typeof WorkletEventHandler>;
};
@@ -1,19 +1,19 @@
'use strict';
import { initialUpdaterRun } from '../animation';
import type { StyleProps } from '../commonTypes';
import type { AnimatedStyleHandle } from '../hook/commonTypes';
import { isSharedValue } from '../isSharedValue';
import { WorkletEventHandler } from '../WorkletEventHandler';
import type { StyleProps, SharedValue } from '../reanimated2';
import { isSharedValue } from '../reanimated2';
import { isChromeDebugger } from '../reanimated2/PlatformChecker';
import WorkletEventHandler from '../reanimated2/WorkletEventHandler';
import { initialUpdaterRun } from '../reanimated2/animation';
import { hasInlineStyles, getInlineStyle } from './InlinePropManager';
import type {
AnimatedComponentProps,
AnimatedComponentType,
AnimatedProps,
InitialComponentProps,
IAnimatedComponentInternal,
IPropsFilter,
} from './commonTypes';
import { getInlineStyle, hasInlineStyles } from './InlinePropManager';
import { flattenArray, has } from './utils';
import { StyleSheet } from 'react-native';
function dummyListener() {
// empty listener we use to assign to listener properties for which animated
@@ -21,68 +21,56 @@ function dummyListener() {
}
export class PropsFilter implements IPropsFilter {
private _initialPropsMap = new Map<AnimatedStyleHandle, StyleProps>();
private _initialStyle = {};
public filterNonAnimatedProps(
component: AnimatedComponentType
component: React.Component<unknown, unknown> & IAnimatedComponentInternal
): Record<string, unknown> {
const inputProps =
component.props as AnimatedComponentProps<InitialComponentProps>;
const props: Record<string, unknown> = {};
for (const key in inputProps) {
const value = inputProps[key];
if (key === 'style') {
const styleProp = inputProps.style;
const styles = flattenArray<StyleProps>(styleProp ?? []);
const processedStyle: StyleProps[] = styles.map((style) => {
if (style?.viewDescriptors) {
const handle = style as AnimatedStyleHandle;
const processedStyle: StyleProps = styles.map((style) => {
if (style && style.viewDescriptors) {
// this is how we recognize styles returned by useAnimatedStyle
style.viewsRef.add(component);
if (component._isFirstRender) {
this._initialPropsMap.set(handle, {
...handle.initial.value,
...initialUpdaterRun(handle.initial.updater),
} as StyleProps);
this._initialStyle = {
...style.initial.value,
...this._initialStyle,
...initialUpdaterRun<StyleProps>(style.initial.updater),
};
}
return this._initialPropsMap.get(handle) ?? {};
return this._initialStyle;
} else if (hasInlineStyles(style)) {
return getInlineStyle(style, component._isFirstRender);
} else {
return style;
}
});
// keep styles as they were passed by the user
// it will help other libs to interpret styles correctly
props[key] = processedStyle;
props[key] = StyleSheet.flatten(processedStyle);
} else if (key === 'animatedProps') {
const animatedPropsProp = inputProps.animatedProps;
const animatedPropsArray = flattenArray<
Partial<AnimatedComponentProps<AnimatedProps>>
>(animatedPropsProp ?? []);
animatedPropsArray.forEach((animatedProps) => {
if (animatedProps?.viewDescriptors && animatedProps.initial) {
Object.keys(animatedProps.initial.value).forEach(
(initialValueKey) => {
props[initialValueKey] =
animatedProps.initial?.value[initialValueKey];
}
);
}
});
const animatedProp = inputProps.animatedProps as Partial<
AnimatedComponentProps<AnimatedProps>
>;
if (animatedProp.initial !== undefined) {
Object.keys(animatedProp.initial.value).forEach((key) => {
props[key] = animatedProp.initial?.value[key];
animatedProp.viewsRef?.add(component);
});
}
} else if (
has('workletEventHandler', value) &&
value.workletEventHandler instanceof WorkletEventHandler
has('current', value) &&
value.current 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]
if (value.current.eventNames.length > 0) {
value.current.eventNames.forEach((eventName) => {
props[eventName] = has('listeners', value.current)
? (value.current.listeners as Record<string, unknown>)[eventName]
: dummyListener;
});
} else {
@@ -90,9 +78,9 @@ export class PropsFilter implements IPropsFilter {
}
} else if (isSharedValue(value)) {
if (component._isFirstRender) {
props[key] = value.value;
props[key] = (value as SharedValue<unknown>).value;
}
} else {
} else if (key !== 'onGestureHandlerStateChange' || !isChromeDebugger()) {
props[key] = value;
}
}
@@ -1,35 +1,32 @@
'use strict';
import type { Component, Ref, RefObject } from 'react';
import type { Ref, Component } from 'react';
import type {
AnimatedStyle,
EntryExitAnimationFunction,
ILayoutAnimationBuilder,
ShadowNodeWrapper,
SharedValue,
StyleProps,
StyleUpdaterContainer,
} from '../commonTypes';
import type { SkipEnteringContext } from '../component/LayoutAnimationConfig';
import type { BaseAnimationBuilder } from '../layoutReanimation';
import type { ViewDescriptorsSet } from '../ViewDescriptorsSet';
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>;
styleUpdaterContainer?: StyleUpdaterContainer;
}
export interface ViewInfo {
viewTag: number | AnimatedComponentRef | HTMLElement | null;
viewTag: number | HTMLElement | null;
viewName: string | null;
shadowNodeWrapper: ShadowNodeWrapper | null;
// This is a React host instance view name which might differ from the
// Fabric component name. For clarity, we use the viewName property
// here and componentName in C++ after converting react viewName to
// Fabric component name.
// (see react/renderer/componentregistry/componentNameByReactViewName.cpp)
viewName?: string;
DOMElement?: HTMLElement | null;
viewConfig: ViewConfig;
}
export interface IInlinePropManager {
@@ -40,49 +37,31 @@ export interface IInlinePropManager {
detachInlineProps(): void;
}
export type AnimatedComponentType = React.Component<unknown, unknown> &
IAnimatedComponentInternal;
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
export type PropUpdates = StyleProps | AnimatedStyle<any>;
export interface IPropsFilter {
filterNonAnimatedProps: (
component: AnimatedComponentType
component: React.Component<unknown, unknown> & IAnimatedComponentInternal
) => Record<string, unknown>;
}
export type JSPropsOperation = {
tag: number;
updates: StyleProps;
};
export interface IJSPropsUpdater {
registerComponent(
animatedComponent: AnimatedComponentType,
jsProps: string[]
addOnJSPropsChangeListener(
animatedComponent: React.Component<unknown, unknown> &
IAnimatedComponentInternal
): void;
removeOnJSPropsChangeListener(
animatedComponent: React.Component<unknown, unknown> &
IAnimatedComponentInternal
): void;
unregisterComponent(animatedComponent: AnimatedComponentType): void;
updateProps(operations: JSPropsOperation[]): void;
}
export interface INativeEventsManager {
attachEvents(): void;
detachEvents(): void;
updateEvents(prevProps: AnimatedComponentProps<InitialComponentProps>): void;
}
export type LayoutAnimationStaticContext = {
presetName: string;
};
export type AnimatedComponentProps<
P extends Record<string, unknown> = Record<string, unknown>,
> = P & {
ref?: Ref<Component>;
export type AnimatedComponentProps<P extends Record<string, unknown>> = P & {
forwardedRef?: Ref<Component>;
style?: NestedArray<StyleProps>;
animatedProps?: Partial<AnimatedComponentProps<AnimatedProps>>;
jestAnimatedValues?: RefObject<AnimatedProps>;
animatedStyle?: StyleProps;
layout?: (
| BaseAnimationBuilder
@@ -104,67 +83,34 @@ export type AnimatedComponentProps<
| Keyframe
) &
LayoutAnimationStaticContext;
sharedTransitionTag?: string;
sharedTransitionStyle?: SharedTransition;
};
export type LayoutAnimationOrBuilder = (
| BaseAnimationBuilder
| typeof BaseAnimationBuilder
| EntryExitAnimationFunction
| Keyframe
| ILayoutAnimationBuilder
) &
LayoutAnimationStaticContext;
export interface AnimatedComponentRef extends Component {
setNativeProps?: (props: Record<string, unknown>) => void;
getScrollableNode?: () => AnimatedComponentRef;
getAnimatableRef?: () => AnimatedComponentRef;
// Case for SVG components on Web
elementRef?: React.RefObject<HTMLElement>;
}
export interface IAnimatedComponentInternalBase {
ChildComponent: AnyComponent;
_componentRef: AnimatedComponentRef | HTMLElement | null;
_hasAnimatedRef: boolean;
_viewInfo?: ViewInfo;
/**
* Used for Layout Animations and Animated Styles. It is not related to event
* handling.
*/
getComponentViewTag: () => number;
}
export interface IAnimatedComponentInternal
extends IAnimatedComponentInternalBase {
_animatedStyles: StyleProps[];
_prevAnimatedStyles: StyleProps[];
_animatedProps: Partial<AnimatedComponentProps<AnimatedProps>>[];
_prevAnimatedProps: Partial<AnimatedComponentProps<AnimatedProps>>[];
export interface IAnimatedComponentInternal {
_styles: StyleProps[] | null;
_animatedProps?: Partial<AnimatedComponentProps<AnimatedProps>>;
_viewTag: number;
_isFirstRender: boolean;
jestInlineStyle: NestedArray<StyleProps> | undefined;
jestAnimatedStyle: { value: StyleProps };
jestAnimatedProps: { value: AnimatedProps };
animatedStyle: { value: StyleProps };
_component: AnimatedComponentRef | HTMLElement | null;
_sharedElementTransition: SharedTransition | null;
_jsPropsUpdater: IJSPropsUpdater;
_InlinePropManager: IInlinePropManager;
_PropsFilter: IPropsFilter;
/** Doesn't exist on web. */
_NativeEventsManager?: INativeEventsManager;
_viewInfo?: ViewInfo;
context: React.ContextType<typeof SkipEnteringContext>;
setNativeProps: (props: StyleProps) => void;
}
export type NestedArray<T> = T | NestedArray<T>[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyComponent = React.ComponentType<any>;
export interface InitialComponentProps extends Record<string, unknown> {
ref?: Ref<Component>;
collapsable?: boolean;
}
export type ManagedAnimatedComponent = React.Component<
AnimatedComponentProps<InitialComponentProps>
> &
IAnimatedComponentInternal;
@@ -1,24 +1,81 @@
'use strict';
import type {
Component,
ComponentClass,
ComponentType,
FunctionComponent,
Ref,
MutableRefObject,
} from 'react';
import React from 'react';
import type { FlatList, FlatListProps } from 'react-native';
import type { AnimatedProps } from '../helperTypes';
import type { Options } from './AnimatedComponent';
import AnimatedComponentImpl from './AnimatedComponent';
import { 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 {
configureLayoutAnimations,
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,
} from '../reanimated2/layoutReanimation/web';
import type { CustomConfig } from '../reanimated2/layoutReanimation/web/config';
import type { FlatList, FlatListProps } from 'react-native';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnimatableComponent<C extends ComponentType<any>> = C & {
jsProps?: string[];
const IS_WEB = isWeb();
const IS_FABRIC = isFabric();
function onlyAnimatedStyles(styles: StyleProps[]): StyleProps[] {
return styles.filter((style) => style?.viewDescriptors);
}
function isSameAnimatedStyle(
style1?: StyleProps,
style2?: StyleProps
): boolean {
// We cannot use equality check to compare useAnimatedStyle outputs directly.
// Instead, we can compare its viewsRefs.
return style1?.viewsRef === style2?.viewsRef;
}
const isSameAnimatedProps = isSameAnimatedStyle;
type Options<P> = {
setNativeProps: (ref: AnimatedComponentRef, props: P) => void;
};
/**
@@ -29,70 +86,517 @@ type AnimatableComponent<C extends ComponentType<any>> = C & {
* @see https://docs.swmansion.com/react-native-reanimated/docs/core/createAnimatedComponent
*/
// Don't change the order of overloads, since such a change breaks current behavior
export function createAnimatedComponent<P extends object>(
component: AnimatableComponent<FunctionComponent<P>>,
options?: Options<P>
): FunctionComponent<AnimatedProps<P>>;
export function createAnimatedComponent<P extends object>(
component: AnimatableComponent<ComponentClass<P>>,
options?: Options<P>
): ComponentClass<AnimatedProps<P>>;
export function createAnimatedComponent<P extends object>(
// Actually ComponentType<P = {}> = ComponentClass<P> | FunctionComponent<P> but we need this overload too
// since some external components (like FastImage) are typed just as ComponentType
component: AnimatableComponent<ComponentType<P>>,
options?: Options<P>
): FunctionComponent<AnimatedProps<P>> | ComponentClass<AnimatedProps<P>>;
/**
* @deprecated Please use `Animated.FlatList` component instead of calling
* `Animated.createAnimatedComponent(FlatList)` manually.
* @deprecated Please use `Animated.FlatList` component instead of calling `Animated.createAnimatedComponent(FlatList)` manually.
*/
// @ts-ignore This is required to create this overload, since type of createAnimatedComponent is incorrect and doesn't include typeof FlatList
export function createAnimatedComponent(
component: AnimatableComponent<typeof FlatList<unknown>>,
options?: Options<typeof FlatList<unknown>>
): ComponentClass<AnimatedProps<FlatListProps<unknown>>>;
component: typeof FlatList<unknown>,
options?: Options<any>
): ComponentClass<AnimateProps<FlatListProps<unknown>>>;
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(
Component: AnimatableComponent<ComponentType<InitialComponentProps>>,
Component: ComponentType<InitialComponentProps>,
options?: Options<InitialComponentProps>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): any {
class AnimatedComponent extends AnimatedComponentImpl {
static displayName = `AnimatedComponent(${
Component.displayName || Component.name || 'Component'
})`;
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;
animatedStyle: { 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>) {
// User can override component-defined jsProps via options
const jsProps = options?.jsProps ?? Component.jsProps;
const modifiedOptions = jsProps?.length
? { ...options, jsProps }
: options;
super(Component, props, AnimatedComponent.displayName, modifiedOptions);
super(props);
if (isJest()) {
this.animatedStyle = { value: {} };
}
}
componentDidMount() {
this._attachNativeEvents();
this._jsPropsUpdater.addOnJSPropsChangeListener(this);
this._attachAnimatedStyles();
this._InlinePropManager.attachInlineProps(this, this._getViewInfo());
if (IS_WEB) {
configureWebLayoutAnimations();
if (!this.props.entering) {
this._isFirstRender = false;
return;
}
if (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();
this._sharedElementTransition?.unregisterTransition(this._viewTag);
if (
IS_WEB &&
this.props.exiting &&
!getReducedMotionFromConfig(this.props.exiting as CustomConfig)
) {
startWebLayoutAnimation(
this.props,
this._component as HTMLElement,
LayoutAnimationType.EXITING
);
}
}
_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() {
const node = this._getEventViewRef() as AnimatedComponentRef;
let viewTag = null; // We set it only if needed
for (const key in this.props) {
const prop = this.props[key];
if (
has('current', prop) &&
prop.current instanceof WorkletEventHandler
) {
if (viewTag === null) {
viewTag = findNodeHandle(options?.setNativeProps ? this : node);
}
prop.current.registerForEvents(viewTag as number, key);
}
}
}
_detachNativeEvents() {
for (const key in this.props) {
const prop = this.props[key];
if (
has('current', prop) &&
prop.current instanceof WorkletEventHandler
) {
prop.current.unregisterFromEvents();
}
}
}
_detachStyles() {
if (IS_WEB && this._styles !== null) {
for (const style of this._styles) {
if (style?.viewsRef) {
style.viewsRef.remove(this);
}
}
} else if (this._viewTag !== -1 && this._styles !== null) {
for (const style of this._styles) {
style.viewDescriptors.remove(this._viewTag);
}
if (this.props.animatedProps?.viewDescriptors) {
this.props.animatedProps.viewDescriptors.remove(this._viewTag);
}
if (IS_FABRIC) {
removeFromPropsRegistry(this._viewTag);
}
}
}
_reattachNativeEvents(
prevProps: AnimatedComponentProps<InitialComponentProps>
) {
for (const key in prevProps) {
const prop = this.props[key];
if (
has('current', prop) &&
prop.current instanceof WorkletEventHandler &&
prop.current.reattachNeeded
) {
prop.current.unregisterFromEvents();
}
}
let viewTag = null;
for (const key in this.props) {
const prop = this.props[key];
if (
has('current', prop) &&
prop.current instanceof WorkletEventHandler &&
prop.current.reattachNeeded
) {
if (viewTag === null) {
const node = this._getEventViewRef() as AnimatedComponentRef;
viewTag = findNodeHandle(options?.setNativeProps ? this : node);
}
prop.current.registerForEvents(viewTag as number, key);
prop.current.reattachNeeded = false;
}
}
}
_updateFromNative(props: StyleProps) {
if (options?.setNativeProps) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
options.setNativeProps(this._component as AnimatedComponentRef, props);
} else {
// eslint-disable-next-line no-unused-expressions
(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?'
);
}
// we can access view tag in the same way it's accessed here https://github.com/facebook/react/blob/e3f4eb7272d4ca0ee49f27577156b57eeb07cf73/packages/react-native-renderer/src/ReactFabric.js#L146
viewTag = hostInstance?._nativeTag;
/**
* RN uses viewConfig for components for storing different properties of the component(example: https://github.com/facebook/react-native/blob/main/packages/react-native/Libraries/Components/ScrollView/ScrollViewNativeComponent.js#L24).
* The name we're looking for is in the field named uiViewClassName.
*/
viewName = hostInstance?.viewConfig?.uiViewClassName;
viewConfig = hostInstance?.viewConfig;
if (IS_FABRIC) {
shadowNodeWrapper = getShadowNodeWrapperFromRef(this);
}
}
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 &&
isSameAnimatedStyle(styles[0], prevStyles[0]);
if (!hasOneSameStyle) {
// otherwise, remove each style that is not present in new styles
for (const prevStyle of prevStyles) {
const isPresent = styles.some((style) =>
isSameAnimatedStyle(style, prevStyle)
);
if (!isPresent) {
prevStyle.viewDescriptors.remove(viewTag);
}
}
}
}
styles.forEach((style) => {
style.viewDescriptors.add({
tag: viewTag,
name: viewName,
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.animatedStyle.value = {
...this.animatedStyle.value,
...style.initial.value,
};
style.animatedStyle.current = this.animatedStyle;
}
});
// detach old animatedProps
if (
prevAnimatedProps &&
!isSameAnimatedProps(prevAnimatedProps, this.props.animatedProps)
) {
prevAnimatedProps.viewDescriptors!.remove(viewTag as number);
}
// attach animatedProps property
if (this.props.animatedProps?.viewDescriptors) {
this.props.animatedProps.viewDescriptors.add({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
tag: viewTag as number,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
name: viewName!,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
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
) {
this._reattachNativeEvents(prevProps);
this._attachAnimatedStyles();
this._InlinePropManager.attachInlineProps(this, this._getViewInfo());
// 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
);
}
}
_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);
const { layout, entering, exiting, sharedTransitionTag } = this.props;
if (
(layout || entering || exiting || sharedTransitionTag) &&
tag != null
) {
if (!shouldBeUseWeb()) {
enableLayoutAnimations(true, false);
}
if (layout) {
configureLayoutAnimations(
tag,
LayoutAnimationType.LAYOUT,
maybeBuild(
layout,
undefined /* We don't have to warn user if style has common properties with animation for LAYOUT */,
AnimatedComponent.displayName
)
);
}
const skipEntering = this.context?.current;
if (entering && !skipEntering) {
configureLayoutAnimations(
tag,
LayoutAnimationType.ENTERING,
maybeBuild(
entering,
this.props?.style,
AnimatedComponent.displayName
)
);
}
if (exiting) {
const reduceMotionInExiting =
'getReduceMotion' in exiting &&
typeof exiting.getReduceMotion === 'function'
? getReduceMotionFromConfig(exiting.getReduceMotion())
: getReduceMotionFromConfig();
if (!reduceMotionInExiting) {
configureLayoutAnimations(
tag,
LayoutAnimationType.EXITING,
maybeBuild(
exiting,
this.props?.style,
AnimatedComponent.displayName
)
);
}
}
if (sharedTransitionTag && !IS_WEB) {
const sharedElementTransition =
this.props.sharedTransitionStyle ?? new SharedTransition();
const reduceMotionInTransition = getReduceMotionFromConfig(
sharedElementTransition.getReduceMotion()
);
if (!reduceMotionInTransition) {
sharedElementTransition.registerTransition(
tag as number,
sharedTransitionTag
);
this._sharedElementTransition = sharedElementTransition;
}
}
}
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 (
(this._component as HTMLElement).getBoundingClientRect !== undefined
) {
return (this._component as HTMLElement).getBoundingClientRect();
}
return null;
}
render() {
const props = this._PropsFilter.filterNonAnimatedProps(this);
if (isJest()) {
props.animatedStyle = this.animatedStyle;
}
// 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 &&
props.entering &&
!getReducedMotionFromConfig(props.entering as CustomConfig)
) {
props.style = {
...(props.style ?? {}),
visibility: 'hidden', // Hide component until `componentDidMount` triggers
};
}
const platformProps = Platform.select({
web: {},
default: { collapsable: false },
});
return (
<Component
{...props}
// 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}
/>
);
}
}
const animatedComponent = (
props: AnimatedComponentProps & { ref: Ref<AnimatedComponent> }
) => {
AnimatedComponent.displayName = `AnimatedComponent(${
Component.displayName || Component.name || 'Component'
})`;
return React.forwardRef<Component>((props, ref) => {
return (
<AnimatedComponent
{...props}
// Needed to prevent react from signing AnimatedComponent to the ref
// (we want to handle the ref assignment in the AnimatedComponent)
ref={null}
{...(props.ref === null ? null : { forwardedRef: props.ref })}
{...(ref === null ? null : { forwardedRef: ref })}
/>
);
};
animatedComponent.displayName =
Component.displayName || Component.name || 'Component';
return animatedComponent;
});
}
@@ -1,15 +0,0 @@
'use strict';
import type { HostInstance } from '../platform-specific/findHostInstance';
export function getViewInfo(element: HostInstance): {
viewName?: string;
viewTag?: number;
} {
return {
viewName: (element?._viewConfig?.uiViewClassName ??
element?.__internalInstanceHandle?.type ??
element?.__internalInstanceHandle?.elementType) as string,
viewTag: element?.__nativeTag,
};
}
@@ -1,6 +1,4 @@
'use strict';
import type { StyleProps } from '../commonTypes';
import type { CSSStyle } from '../css';
import type { NestedArray } from './commonTypes';
export function flattenArray<T>(array: NestedArray<T>): T[] {
@@ -25,7 +23,7 @@ export function flattenArray<T>(array: NestedArray<T>): T[] {
export const has = <K extends string>(
key: K,
x: unknown
): x is { [key in K]: unknown } => {
): x is typeof x & { [key in K]: unknown } => {
if (typeof x === 'function' || typeof x === 'object') {
if (x === null || x === undefined) {
return false;
@@ -35,26 +33,3 @@ export const has = <K extends string>(
}
return false;
};
type FilteredStyles = {
cssStyle: CSSStyle | null;
animatedStyles: StyleProps[];
};
export function filterStyles(styles: StyleProps[] | undefined): FilteredStyles {
if (!styles) {
return { animatedStyles: [], cssStyle: null };
}
return styles.reduce<FilteredStyles>(
({ animatedStyles, cssStyle }, style) => {
if (style?.viewDescriptors) {
animatedStyles.push(style);
} else {
cssStyle = { ...cssStyle, ...style } as CSSStyle;
}
return { animatedStyles, cssStyle };
},
{ animatedStyles: [], cssStyle: null }
);
}
@@ -1,220 +0,0 @@
'use strict';
import type { ComponentProps, Ref } from 'react';
import { Component } from 'react';
import type { StyleProp } from 'react-native';
import { Platform, StyleSheet } from 'react-native';
import { IS_JEST, ReanimatedError, SHOULD_BE_USE_WEB } from '../../common';
import type { ShadowNodeWrapper, WrapperRef } from '../../commonTypes';
import type {
AnimatedComponentRef,
IAnimatedComponentInternalBase,
ViewInfo,
} from '../../createAnimatedComponent/commonTypes';
import { getViewInfo } from '../../createAnimatedComponent/getViewInfo';
import { getShadowNodeWrapperFromRef } from '../../fabricUtils';
import { findHostInstance } from '../../platform-specific/findHostInstance';
import { markNodeAsRemovable, unmarkNodeAsRemovable } from '../native';
import { CSSManager } from '../platform';
import type { AnyComponent, AnyRecord, CSSStyle, PlainStyle } from '../types';
import { filterNonCSSStyleProps } from './utils';
export type AnimatedComponentProps = Record<string, unknown> & {
ref?: Ref<Component>;
style?: StyleProp<PlainStyle>;
};
// TODO - change these ugly underscore prefixed methods and properties to real
// private/protected ones when possible (when changes from this repo are merged
// to the main one)
export default class AnimatedComponent<
P extends AnyRecord = AnimatedComponentProps,
>
extends Component<P>
implements IAnimatedComponentInternalBase
{
ChildComponent: AnyComponent;
_CSSManager?: CSSManager;
_viewInfo?: ViewInfo;
_cssStyle: CSSStyle = {}; // RN style object with Reanimated CSS properties
_componentRef: AnimatedComponentRef | HTMLElement | null = null;
_hasAnimatedRef = false;
// Used only on web
_componentDOMRef: HTMLElement | null = null;
_willUnmount: boolean = false;
constructor(ChildComponent: AnyComponent, props: P) {
super(props);
this.ChildComponent = ChildComponent;
}
getComponentViewTag() {
return this._getViewInfo().viewTag as number;
}
_onSetLocalRef() {
// noop - can be overridden in subclasses
}
_getViewInfo(): ViewInfo {
if (this._viewInfo !== undefined) {
return this._viewInfo;
}
let viewTag: number | typeof this._componentRef;
let shadowNodeWrapper: ShadowNodeWrapper | null = null;
let DOMElement: HTMLElement | null = null;
let viewName: string | undefined;
if (SHOULD_BE_USE_WEB) {
// At this point we 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
// TODO - implement a valid solution later on - this is a temporary fix
viewTag = this._componentRef;
DOMElement = this._componentDOMRef;
} else {
const hostInstance = findHostInstance(this);
if (!hostInstance) {
/*
findHostInstance can return 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
*/
throw new ReanimatedError(
'Cannot find host instance for this component. Maybe it renders nothing?'
);
}
const viewInfo = getViewInfo(hostInstance);
viewTag = viewInfo.viewTag ?? -1;
viewName = viewInfo.viewName;
shadowNodeWrapper = getShadowNodeWrapperFromRef(
this as WrapperRef,
hostInstance
);
}
this._viewInfo = { viewTag, shadowNodeWrapper, viewName };
if (DOMElement) {
this._viewInfo.DOMElement = DOMElement;
}
return this._viewInfo;
}
_setComponentRef = (ref: Component | HTMLElement) => {
const forwardedRef = this.props.forwardedRef;
// 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) {
// Handle createRef-based refs
forwardedRef.current = ref;
}
if (!ref) {
// component has been unmounted
return;
}
if (ref !== this._componentRef) {
this._componentRef = this._resolveComponentRef(ref);
// if ref is changed, reset viewInfo
this._viewInfo = undefined;
}
this._onSetLocalRef();
};
_resolveComponentRef = (ref: Component | HTMLElement | null) => {
const componentRef = ref as AnimatedComponentRef;
// Component can specify ref which should be animated when animated version of the component is created.
// Otherwise, we animate the component itself.
if (componentRef && componentRef.getAnimatableRef) {
this._hasAnimatedRef = true;
return componentRef.getAnimatableRef();
}
// Case for SVG components on Web
if (SHOULD_BE_USE_WEB) {
if (componentRef && componentRef.elementRef) {
this._componentDOMRef = componentRef.elementRef.current;
} else {
this._componentDOMRef = ref as HTMLElement;
}
}
return componentRef;
};
_updateStyles(props: P) {
this._cssStyle = StyleSheet.flatten(props.style) ?? {};
}
componentDidMount() {
this._updateStyles(this.props);
const viewTag = this._viewInfo?.viewTag;
if (
!SHOULD_BE_USE_WEB &&
this._willUnmount &&
typeof viewTag === 'number'
) {
unmarkNodeAsRemovable(viewTag);
}
if (!IS_JEST) {
this._CSSManager ??= new CSSManager(this._getViewInfo());
this._CSSManager?.update(this._cssStyle);
}
this._willUnmount = false;
}
componentWillUnmount() {
if (!IS_JEST && this._CSSManager) {
this._CSSManager.unmountCleanup();
}
const wrapper = this._viewInfo?.shadowNodeWrapper;
if (!SHOULD_BE_USE_WEB && wrapper) {
// Mark node as removable on the native (C++) side, but only actually remove it
// when it no longer exists in the Shadow Tree. This ensures proper cleanup of
// animations/transitions/props while handling cases where the node might be
// remounted (e.g., when frozen) after componentWillUnmount is called.
markNodeAsRemovable(wrapper);
}
this._willUnmount = true;
}
shouldComponentUpdate(nextProps: P) {
this._updateStyles(nextProps);
if (this._CSSManager) {
this._CSSManager.update(this._cssStyle);
}
// TODO - maybe check if the render is necessary instead of always returning true
return true;
}
render(props?: ComponentProps<AnyComponent>) {
const { ChildComponent } = this;
const platformProps = Platform.select({
web: {},
default: { collapsable: false },
});
return (
<ChildComponent
{...(props ?? this.props)}
{...platformProps}
style={filterNonCSSStyleProps(props?.style ?? this.props.style)}
// 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}
/>
);
}
}
@@ -1,71 +0,0 @@
'use strict';
import type {
ComponentClass,
ComponentType,
FunctionComponent,
Ref,
} from 'react';
import React from 'react';
import type { FlatList, FlatListProps } from 'react-native';
import type { CSSProps } from '../types';
import type { AnimatedComponentProps } from './AnimatedComponent';
import AnimatedComponentImpl from './AnimatedComponent';
// Don't change the order of overloads, since such a change breaks current behavior
export default function createAnimatedComponent<P extends object>(
Component: FunctionComponent<P>
): FunctionComponent<CSSProps<P>>;
export default function createAnimatedComponent<P extends object>(
Component: ComponentClass<P>
): ComponentClass<CSSProps<P>>;
export default 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>
): FunctionComponent<CSSProps<P>> | ComponentClass<CSSProps<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 default function createAnimatedComponent(
Component: typeof FlatList<unknown>
): ComponentClass<CSSProps<FlatListProps<unknown>>>;
export default function createAnimatedComponent<P extends object>(
Component: ComponentType<P>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): any {
class AnimatedComponent extends AnimatedComponentImpl {
static displayName = `AnimatedComponent(${
Component.displayName || Component.name || 'Component'
})`;
constructor(props: AnimatedComponentProps) {
super(Component, props);
}
}
const animatedComponent = (
props: AnimatedComponentProps & { ref: Ref<AnimatedComponent> }
) => {
return (
<AnimatedComponent
{...props}
// Needed to prevent react from signing AnimatedComponent to the ref
// (we want to handle the ref assignment in the AnimatedComponent)
ref={null}
{...(props.ref === null ? null : { forwardedRef: props.ref })}
/>
);
};
animatedComponent.displayName =
Component.displayName || Component.name || 'Component';
return animatedComponent;
}
@@ -1,2 +0,0 @@
'use strict';
export { default as createAnimatedComponent } from './createAnimatedComponent';
@@ -1,36 +0,0 @@
'use strict';
import type { StyleProp } from 'react-native';
import type { AnyRecord, CSSStyle } from '../types';
import { isCSSStyleProp } from '../utils/guards';
function filterNonCSSStylePropsRecursive(
props: StyleProp<CSSStyle>
): StyleProp<CSSStyle> {
if (Array.isArray(props)) {
return props.map((prop) =>
filterNonCSSStylePropsRecursive(prop as StyleProp<CSSStyle>)
);
}
if (!props) {
return props;
}
if (typeof props === 'object') {
return Object.entries(props).reduce<AnyRecord>((acc, [key, value]) => {
if (!isCSSStyleProp(key)) {
acc[key] = value;
}
return acc;
}, {});
}
return props;
}
export function filterNonCSSStyleProps(
props: StyleProp<CSSStyle>
): StyleProp<CSSStyle> {
return filterNonCSSStylePropsRecursive(props);
}
@@ -1,15 +0,0 @@
'use strict';
export const FONT_WEIGHT_MAPPINGS = {
thin: '100',
ultralight: '200',
light: '300',
normal: '400',
regular: '400',
medium: '500',
condensed: '500',
semibold: '600',
bold: '700',
condensedBold: '700',
heavy: '800',
black: '900',
} as const;
@@ -1,5 +0,0 @@
'use strict';
export * from './font';
export * from './misc';
export * from './regex';
export * from './settings';
@@ -1,2 +0,0 @@
'use strict';
export const ANIMATION_NAME_PREFIX = 'REA-CSS-';
@@ -1,4 +0,0 @@
'use strict';
export const PERCENTAGE_REGEX = /^-?(\d*\.)?\d+%$/;
export const MILLISECONDS_REGEX = /^-?(\d*\.)?\d+ms$/;
export const SECONDS_REGEX = /^-?(\d*\.)?\d+s$/;
@@ -1,48 +0,0 @@
'use strict';
import type { PredefinedTimingFunction, StepsModifier } from '../easing';
import type { CSSAnimationProp, CSSTransitionProp } from '../types';
export const ANIMATION_PROPS: CSSAnimationProp[] = [
'animationName',
'animationDuration',
'animationTimingFunction',
'animationDelay',
'animationIterationCount',
'animationDirection',
'animationFillMode',
'animationPlayState',
];
export const TRANSITION_PROPS: CSSTransitionProp[] = [
'transitionProperty',
'transitionDuration',
'transitionTimingFunction',
'transitionDelay',
'transitionBehavior',
'transition',
];
export const VALID_STEPS_MODIFIERS: StepsModifier[] = [
'jump-start',
'start',
'jump-end',
'end',
'jump-none',
'jump-both',
];
export const VALID_PREDEFINED_TIMING_FUNCTIONS: PredefinedTimingFunction[] = [
'linear',
'ease',
'ease-in',
'ease-out',
'ease-in-out',
'step-start',
'step-end',
];
export const VALID_PARAMETRIZED_TIMING_FUNCTIONS: string[] = [
'cubic-bezier',
'steps',
'linear',
];
@@ -1,46 +0,0 @@
'use strict';
import { ReanimatedError } from '../../common';
import type {
NormalizedCubicBezierEasing,
ParametrizedTimingFunction,
} from './types';
export const ERROR_MESSAGES = {
invalidCoordinate: (coordinate: string, value: number) =>
`Invalid ${coordinate} coordinate for cubic bezier easing point, it should be a number between 0 and 1, received ${value}`,
};
export class CubicBezierEasing implements ParametrizedTimingFunction {
static readonly easingName = 'cubicBezier';
readonly x1: number;
readonly y1: number;
readonly x2: number;
readonly y2: number;
constructor(x1: number, y1: number, x2: number, y2: number) {
if (x1 < 0 || x1 > 1) {
throw new ReanimatedError(ERROR_MESSAGES.invalidCoordinate('x1', x1));
}
if (x2 < 0 || x2 > 1) {
throw new ReanimatedError(ERROR_MESSAGES.invalidCoordinate('x2', x2));
}
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
}
toString(): string {
return `${CubicBezierEasing.easingName}(${this.x1}, ${this.y1}, ${this.x2}, ${this.y2})`;
}
normalize(): NormalizedCubicBezierEasing {
return {
name: CubicBezierEasing.easingName,
x1: this.x1,
y1: this.y1,
x2: this.x2,
y2: this.y2,
};
}
}
@@ -1,24 +0,0 @@
'use strict';
import { CubicBezierEasing } from './cubicBezier';
import { LinearEasing } from './linear';
import { StepsEasing } from './steps';
import type { ControlPoint, StepsModifier } from './types';
export function cubicBezier(x1: number, y1: number, x2: number, y2: number) {
return new CubicBezierEasing(x1, y1, x2, y2);
}
export function steps(
stepsNumber: number,
modifier: StepsModifier = 'jump-end'
) {
return new StepsEasing(stepsNumber, modifier);
}
export function linear(...points: ControlPoint[]) {
return new LinearEasing(points);
}
export { CubicBezierEasing, LinearEasing, StepsEasing };
export type * from './types';
@@ -1,153 +0,0 @@
'use strict';
import { logger, ReanimatedError } from '../../common';
import { PERCENTAGE_REGEX } from '../constants';
import type { Point } from '../types';
import type {
ControlPoint,
NormalizedLinearEasing,
ParametrizedTimingFunction,
} from './types';
export const ERROR_MESSAGES = {
invalidPointsCount: () =>
`Invalid linear easing points count. There should be at least two points`,
invalidInputProgressValue: (inputProgress: string | number) =>
`Invalid input progress ${inputProgress} value, it should be a percentage between 0% and 100%`,
};
export const WARN_MESSAGES = {
inputProgressLessThanPrecedingPoint: (x: number, precedingX: number) =>
`Linear easing point x value ${x} is less than value of the preceding control point ${precedingX}. Value will be overridden by ${precedingX}`,
};
const parsePercentage = (percentage: string | number): number => {
let result: number | undefined;
if (typeof percentage === 'number') {
result = percentage;
} else if (PERCENTAGE_REGEX.test(percentage)) {
result = parseFloat(percentage) / 100;
}
if (result === undefined || result < 0 || result > 1) {
throw new ReanimatedError(
ERROR_MESSAGES.invalidInputProgressValue(percentage)
);
}
return result;
};
const extrapolate = (x: number, point1: Point, point2: Point) => {
const slope = (point2.y - point1.y) / (point2.x - point1.x);
return point1.y + slope * (x - point1.x);
};
export class LinearEasing implements ParametrizedTimingFunction {
static readonly easingName = 'linear';
readonly points: ControlPoint[];
constructor(points: ControlPoint[]) {
if (points.length < 2) {
throw new ReanimatedError(ERROR_MESSAGES.invalidPointsCount());
}
this.points = points.map((p) =>
Array.isArray(p) && p.length === 1 ? p[0] : p
);
}
toString(): string {
return `${LinearEasing.easingName}(${this.points
.map((point) =>
Array.isArray(point)
? `[${point.map((p) => (typeof p === 'string' ? `"${p}"` : p)).join(', ')}]`
: point
)
.join(', ')})`;
}
normalize(): NormalizedLinearEasing {
const points = this.canonicalize();
// Extrapolate points if the input progress of the first one is greater than 0
// or the input progress of the last one is less than 1
if (points[0].x > 0) {
points.unshift({ x: 0, y: extrapolate(0, points[0], points[1]) });
}
if (points[points.length - 1].x < 1) {
points.push({
x: 1,
y: extrapolate(1, points[points.length - 2], points[points.length - 1]),
});
}
return { name: LinearEasing.easingName, points };
}
private canonicalize() {
const result = this.points.flatMap<{ x?: number; y: number }>((point) =>
Array.isArray(point)
? point.slice(1).map((x) => ({ x: parsePercentage(x), y: point[0] }))
: [{ y: point }]
);
// 1. If the first control point lacks an input progress value,
// set its input progress value to 0.
if (result[0].x === undefined) {
result[0].x = 0;
}
// 2.If the last control point lacks an input progress value,
// set its input progress value to 1.
if (result[result.length - 1].x === undefined) {
result[result.length - 1].x = 1;
}
// 3. If any control point has an input progress value that is less
// than the input progress value of any preceding control point, set
// its input progress value to the largest input progress value of
// any preceding control point.
let maxPrecedingX = 0;
for (let i = 1; i < result.length - 1; i++) {
const x = result[i].x;
if (x !== undefined) {
if (x < maxPrecedingX) {
logger.warn(
WARN_MESSAGES.inputProgressLessThanPrecedingPoint(x, maxPrecedingX)
);
result[i].x = maxPrecedingX;
} else {
maxPrecedingX = x;
}
}
}
// 4. If any control point still lacks an input progress value, then
// for each contiguous run of such control points, set their input
// progress values so that they are evenly spaced between the preceding
// and following control points with input progress values.
let precedingX = result[0].x;
let missingCount = 0;
for (let i = 1; i < result.length; i++) {
const x = result[i].x;
if (x === undefined) {
missingCount++;
continue;
}
if (missingCount > 0) {
const range = x - precedingX;
for (let j = 0; j < missingCount; j++) {
result[i - missingCount + j].x =
precedingX + (range * (j + 1)) / (missingCount + 1);
}
}
precedingX = x;
missingCount = 0;
}
return result as Point[];
}
}
@@ -1,104 +0,0 @@
'use strict';
import { ReanimatedError } from '../../common';
import type { Point } from '../types';
import type {
NormalizedStepsEasing,
ParametrizedTimingFunction,
StepsModifier,
} from './types';
export const ERROR_MESSAGES = {
invalidStepsNumber: (stepsNumber: number) =>
`Steps easing function accepts only positive integers as numbers of steps, ${stepsNumber} isn't a one`,
};
export class StepsEasing implements ParametrizedTimingFunction {
static readonly easingName = 'steps';
readonly stepsNumber: number;
readonly modifier: StepsModifier;
constructor(stepsNumber: number, modifier: StepsModifier = 'jump-end') {
if (stepsNumber <= 0 || stepsNumber % 1 !== 0) {
throw new ReanimatedError(ERROR_MESSAGES.invalidStepsNumber(stepsNumber));
}
this.stepsNumber = stepsNumber;
this.modifier = modifier;
}
toString(): string {
return `${StepsEasing.easingName}(${this.stepsNumber}, ${this.modifier})`;
}
normalize(): NormalizedStepsEasing | 'linear' {
switch (this.modifier) {
case 'jump-start':
case 'start':
return this.jumpStart();
case 'jump-end':
case 'end':
return this.jumpEnd();
case 'jump-both':
return this.jumpBoth();
case 'jump-none':
default:
if (this.stepsNumber === 1) {
// CSS animations standard returns here linear easing
return 'linear';
}
return this.jumpNone();
}
}
private jumpNone() {
const points: Point[] = [];
const div = this.stepsNumber - 1;
for (let i = 0; i < this.stepsNumber; i++) {
points.push({ x: i / this.stepsNumber, y: i / div });
}
return this.withName(points);
}
private jumpStart() {
const points: Point[] = [];
for (let i = 0; i < this.stepsNumber; i++) {
points.push({ x: i / this.stepsNumber, y: (i + 1) / this.stepsNumber });
}
return this.withName(points);
}
private jumpEnd() {
const points: Point[] = [];
for (let i = 0; i < this.stepsNumber; i++) {
points.push({ x: i / this.stepsNumber, y: i / this.stepsNumber });
}
// Final jump
points.push({ x: 1, y: 1 });
return this.withName(points);
}
private jumpBoth() {
const points: Point[] = [];
const div = this.stepsNumber + 1;
for (let i = 0; i < this.stepsNumber; i++) {
points.push({ x: i / this.stepsNumber, y: (i + 1) / div });
}
// Final jump
points.push({ x: 1, y: 1 });
return this.withName(points);
}
private withName(points: Point[]) {
return {
name: StepsEasing.easingName,
points,
};
}
}
@@ -1,54 +0,0 @@
'use strict';
import type { Percentage, Point } from '../types';
export type NormalizedCubicBezierEasing = {
name: string;
x1: number;
y1: number;
x2: number;
y2: number;
};
export type NormalizedLinearEasing = {
name: string;
points: Point[];
};
export type NormalizedStepsEasing = {
name: string;
points: Point[];
};
export type ControlPoint = number | [number, ...Percentage[]];
export type StepsModifier =
| 'jump-start'
| 'start'
| 'jump-end'
| 'end'
| 'jump-none'
| 'jump-both';
export type PredefinedTimingFunction =
| 'linear'
| 'ease'
| 'ease-in'
| 'ease-out'
| 'ease-in-out'
| 'step-start'
| 'step-end';
export type CSSTimingFunction =
| PredefinedTimingFunction
| ParametrizedTimingFunction;
export type NormalizedCSSTimingFunction =
| PredefinedTimingFunction
| NormalizedCubicBezierEasing
| NormalizedLinearEasing
| NormalizedStepsEasing;
export interface ParametrizedTimingFunction {
toString(): string;
normalize(): NormalizedCSSTimingFunction;
}
-26
View File
@@ -1,26 +0,0 @@
'use strict';
export { createAnimatedComponent as createCSSAnimatedComponent } from './component';
export { cubicBezier, linear, steps } from './easing';
export * from './stylesheet';
export type {
CSSAnimationDelay,
CSSAnimationDirection,
CSSAnimationDuration,
CSSAnimationFillMode,
CSSAnimationIterationCount,
CSSAnimationKeyframes,
CSSAnimationKeyframeSelector,
CSSAnimationPlayState,
CSSAnimationProperties,
CSSAnimationSettings,
CSSAnimationTimingFunction,
CSSKeyframesRule,
CSSStyle,
CSSTransitionDelay,
CSSTransitionDuration,
CSSTransitionProperties,
CSSTransitionProperty,
CSSTransitionSettings,
CSSTransitionShorthand,
CSSTransitionTimingFunction,
} from './types';
@@ -1,46 +0,0 @@
'use strict';
import { ANIMATION_NAME_PREFIX } from '../constants';
import type {
CSSAnimationKeyframes,
CSSKeyframesRule,
PlainStyle,
} from '../types';
export default abstract class CSSKeyframesRuleBase<S extends PlainStyle>
implements CSSKeyframesRule
{
private static currentAnimationID = 0;
// TODO - change cssRules prop to match specification
private readonly cssRules_: CSSAnimationKeyframes<S>;
private readonly cssText_: string;
private readonly length_: number;
private readonly name_: string;
constructor(keyframes: CSSAnimationKeyframes<S>, cssText?: string) {
this.cssRules_ = keyframes;
this.cssText_ = cssText ?? JSON.stringify(keyframes);
this.length_ = Object.keys(keyframes).length;
this.name_ = CSSKeyframesRuleBase.generateNextKeyframeName();
}
get cssRules() {
return this.cssRules_;
}
get cssText() {
return this.cssText_;
}
get length() {
return this.length_;
}
get name() {
return this.name_;
}
static generateNextKeyframeName() {
return `${ANIMATION_NAME_PREFIX}${CSSKeyframesRuleBase.currentAnimationID++}`;
}
}
@@ -1,2 +0,0 @@
'use strict';
export { default as CSSKeyframesRuleBase } from './CSSKeyframesRuleBase';
@@ -1,8 +0,0 @@
'use strict';
export * from './keyframes';
export * from './managers';
export * from './normalization';
export * from './proxy';
export * from './registry';
export * from './style';
export type * from './types';
@@ -1,99 +0,0 @@
'use strict';
import { registerCSSKeyframes, unregisterCSSKeyframes } from '../proxy';
import type CSSKeyframesRuleImpl from './CSSKeyframesRuleImpl';
type KeyframesEntry = {
keyframesRule: CSSKeyframesRuleImpl;
usedBy: Record<string, Set<number>>;
};
/**
* This class is responsible for managing the registry of CSS animation
* keyframes. It keeps track of views that use specific animations and handles
* native-side registration. Animation keyframes are registered on the native
* side only when used for the first time and unregistered when removed from the
* last view that uses them.
*/
class CSSKeyframesRegistry {
private readonly cssTextToNameMap_: Map<string, string> = new Map();
private readonly nameToKeyframes_: Map<string, KeyframesEntry> = new Map();
get(nameOrCssText: string) {
const result = this.nameToKeyframes_.get(nameOrCssText);
if (result) {
return result.keyframesRule;
}
const animationName = this.cssTextToNameMap_.get(nameOrCssText);
if (animationName) {
return this.nameToKeyframes_.get(animationName)?.keyframesRule;
}
}
add(keyframesRule: CSSKeyframesRuleImpl, viewName: string, viewTag: number) {
const existingKeyframesEntry = this.nameToKeyframes_.get(
keyframesRule.name
);
const existingComponentEntry = existingKeyframesEntry?.usedBy[viewName];
if (existingComponentEntry) {
// Just add the view tag to the existing component entry if keyframes
// for the specific animation and component name are already registered
existingComponentEntry.add(viewTag);
return;
}
// Otherwise, we have to register keyframes preprocessed for the specific
// component name
if (existingKeyframesEntry) {
existingKeyframesEntry.usedBy[viewName] = new Set([viewTag]);
} else {
this.nameToKeyframes_.set(keyframesRule.name, {
keyframesRule,
usedBy: { [viewName]: new Set([viewTag]) },
});
}
// Store the keyframes to name mapping in order to reuse the same
// animation name when possible (when the same inline keyframes object
// is used)
this.cssTextToNameMap_.set(keyframesRule.cssText, keyframesRule.name);
// Register animation keyframes only if they are not already registered
// (when they are added for the first time)
registerCSSKeyframes(
keyframesRule.name,
viewName,
keyframesRule.getNormalizedKeyframesConfig(viewName)
);
}
remove(animationName: string, viewName: string, viewTag: number) {
const keyframesEntry = this.nameToKeyframes_.get(animationName);
if (!keyframesEntry) {
return;
}
const componentEntry = keyframesEntry.usedBy[viewName];
componentEntry.delete(viewTag);
if (componentEntry.size === 0) {
delete keyframesEntry.usedBy[viewName];
unregisterCSSKeyframes(animationName, viewName);
}
if (Object.keys(keyframesEntry.usedBy).length === 0) {
this.nameToKeyframes_.delete(animationName);
this.cssTextToNameMap_.delete(keyframesEntry.keyframesRule.cssText);
}
}
clear() {
this.nameToKeyframes_.clear();
this.cssTextToNameMap_.clear();
}
}
const cssKeyframesRegistry = new CSSKeyframesRegistry();
export default cssKeyframesRegistry;
@@ -1,32 +0,0 @@
'use strict';
import { CSSKeyframesRuleBase } from '../../models';
import type { CSSAnimationKeyframes, PlainStyle } from '../../types';
import { normalizeAnimationKeyframes } from '../normalization';
import { getStyleBuilder } from '../registry';
import type { NormalizedCSSAnimationKeyframesConfig } from '../types';
export default class CSSKeyframesRuleImpl<
S extends PlainStyle = PlainStyle,
> extends CSSKeyframesRuleBase<S> {
private readonly normalizedKeyframesCache_: Record<
string,
NormalizedCSSAnimationKeyframesConfig
> = {};
constructor(keyframes: CSSAnimationKeyframes<S>, cssText?: string) {
super(keyframes, cssText);
}
getNormalizedKeyframesConfig(
viewName: string
): NormalizedCSSAnimationKeyframesConfig {
if (!this.normalizedKeyframesCache_[viewName]) {
this.normalizedKeyframesCache_[viewName] = normalizeAnimationKeyframes(
this.cssRules,
getStyleBuilder(viewName)
);
}
return this.normalizedKeyframesCache_[viewName];
}
}
@@ -1,3 +0,0 @@
'use strict';
export { default as cssKeyframesRegistry } from './CSSKeyframesRegistry';
export { default as CSSKeyframesRuleImpl } from './CSSKeyframesRuleImpl';
@@ -1,228 +0,0 @@
'use strict';
import type { ShadowNodeWrapper } from '../../../commonTypes';
import type {
CSSAnimationKeyframes,
ExistingCSSAnimationProperties,
ICSSAnimationsManager,
} from '../../types';
import { cssKeyframesRegistry, CSSKeyframesRuleImpl } from '../keyframes';
import {
createSingleCSSAnimationProperties,
getAnimationSettingsUpdates,
normalizeSingleCSSAnimationSettings,
} from '../normalization';
import { applyCSSAnimations, unregisterCSSAnimations } from '../proxy';
import type {
CSSAnimationUpdates,
NormalizedSingleCSSAnimationSettings,
} from '../types';
type ProcessedAnimation = {
normalizedSettings: NormalizedSingleCSSAnimationSettings;
keyframesRule: CSSKeyframesRuleImpl;
};
export default class CSSAnimationsManager implements ICSSAnimationsManager {
private readonly shadowNodeWrapper: ShadowNodeWrapper;
private readonly viewName: string;
private readonly viewTag: number;
private attachedAnimations: ProcessedAnimation[] = [];
constructor(
shadowNodeWrapper: ShadowNodeWrapper,
viewName: string,
viewTag: number
) {
this.shadowNodeWrapper = shadowNodeWrapper;
this.viewName = viewName;
this.viewTag = viewTag;
}
update(animationProperties: ExistingCSSAnimationProperties | null): void {
if (!animationProperties) {
this.detach();
return;
}
const processedAnimations = this.processAnimations(animationProperties);
this.registerKeyframesUsage(processedAnimations);
const animationUpdates = this.getAnimationUpdates(processedAnimations);
this.attachedAnimations = processedAnimations;
if (animationUpdates) {
if (
animationUpdates.animationNames &&
animationUpdates.animationNames.length === 0
) {
this.detach();
return;
}
applyCSSAnimations(this.shadowNodeWrapper, animationUpdates);
}
}
unmountCleanup(): void {
this.unregisterKeyframesUsage();
}
private detach() {
if (this.attachedAnimations.length > 0) {
unregisterCSSAnimations(this.viewTag);
this.unregisterKeyframesUsage();
this.attachedAnimations = [];
}
}
private registerKeyframesUsage(processedAnimations: ProcessedAnimation[]) {
const newAnimationNames = new Set();
// Register keyframes for all new animations
processedAnimations.forEach(({ keyframesRule }) => {
cssKeyframesRegistry.add(keyframesRule, this.viewName, this.viewTag);
newAnimationNames.add(keyframesRule.name);
});
// Unregister keyframes for all old animations that are no longer attached
// to the view
this.attachedAnimations.forEach(({ keyframesRule: { name } }) => {
if (!newAnimationNames.has(name)) {
cssKeyframesRegistry.remove(name, this.viewName, this.viewTag);
}
});
}
private unregisterKeyframesUsage() {
// Unregister keyframes usage by the view (it is necessary to clean up
// keyframes from the CPP registry once all views that use them are unmounted)
this.attachedAnimations.forEach(({ keyframesRule: { name } }) => {
cssKeyframesRegistry.remove(name, this.viewName, this.viewTag);
});
}
private processAnimations(
animationProperties: ExistingCSSAnimationProperties
): ProcessedAnimation[] {
const singleAnimationPropertiesArray =
createSingleCSSAnimationProperties(animationProperties);
const processedAnimations = singleAnimationPropertiesArray.map(
(properties) => {
const keyframes = properties.animationName;
let keyframesRule: CSSKeyframesRuleImpl;
if (keyframes instanceof CSSKeyframesRuleImpl) {
// If the instance of the CSSKeyframesRule class was passed, we can just compare
// references to the instance (css.keyframes() call should be memoized in order
// to preserve the same animation. If used inline, it will restart the animation
// on every component re-render)
keyframesRule = keyframes;
} else {
// If the keyframes are not an instance of the CSSKeyframesRule class (e.g. someone
// passes a keyframes object inline in the component's style without using css.keyframes()
// function), we don't want to restart the animation on every component re-render.
// In this case, we need to check if the animation with the same keyframes is already
// registered in the registry. If it is, we can just use the existing keyframes rule.
// Otherwise, we need to create a new keyframes rule.
const cssText = JSON.stringify(keyframes);
keyframesRule =
cssKeyframesRegistry.get(cssText) ??
new CSSKeyframesRuleImpl(
keyframes as CSSAnimationKeyframes,
cssText
);
}
return {
normalizedSettings: normalizeSingleCSSAnimationSettings(properties),
keyframesRule,
};
}
);
return processedAnimations;
}
private buildAnimationsMap(animations: ProcessedAnimation[]) {
// Iterate over attached animations from last to first for faster pop from
// the end of the array when removing used animations
return animations.reduceRight<Record<string, ProcessedAnimation[]>>(
(acc, animation) => {
const name = animation.keyframesRule.name;
if (!acc[name]) {
acc[name] = [animation];
} else {
acc[name].push(animation);
}
return acc;
},
{}
);
}
private getAnimationUpdates(
processedAnimations: ProcessedAnimation[]
): CSSAnimationUpdates | null {
const newAnimationSettings: Record<
number,
NormalizedSingleCSSAnimationSettings
> = {};
const settingsUpdates: Record<
number,
Partial<NormalizedSingleCSSAnimationSettings>
> = {};
let animationsArrayChanged =
this.attachedAnimations.length !== processedAnimations.length;
let hasNewAnimations = false;
let hasSettingsUpdates = false;
const oldAnimations = this.buildAnimationsMap(this.attachedAnimations);
processedAnimations.forEach(({ keyframesRule, normalizedSettings }, i) => {
const oldAnimation = oldAnimations[keyframesRule.name]?.pop();
if (!oldAnimation) {
hasNewAnimations = true;
animationsArrayChanged = true;
newAnimationSettings[i] = normalizedSettings;
return;
}
const updates = getAnimationSettingsUpdates(
oldAnimation.normalizedSettings,
normalizedSettings
);
if (Object.keys(updates).length > 0) {
hasSettingsUpdates = true;
settingsUpdates[i] = updates;
}
if (oldAnimation.keyframesRule.name !== keyframesRule.name) {
animationsArrayChanged = true;
}
});
const result: CSSAnimationUpdates = {};
if (animationsArrayChanged) {
result.animationNames = processedAnimations.map(
({ keyframesRule }) => keyframesRule.name
);
}
if (hasNewAnimations) {
result.newAnimationSettings = newAnimationSettings;
}
if (hasSettingsUpdates) {
result.settingsUpdates = settingsUpdates;
}
if (hasNewAnimations || hasSettingsUpdates || animationsArrayChanged) {
return result;
}
return null;
}
}
@@ -1,73 +0,0 @@
'use strict';
import { ReanimatedError } from '../../../common';
import type { ShadowNodeWrapper } from '../../../commonTypes';
import type { ViewInfo } from '../../../createAnimatedComponent/commonTypes';
import type { AnyRecord, CSSStyle } from '../../types';
import type { ICSSManager } from '../../types/interfaces';
import { filterCSSAndStyleProperties } from '../../utils';
import { setViewStyle } from '../proxy';
import { getStyleBuilder, hasStyleBuilder } from '../registry';
import type { StyleBuilder } from '../style';
import CSSAnimationsManager from './CSSAnimationsManager';
import CSSTransitionsManager from './CSSTransitionsManager';
export default class CSSManager implements ICSSManager {
private readonly cssAnimationsManager: CSSAnimationsManager;
private readonly cssTransitionsManager: CSSTransitionsManager;
private readonly viewTag: number;
private readonly viewName: string;
private readonly styleBuilder: StyleBuilder<AnyRecord> | null = null;
private isFirstUpdate: boolean = true;
constructor({ shadowNodeWrapper, viewTag, viewName = 'RCTView' }: ViewInfo) {
const tag = (this.viewTag = viewTag as number);
const wrapper = shadowNodeWrapper as ShadowNodeWrapper;
this.viewName = viewName;
this.styleBuilder = hasStyleBuilder(viewName)
? getStyleBuilder(viewName)
: null;
this.cssAnimationsManager = new CSSAnimationsManager(
wrapper,
viewName,
tag
);
this.cssTransitionsManager = new CSSTransitionsManager(wrapper, tag);
}
update(style: CSSStyle): void {
const [animationProperties, transitionProperties, filteredStyle] =
filterCSSAndStyleProperties(style);
if (!this.styleBuilder && (animationProperties || transitionProperties)) {
throw new ReanimatedError(
`Tried to apply CSS animations to ${this.viewName} which is not supported`
);
}
const normalizedStyle = this.styleBuilder?.buildFrom(filteredStyle);
// If the update is called during the first css style update, we won't
// trigger CSS transitions and set styles before attaching CSS transitions
if (this.isFirstUpdate && normalizedStyle) {
setViewStyle(this.viewTag, normalizedStyle);
}
this.cssTransitionsManager.update(transitionProperties);
this.cssAnimationsManager.update(animationProperties);
// If the current update is not the fist one, we want to update CSS
// animations and transitions first and update the style then to make
// sure that the new transition is fired with new settings (like duration)
if (!this.isFirstUpdate && normalizedStyle) {
setViewStyle(this.viewTag, normalizedStyle);
}
this.isFirstUpdate = false;
}
unmountCleanup(): void {
this.cssAnimationsManager.unmountCleanup();
this.cssTransitionsManager.unmountCleanup();
}
}
@@ -1,74 +0,0 @@
'use strict';
import type { ShadowNodeWrapper } from '../../../commonTypes';
import type {
CSSTransitionProperties,
ICSSTransitionsManager,
} from '../../types';
import {
getNormalizedCSSTransitionConfigUpdates,
normalizeCSSTransitionProperties,
} from '../normalization';
import {
registerCSSTransition,
unregisterCSSTransition,
updateCSSTransition,
} from '../proxy';
import type { NormalizedCSSTransitionConfig } from '../types';
export default class CSSTransitionsManager implements ICSSTransitionsManager {
private readonly viewTag: number;
private readonly shadowNodeWrapper: ShadowNodeWrapper;
private transitionConfig: NormalizedCSSTransitionConfig | null = null;
constructor(shadowNodeWrapper: ShadowNodeWrapper, viewTag: number) {
this.viewTag = viewTag;
this.shadowNodeWrapper = shadowNodeWrapper;
}
update(transitionProperties: CSSTransitionProperties | null): void {
if (!transitionProperties) {
this.detach();
return;
}
const transitionConfig =
normalizeCSSTransitionProperties(transitionProperties);
if (!transitionConfig) {
this.detach();
return;
}
if (this.transitionConfig) {
const configUpdates = getNormalizedCSSTransitionConfigUpdates(
this.transitionConfig,
transitionConfig
);
if (Object.keys(configUpdates).length > 0) {
this.transitionConfig = transitionConfig;
updateCSSTransition(this.viewTag, configUpdates);
}
} else {
this.attachTransition(transitionConfig);
}
}
unmountCleanup(): void {
// noop
}
private detach() {
if (this.transitionConfig) {
unregisterCSSTransition(this.viewTag);
this.transitionConfig = null;
}
}
private attachTransition(transitionConfig: NormalizedCSSTransitionConfig) {
if (!this.transitionConfig) {
registerCSSTransition(this.shadowNodeWrapper, transitionConfig);
this.transitionConfig = transitionConfig;
}
}
}

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