first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

View File

@@ -0,0 +1,71 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {InterpolationConfigType} from './AnimatedInterpolation';
import type AnimatedNode from './AnimatedNode';
import type {AnimatedNodeConfig} from './AnimatedNode';
import AnimatedInterpolation from './AnimatedInterpolation';
import AnimatedValue from './AnimatedValue';
import AnimatedWithChildren from './AnimatedWithChildren';
export default class AnimatedAddition extends AnimatedWithChildren {
_a: AnimatedNode;
_b: AnimatedNode;
constructor(
a: AnimatedNode | number,
b: AnimatedNode | number,
config?: ?AnimatedNodeConfig,
) {
super(config);
this._a = typeof a === 'number' ? new AnimatedValue(a) : a;
this._b = typeof b === 'number' ? new AnimatedValue(b) : b;
}
__makeNative(platformConfig: ?PlatformConfig) {
this._a.__makeNative(platformConfig);
this._b.__makeNative(platformConfig);
super.__makeNative(platformConfig);
}
__getValue(): number {
return this._a.__getValue() + this._b.__getValue();
}
interpolate<OutputT: number | string>(
config: InterpolationConfigType<OutputT>,
): AnimatedInterpolation<OutputT> {
return new AnimatedInterpolation(this, config);
}
__attach(): void {
this._a.__addChild(this);
this._b.__addChild(this);
super.__attach();
}
__detach(): void {
this._a.__removeChild(this);
this._b.__removeChild(this);
super.__detach();
}
__getNativeConfig(): any {
return {
type: 'addition',
input: [this._a.__getNativeTag(), this._b.__getNativeTag()],
debugID: this.__getDebugID(),
};
}
}

View File

@@ -0,0 +1,339 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
import type {ProcessedColorValue} from '../../StyleSheet/processColor';
import type {ColorValue} from '../../StyleSheet/StyleSheet';
import type {NativeColorValue} from '../../StyleSheet/StyleSheetTypes';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {AnimatedNodeConfig} from './AnimatedNode';
import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper';
import normalizeColor from '../../StyleSheet/normalizeColor';
import {processColorObject} from '../../StyleSheet/PlatformColorValueTypes';
import AnimatedValue, {flushValue} from './AnimatedValue';
import AnimatedWithChildren from './AnimatedWithChildren';
export type AnimatedColorConfig = $ReadOnly<{
...AnimatedNodeConfig,
useNativeDriver: boolean,
}>;
type ColorListenerCallback = (value: ColorValue) => mixed;
export type RgbaValue = {
+r: number,
+g: number,
+b: number,
+a: number,
...
};
type RgbaAnimatedValue = {
+r: AnimatedValue,
+g: AnimatedValue,
+b: AnimatedValue,
+a: AnimatedValue,
...
};
export type InputValue = ?(RgbaValue | RgbaAnimatedValue | ColorValue);
const NativeAnimatedAPI = NativeAnimatedHelper.API;
const defaultColor: RgbaValue = {r: 0, g: 0, b: 0, a: 1.0};
/* eslint no-bitwise: 0 */
function processColor(
color?: ?(ColorValue | RgbaValue),
): ?(RgbaValue | NativeColorValue) {
if (color === undefined || color === null) {
return null;
}
if (isRgbaValue(color)) {
// $FlowFixMe[incompatible-type] - Type is verified above
return (color: RgbaValue);
}
let normalizedColor: ?ProcessedColorValue = normalizeColor(
// $FlowFixMe[incompatible-type] - Type is verified above
(color: ColorValue),
);
if (normalizedColor === undefined || normalizedColor === null) {
return null;
}
if (typeof normalizedColor === 'object') {
const processedColorObj: ?NativeColorValue =
processColorObject(normalizedColor);
if (processedColorObj != null) {
return processedColorObj;
}
} else if (typeof normalizedColor === 'number') {
const r: number = (normalizedColor & 0xff000000) >>> 24;
const g: number = (normalizedColor & 0x00ff0000) >>> 16;
const b: number = (normalizedColor & 0x0000ff00) >>> 8;
const a: number = (normalizedColor & 0x000000ff) / 255;
return {r, g, b, a};
}
return null;
}
function isRgbaValue(value: any): boolean {
return (
value &&
typeof value.r === 'number' &&
typeof value.g === 'number' &&
typeof value.b === 'number' &&
typeof value.a === 'number'
);
}
function isRgbaAnimatedValue(value: any): boolean {
return (
value &&
value.r instanceof AnimatedValue &&
value.g instanceof AnimatedValue &&
value.b instanceof AnimatedValue &&
value.a instanceof AnimatedValue
);
}
export function getRgbaValueAndNativeColor(
value: RgbaValue | ColorValue,
): $ReadOnly<{
rgbaValue: RgbaValue,
nativeColor?: NativeColorValue,
}> {
const processedColor: RgbaValue | NativeColorValue =
// $FlowFixMe[incompatible-type] - Type is verified above
processColor((value: ColorValue | RgbaValue)) ?? defaultColor;
if (isRgbaValue(processedColor)) {
// $FlowFixMe[incompatible-type] - Type is verified above
return {rgbaValue: (processedColor: RgbaValue)};
} else {
return {
// $FlowFixMe[incompatible-type] - Type is verified above
nativeColor: (processedColor: NativeColorValue),
rgbaValue: defaultColor,
};
}
}
export default class AnimatedColor extends AnimatedWithChildren {
r: AnimatedValue;
g: AnimatedValue;
b: AnimatedValue;
a: AnimatedValue;
nativeColor: ?NativeColorValue;
_suspendCallbacks: number = 0;
constructor(valueIn?: InputValue, config?: ?AnimatedColorConfig) {
super(config);
let value: RgbaValue | RgbaAnimatedValue | ColorValue =
valueIn ?? defaultColor;
if (isRgbaAnimatedValue(value)) {
// $FlowFixMe[incompatible-type] - Type is verified above
const rgbaAnimatedValue: RgbaAnimatedValue = (value: RgbaAnimatedValue);
this.r = rgbaAnimatedValue.r;
this.g = rgbaAnimatedValue.g;
this.b = rgbaAnimatedValue.b;
this.a = rgbaAnimatedValue.a;
} else {
const {rgbaValue: initColor, nativeColor} = getRgbaValueAndNativeColor(
// $FlowFixMe[incompatible-type] - Type is verified above
(value: ColorValue | RgbaValue),
);
if (nativeColor) {
this.nativeColor = nativeColor;
}
this.r = new AnimatedValue(initColor.r);
this.g = new AnimatedValue(initColor.g);
this.b = new AnimatedValue(initColor.b);
this.a = new AnimatedValue(initColor.a);
}
if (config?.useNativeDriver) {
this.__makeNative();
}
}
/**
* Directly set the value. This will stop any animations running on the value
* and update all the bound properties.
*/
setValue(value: RgbaValue | ColorValue): void {
let shouldUpdateNodeConfig = false;
if (this.__isNative) {
const nativeTag = this.__getNativeTag();
NativeAnimatedAPI.setWaitingForIdentifier(nativeTag.toString());
}
const processedColor: RgbaValue | NativeColorValue =
processColor(value) ?? defaultColor;
this._withSuspendedCallbacks(() => {
if (isRgbaValue(processedColor)) {
// $FlowFixMe[incompatible-type] - Type is verified above
const rgbaValue: RgbaValue = processedColor;
this.r.setValue(rgbaValue.r);
this.g.setValue(rgbaValue.g);
this.b.setValue(rgbaValue.b);
this.a.setValue(rgbaValue.a);
if (this.nativeColor != null) {
this.nativeColor = null;
shouldUpdateNodeConfig = true;
}
} else {
// $FlowFixMe[incompatible-type] - Type is verified above
const nativeColor: NativeColorValue = processedColor;
if (this.nativeColor !== nativeColor) {
this.nativeColor = nativeColor;
shouldUpdateNodeConfig = true;
}
}
});
if (this.__isNative) {
const nativeTag = this.__getNativeTag();
if (shouldUpdateNodeConfig) {
NativeAnimatedAPI.updateAnimatedNodeConfig(
nativeTag,
this.__getNativeConfig(),
);
}
NativeAnimatedAPI.unsetWaitingForIdentifier(nativeTag.toString());
} else {
flushValue(this);
}
// $FlowFixMe[incompatible-type]
this.__callListeners(this.__getValue());
}
/**
* Sets an offset that is applied on top of whatever value is set, whether
* via `setValue`, an animation, or `Animated.event`. Useful for compensating
* things like the start of a pan gesture.
*/
setOffset(offset: RgbaValue): void {
this.r.setOffset(offset.r);
this.g.setOffset(offset.g);
this.b.setOffset(offset.b);
this.a.setOffset(offset.a);
}
/**
* Merges the offset value into the base value and resets the offset to zero.
* The final output of the value is unchanged.
*/
flattenOffset(): void {
this.r.flattenOffset();
this.g.flattenOffset();
this.b.flattenOffset();
this.a.flattenOffset();
}
/**
* Sets the offset value to the base value, and resets the base value to
* zero. The final output of the value is unchanged.
*/
extractOffset(): void {
this.r.extractOffset();
this.g.extractOffset();
this.b.extractOffset();
this.a.extractOffset();
}
/**
* Stops any running animation or tracking. `callback` is invoked with the
* final value after stopping the animation, which is useful for updating
* state to match the animation position with layout.
*/
stopAnimation(callback?: ColorListenerCallback): void {
this.r.stopAnimation();
this.g.stopAnimation();
this.b.stopAnimation();
this.a.stopAnimation();
callback && callback(this.__getValue());
}
/**
* Stops any animation and resets the value to its original.
*/
resetAnimation(callback?: ColorListenerCallback): void {
this.r.resetAnimation();
this.g.resetAnimation();
this.b.resetAnimation();
this.a.resetAnimation();
callback && callback(this.__getValue());
}
__getValue(): ColorValue {
if (this.nativeColor != null) {
return this.nativeColor;
} else {
return `rgba(${this.r.__getValue()}, ${this.g.__getValue()}, ${this.b.__getValue()}, ${this.a.__getValue()})`;
}
}
__attach(): void {
this.r.__addChild(this);
this.g.__addChild(this);
this.b.__addChild(this);
this.a.__addChild(this);
super.__attach();
}
__detach(): void {
this.r.__removeChild(this);
this.g.__removeChild(this);
this.b.__removeChild(this);
this.a.__removeChild(this);
super.__detach();
}
_withSuspendedCallbacks(callback: () => void) {
this._suspendCallbacks++;
callback();
this._suspendCallbacks--;
}
__callListeners(value: number): void {
if (this._suspendCallbacks === 0) {
super.__callListeners(value);
}
}
__makeNative(platformConfig: ?PlatformConfig) {
this.r.__makeNative(platformConfig);
this.g.__makeNative(platformConfig);
this.b.__makeNative(platformConfig);
this.a.__makeNative(platformConfig);
super.__makeNative(platformConfig);
}
__getNativeConfig(): {...} {
return {
type: 'color',
r: this.r.__getNativeTag(),
g: this.g.__getNativeTag(),
b: this.b.__getNativeTag(),
a: this.a.__getNativeTag(),
nativeColor: this.nativeColor,
debugID: this.__getDebugID(),
};
}
}

View File

@@ -0,0 +1,80 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {InterpolationConfigType} from './AnimatedInterpolation';
import type AnimatedNode from './AnimatedNode';
import type {AnimatedNodeConfig} from './AnimatedNode';
import AnimatedInterpolation from './AnimatedInterpolation';
import AnimatedWithChildren from './AnimatedWithChildren';
export default class AnimatedDiffClamp extends AnimatedWithChildren {
_a: AnimatedNode;
_min: number;
_max: number;
_value: number;
_lastValue: number;
constructor(
a: AnimatedNode,
min: number,
max: number,
config?: ?AnimatedNodeConfig,
) {
super(config);
this._a = a;
this._min = min;
this._max = max;
this._value = this._lastValue = this._a.__getValue();
}
__makeNative(platformConfig: ?PlatformConfig) {
this._a.__makeNative(platformConfig);
super.__makeNative(platformConfig);
}
interpolate<OutputT: number | string>(
config: InterpolationConfigType<OutputT>,
): AnimatedInterpolation<OutputT> {
return new AnimatedInterpolation(this, config);
}
__getValue(): number {
const value = this._a.__getValue();
const diff = value - this._lastValue;
this._lastValue = value;
this._value = Math.min(Math.max(this._value + diff, this._min), this._max);
return this._value;
}
__attach(): void {
this._a.__addChild(this);
super.__attach();
}
__detach(): void {
this._a.__removeChild(this);
super.__detach();
}
__getNativeConfig(): any {
return {
type: 'diffclamp',
input: this._a.__getNativeTag(),
min: this._min,
max: this._max,
debugID: this.__getDebugID(),
};
}
}

View File

@@ -0,0 +1,87 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {InterpolationConfigType} from './AnimatedInterpolation';
import type {AnimatedNodeConfig} from './AnimatedNode';
import AnimatedInterpolation from './AnimatedInterpolation';
import AnimatedNode from './AnimatedNode';
import AnimatedValue from './AnimatedValue';
import AnimatedWithChildren from './AnimatedWithChildren';
export default class AnimatedDivision extends AnimatedWithChildren {
_a: AnimatedNode;
_b: AnimatedNode;
_warnedAboutDivideByZero: boolean = false;
constructor(
a: AnimatedNode | number,
b: AnimatedNode | number,
config?: ?AnimatedNodeConfig,
) {
super(config);
if (b === 0 || (b instanceof AnimatedNode && b.__getValue() === 0)) {
console.error('Detected potential division by zero in AnimatedDivision');
}
this._a = typeof a === 'number' ? new AnimatedValue(a) : a;
this._b = typeof b === 'number' ? new AnimatedValue(b) : b;
}
__makeNative(platformConfig: ?PlatformConfig) {
this._a.__makeNative(platformConfig);
this._b.__makeNative(platformConfig);
super.__makeNative(platformConfig);
}
__getValue(): number {
const a = this._a.__getValue();
const b = this._b.__getValue();
if (b === 0) {
// Prevent spamming the console/LogBox
if (!this._warnedAboutDivideByZero) {
console.error('Detected division by zero in AnimatedDivision');
this._warnedAboutDivideByZero = true;
}
// Passing infinity/NaN to Fabric will cause a native crash
return 0;
}
this._warnedAboutDivideByZero = false;
return a / b;
}
interpolate<OutputT: number | string>(
config: InterpolationConfigType<OutputT>,
): AnimatedInterpolation<OutputT> {
return new AnimatedInterpolation(this, config);
}
__attach(): void {
this._a.__addChild(this);
this._b.__addChild(this);
super.__attach();
}
__detach(): void {
this._a.__removeChild(this);
this._b.__removeChild(this);
super.__detach();
}
__getNativeConfig(): any {
return {
type: 'division',
input: [this._a.__getNativeTag(), this._b.__getNativeTag()],
debugID: this.__getDebugID(),
};
}
}

View File

@@ -0,0 +1,420 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
/* eslint no-bitwise: 0 */
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type AnimatedNode from './AnimatedNode';
import type {AnimatedNodeConfig} from './AnimatedNode';
import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper';
import {validateInterpolation} from '../../../src/private/animated/NativeAnimatedValidation';
import normalizeColor from '../../StyleSheet/normalizeColor';
import processColor from '../../StyleSheet/processColor';
import Easing from '../Easing';
import AnimatedWithChildren from './AnimatedWithChildren';
import invariant from 'invariant';
type ExtrapolateType = 'extend' | 'identity' | 'clamp';
export type InterpolationConfigType<OutputT: number | string> = $ReadOnly<{
...AnimatedNodeConfig,
inputRange: $ReadOnlyArray<number>,
outputRange: $ReadOnlyArray<OutputT>,
easing?: (input: number) => number,
extrapolate?: ExtrapolateType,
extrapolateLeft?: ExtrapolateType,
extrapolateRight?: ExtrapolateType,
}>;
/**
* Very handy helper to map input ranges to output ranges with an easing
* function and custom behavior outside of the ranges.
*/
function createNumericInterpolation(
config: InterpolationConfigType<number>,
): (input: number) => number {
const outputRange: $ReadOnlyArray<number> = (config.outputRange: any);
const inputRange = config.inputRange;
const easing = config.easing || Easing.linear;
let extrapolateLeft: ExtrapolateType = 'extend';
if (config.extrapolateLeft !== undefined) {
extrapolateLeft = config.extrapolateLeft;
} else if (config.extrapolate !== undefined) {
extrapolateLeft = config.extrapolate;
}
let extrapolateRight: ExtrapolateType = 'extend';
if (config.extrapolateRight !== undefined) {
extrapolateRight = config.extrapolateRight;
} else if (config.extrapolate !== undefined) {
extrapolateRight = config.extrapolate;
}
return input => {
invariant(
typeof input === 'number',
'Cannot interpolation an input which is not a number',
);
const range = findRange(input, inputRange);
return (interpolate(
input,
inputRange[range],
inputRange[range + 1],
outputRange[range],
outputRange[range + 1],
easing,
extrapolateLeft,
extrapolateRight,
): any);
};
}
function interpolate(
input: number,
inputMin: number,
inputMax: number,
outputMin: number,
outputMax: number,
easing: (input: number) => number,
extrapolateLeft: ExtrapolateType,
extrapolateRight: ExtrapolateType,
) {
let result = input;
// Extrapolate
if (result < inputMin) {
if (extrapolateLeft === 'identity') {
return result;
} else if (extrapolateLeft === 'clamp') {
result = inputMin;
} else if (extrapolateLeft === 'extend') {
// noop
}
}
if (result > inputMax) {
if (extrapolateRight === 'identity') {
return result;
} else if (extrapolateRight === 'clamp') {
result = inputMax;
} else if (extrapolateRight === 'extend') {
// noop
}
}
if (outputMin === outputMax) {
return outputMin;
}
if (inputMin === inputMax) {
if (input <= inputMin) {
return outputMin;
}
return outputMax;
}
// Input Range
if (inputMin === -Infinity) {
result = -result;
} else if (inputMax === Infinity) {
result = result - inputMin;
} else {
result = (result - inputMin) / (inputMax - inputMin);
}
// Easing
result = easing(result);
// Output Range
if (outputMin === -Infinity) {
result = -result;
} else if (outputMax === Infinity) {
result = result + outputMin;
} else {
result = result * (outputMax - outputMin) + outputMin;
}
return result;
}
const numericComponentRegex = /[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g;
// Maps string inputs an RGBA color or an array of numeric components
function mapStringToNumericComponents(
input: string,
):
| {isColor: true, components: [number, number, number, number]}
| {isColor: false, components: $ReadOnlyArray<number | string>} {
let normalizedColor = normalizeColor(input);
invariant(
normalizedColor == null || typeof normalizedColor !== 'object',
'PlatformColors are not supported',
);
if (typeof normalizedColor === 'number') {
normalizedColor = normalizedColor || 0;
const r = (normalizedColor & 0xff000000) >>> 24;
const g = (normalizedColor & 0x00ff0000) >>> 16;
const b = (normalizedColor & 0x0000ff00) >>> 8;
const a = (normalizedColor & 0x000000ff) / 255;
return {isColor: true, components: [r, g, b, a]};
} else {
const components: Array<string | number> = [];
let lastMatchEnd = 0;
let match: RegExp$matchResult;
while ((match = (numericComponentRegex.exec(input): any)) != null) {
if (match.index > lastMatchEnd) {
components.push(input.substring(lastMatchEnd, match.index));
}
components.push(parseFloat(match[0]));
lastMatchEnd = match.index + match[0].length;
}
invariant(
components.length > 0,
'outputRange must contain color or value with numeric component',
);
if (lastMatchEnd < input.length) {
components.push(input.substring(lastMatchEnd, input.length));
}
return {isColor: false, components};
}
}
/**
* Supports string shapes by extracting numbers so new values can be computed,
* and recombines those values into new strings of the same shape. Supports
* things like:
*
* rgba(123, 42, 99, 0.36) // colors
* -45deg // values with units
*/
function createStringInterpolation(
config: InterpolationConfigType<string>,
): (input: number) => string {
invariant(config.outputRange.length >= 2, 'Bad output range');
const outputRange = config.outputRange.map(mapStringToNumericComponents);
const isColor = outputRange[0].isColor;
if (__DEV__) {
invariant(
outputRange.every(output => output.isColor === isColor),
'All elements of output range should either be a color or a string with numeric components',
);
const firstOutput = outputRange[0].components;
invariant(
outputRange.every(
output => output.components.length === firstOutput.length,
),
'All elements of output range should have the same number of components',
);
invariant(
outputRange.every(output =>
output.components.every(
(component, i) =>
// $FlowFixMe[invalid-compare]
typeof component === 'number' || component === firstOutput[i],
),
),
'All elements of output range should have the same non-numeric components',
);
}
const numericComponents: $ReadOnlyArray<$ReadOnlyArray<number>> =
outputRange.map(output =>
isColor
? // $FlowFixMe[incompatible-type]
output.components
: // $FlowFixMe[incompatible-call]
output.components.filter(c => typeof c === 'number'),
);
const interpolations = numericComponents[0].map((_, i) =>
createNumericInterpolation({
...config,
outputRange: numericComponents.map(components => components[i]),
}),
);
if (!isColor) {
return input => {
const values = interpolations.map(interpolation => interpolation(input));
let i = 0;
return outputRange[0].components
.map(c => (typeof c === 'number' ? values[i++] : c))
.join('');
};
} else {
return input => {
const result = interpolations.map((interpolation, i) => {
const value = interpolation(input);
// rgba requires that the r,g,b are integers.... so we want to round them, but we *dont* want to
// round the opacity (4th column).
return i < 3 ? Math.round(value) : Math.round(value * 1000) / 1000;
});
return `rgba(${result[0]}, ${result[1]}, ${result[2]}, ${result[3]})`;
};
}
}
function findRange(input: number, inputRange: $ReadOnlyArray<number>) {
let i;
for (i = 1; i < inputRange.length - 1; ++i) {
if (inputRange[i] >= input) {
break;
}
}
return i - 1;
}
function checkValidRanges<OutputT: number | string>(
inputRange: $ReadOnlyArray<number>,
outputRange: $ReadOnlyArray<OutputT>,
) {
checkInfiniteRange('outputRange', outputRange);
checkInfiniteRange('inputRange', inputRange);
checkValidInputRange(inputRange);
invariant(
inputRange.length === outputRange.length,
'inputRange (' +
inputRange.length +
') and outputRange (' +
outputRange.length +
') must have the same length',
);
}
function checkValidInputRange(arr: $ReadOnlyArray<number>) {
invariant(arr.length >= 2, 'inputRange must have at least 2 elements');
const message =
'inputRange must be monotonically non-decreasing ' + String(arr);
for (let i = 1; i < arr.length; ++i) {
invariant(arr[i] >= arr[i - 1], message);
}
}
function checkInfiniteRange<OutputT: number | string>(
name: string,
arr: $ReadOnlyArray<OutputT>,
) {
invariant(arr.length >= 2, name + ' must have at least 2 elements');
invariant(
arr.length !== 2 || arr[0] !== -Infinity || arr[1] !== Infinity,
/* $FlowFixMe[incompatible-type] (>=0.13.0) - In the addition expression
* below this comment, one or both of the operands may be something that
* doesn't cleanly convert to a string, like undefined, null, and object,
* etc. If you really mean this implicit string conversion, you can do
* something like String(myThing) */
// $FlowFixMe[unsafe-addition]
name + 'cannot be ]-infinity;+infinity[ ' + arr,
);
}
export default class AnimatedInterpolation<
OutputT: number | string,
> extends AnimatedWithChildren {
_parent: AnimatedNode;
_config: InterpolationConfigType<OutputT>;
_interpolation: ?(input: number) => OutputT;
constructor(parent: AnimatedNode, config: InterpolationConfigType<OutputT>) {
super(config);
this._parent = parent;
this._config = config;
if (__DEV__) {
checkValidRanges(config.inputRange, config.outputRange);
// Create interpolation eagerly in dev, so we can signal errors faster
// even when using the native driver
this._getInterpolation();
}
}
_getInterpolation(): number => OutputT {
if (!this._interpolation) {
const config = this._config;
if (config.outputRange && typeof config.outputRange[0] === 'string') {
this._interpolation = (createStringInterpolation((config: any)): any);
} else {
this._interpolation = (createNumericInterpolation((config: any)): any);
}
}
return this._interpolation;
}
__makeNative(platformConfig: ?PlatformConfig) {
this._parent.__makeNative(platformConfig);
super.__makeNative(platformConfig);
}
__getValue(): OutputT {
const parentValue: number = this._parent.__getValue();
invariant(
typeof parentValue === 'number',
'Cannot interpolate an input which is not a number.',
);
return this._getInterpolation()(parentValue);
}
interpolate<NewOutputT: number | string>(
config: InterpolationConfigType<NewOutputT>,
): AnimatedInterpolation<NewOutputT> {
return new AnimatedInterpolation(this, config);
}
__attach(): void {
this._parent.__addChild(this);
super.__attach();
}
__detach(): void {
this._parent.__removeChild(this);
super.__detach();
}
__getNativeConfig(): any {
if (__DEV__) {
validateInterpolation(this._config);
}
// Only the `outputRange` can contain strings so we don't need to transform `inputRange` here
let outputRange = this._config.outputRange;
let outputType = null;
if (typeof outputRange[0] === 'string') {
// $FlowFixMe[incompatible-type]
outputRange = ((outputRange: $ReadOnlyArray<string>).map(value => {
const processedColor = processColor(value);
if (typeof processedColor === 'number') {
outputType = 'color';
return processedColor;
} else {
return NativeAnimatedHelper.transformDataType(value);
}
}): any);
}
return {
inputRange: this._config.inputRange,
outputRange,
outputType,
extrapolateLeft:
this._config.extrapolateLeft || this._config.extrapolate || 'extend',
extrapolateRight:
this._config.extrapolateRight || this._config.extrapolate || 'extend',
type: 'interpolation',
debugID: this.__getDebugID(),
};
}
}

View File

@@ -0,0 +1,66 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {InterpolationConfigType} from './AnimatedInterpolation';
import type AnimatedNode from './AnimatedNode';
import type {AnimatedNodeConfig} from './AnimatedNode';
import AnimatedInterpolation from './AnimatedInterpolation';
import AnimatedWithChildren from './AnimatedWithChildren';
export default class AnimatedModulo extends AnimatedWithChildren {
_a: AnimatedNode;
_modulus: number;
constructor(a: AnimatedNode, modulus: number, config?: ?AnimatedNodeConfig) {
super(config);
this._a = a;
this._modulus = modulus;
}
__makeNative(platformConfig: ?PlatformConfig) {
this._a.__makeNative(platformConfig);
super.__makeNative(platformConfig);
}
__getValue(): number {
return (
((this._a.__getValue() % this._modulus) + this._modulus) % this._modulus
);
}
interpolate<OutputT: number | string>(
config: InterpolationConfigType<OutputT>,
): AnimatedInterpolation<OutputT> {
return new AnimatedInterpolation(this, config);
}
__attach(): void {
this._a.__addChild(this);
super.__attach();
}
__detach(): void {
this._a.__removeChild(this);
super.__detach();
}
__getNativeConfig(): any {
return {
type: 'modulus',
input: this._a.__getNativeTag(),
modulus: this._modulus,
debugID: this.__getDebugID(),
};
}
}

View File

@@ -0,0 +1,70 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {InterpolationConfigType} from './AnimatedInterpolation';
import type AnimatedNode from './AnimatedNode';
import type {AnimatedNodeConfig} from './AnimatedNode';
import AnimatedInterpolation from './AnimatedInterpolation';
import AnimatedValue from './AnimatedValue';
import AnimatedWithChildren from './AnimatedWithChildren';
export default class AnimatedMultiplication extends AnimatedWithChildren {
_a: AnimatedNode;
_b: AnimatedNode;
constructor(
a: AnimatedNode | number,
b: AnimatedNode | number,
config?: ?AnimatedNodeConfig,
) {
super(config);
this._a = typeof a === 'number' ? new AnimatedValue(a) : a;
this._b = typeof b === 'number' ? new AnimatedValue(b) : b;
}
__makeNative(platformConfig: ?PlatformConfig) {
this._a.__makeNative(platformConfig);
this._b.__makeNative(platformConfig);
super.__makeNative(platformConfig);
}
__getValue(): number {
return this._a.__getValue() * this._b.__getValue();
}
interpolate<OutputT: number | string>(
config: InterpolationConfigType<OutputT>,
): AnimatedInterpolation<OutputT> {
return new AnimatedInterpolation(this, config);
}
__attach(): void {
this._a.__addChild(this);
this._b.__addChild(this);
super.__attach();
}
__detach(): void {
this._a.__removeChild(this);
this._b.__removeChild(this);
super.__detach();
}
__getNativeConfig(): any {
return {
type: 'multiplication',
input: [this._a.__getNativeTag(), this._b.__getNativeTag()],
debugID: this.__getDebugID(),
};
}
}

View File

@@ -0,0 +1,187 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper';
import invariant from 'invariant';
type ValueListenerCallback = (state: {value: number, ...}) => mixed;
export type AnimatedNodeConfig = $ReadOnly<{
debugID?: string,
unstable_disableBatchingForNativeCreate?: boolean,
}>;
let _uniqueId = 1;
let _assertNativeAnimatedModule: ?() => void = () => {
NativeAnimatedHelper.assertNativeAnimatedModule();
// We only have to assert that the module exists once. After we've asserted
// this, clear out the function so we know to skip it in the future.
_assertNativeAnimatedModule = null;
};
export default class AnimatedNode {
_listeners: Map<string, ValueListenerCallback>;
_platformConfig: ?PlatformConfig = undefined;
constructor(
config?: ?$ReadOnly<{
...AnimatedNodeConfig,
...
}>,
) {
this._listeners = new Map();
if (__DEV__) {
this.__debugID = config?.debugID;
}
this.__disableBatchingForNativeCreate =
config?.unstable_disableBatchingForNativeCreate;
}
__attach(): void {}
__detach(): void {
this.removeAllListeners();
if (this.__isNative && this.__nativeTag != null) {
NativeAnimatedHelper.API.dropAnimatedNode(this.__nativeTag);
this.__nativeTag = undefined;
}
}
__getValue(): any {}
__getAnimatedValue(): any {
return this.__getValue();
}
__addChild(child: AnimatedNode) {}
__removeChild(child: AnimatedNode) {}
__getChildren(): $ReadOnlyArray<AnimatedNode> {
return [];
}
/* Methods and props used by native Animated impl */
__isNative: boolean = false;
__nativeTag: ?number = undefined;
__disableBatchingForNativeCreate: ?boolean = undefined;
__makeNative(platformConfig: ?PlatformConfig): void {
// Subclasses are expected to set `__isNative` to true before this.
invariant(
this.__isNative,
'This node cannot be made a "native" animated node',
);
this._platformConfig = platformConfig;
}
/**
* Adds an asynchronous listener to the value so you can observe updates from
* animations. This is useful because there is no way to
* synchronously read the value because it might be driven natively.
*
* See https://reactnative.dev/docs/animatedvalue#addlistener
*/
addListener(callback: (value: any) => mixed): string {
const id = String(_uniqueId++);
this._listeners.set(id, callback);
return id;
}
/**
* Unregister a listener. The `id` param shall match the identifier
* previously returned by `addListener()`.
*
* See https://reactnative.dev/docs/animatedvalue#removelistener
*/
removeListener(id: string): void {
this._listeners.delete(id);
}
/**
* Remove all registered listeners.
*
* See https://reactnative.dev/docs/animatedvalue#removealllisteners
*/
removeAllListeners(): void {
this._listeners.clear();
}
hasListeners(): boolean {
return this._listeners.size > 0;
}
__onAnimatedValueUpdateReceived(value: number, offset: number): void {
this.__callListeners(value + offset);
}
__callListeners(value: number): void {
const event = {value};
this._listeners.forEach(listener => {
listener(event);
});
}
__getNativeTag(): number {
let nativeTag = this.__nativeTag;
if (nativeTag == null) {
_assertNativeAnimatedModule?.();
// `__isNative` is initialized as false and only ever set to true. So we
// only need to check it once here when initializing `__nativeTag`.
invariant(
this.__isNative,
'Attempt to get native tag from node not marked as "native"',
);
nativeTag = NativeAnimatedHelper.generateNewNodeTag();
this.__nativeTag = nativeTag;
const config = this.__getNativeConfig();
if (this._platformConfig) {
config.platformConfig = this._platformConfig;
}
if (this.__disableBatchingForNativeCreate) {
config.disableBatchingForNativeCreate = true;
}
NativeAnimatedHelper.API.createAnimatedNode(nativeTag, config);
}
return nativeTag;
}
__getNativeConfig(): Object {
throw new Error(
'This JS animated node type cannot be used as native animated node',
);
}
__getPlatformConfig(): ?PlatformConfig {
return this._platformConfig;
}
__setPlatformConfig(platformConfig: ?PlatformConfig) {
this._platformConfig = platformConfig;
}
/**
* NOTE: This is intended to prevent `JSON.stringify` from throwing "cyclic
* structure" errors in React DevTools. Avoid depending on this!
*/
toJSON(): mixed {
return this.__getValue();
}
__debugID: ?string = undefined;
__getDebugID(): ?string {
if (__DEV__) {
return this.__debugID;
}
return undefined;
}
}

View File

@@ -0,0 +1,169 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {AnimatedNodeConfig} from './AnimatedNode';
import AnimatedNode from './AnimatedNode';
import AnimatedWithChildren from './AnimatedWithChildren';
import {isValidElement} from 'react';
const MAX_DEPTH = 5;
export function isPlainObject(
value: mixed,
/* $FlowFixMe[incompatible-type-guard] - Flow does not know that the prototype
and ReactElement checks preserve the type refinement of `value`. */
): value is $ReadOnly<{[string]: mixed}> {
return (
// $FlowFixMe[incompatible-type-guard]
value !== null &&
typeof value === 'object' &&
Object.getPrototypeOf(value).isPrototypeOf(Object) &&
!isValidElement(value)
);
}
function flatAnimatedNodes(
value: mixed,
nodes: Array<AnimatedNode> = [],
depth: number = 0,
): Array<AnimatedNode> {
if (depth >= MAX_DEPTH) {
return nodes;
}
if (value instanceof AnimatedNode) {
nodes.push(value);
} else if (Array.isArray(value)) {
for (let ii = 0, length = value.length; ii < length; ii++) {
const element = value[ii];
flatAnimatedNodes(element, nodes, depth + 1);
}
} else if (isPlainObject(value)) {
const keys = Object.keys(value);
for (let ii = 0, length = keys.length; ii < length; ii++) {
const key = keys[ii];
flatAnimatedNodes(value[key], nodes, depth + 1);
}
}
return nodes;
}
// Returns a copy of value with a transformation fn applied to any AnimatedNodes
function mapAnimatedNodes(value: any, fn: any => any, depth: number = 0): any {
if (depth >= MAX_DEPTH) {
return value;
}
if (value instanceof AnimatedNode) {
return fn(value);
} else if (Array.isArray(value)) {
return value.map(element => mapAnimatedNodes(element, fn, depth + 1));
} else if (isPlainObject(value)) {
const result: {[string]: any} = {};
const keys = Object.keys(value);
for (let ii = 0, length = keys.length; ii < length; ii++) {
const key = keys[ii];
result[key] = mapAnimatedNodes(value[key], fn, depth + 1);
}
return result;
} else {
return value;
}
}
export default class AnimatedObject extends AnimatedWithChildren {
_nodes: $ReadOnlyArray<AnimatedNode>;
_value: mixed;
/**
* Creates an `AnimatedObject` if `value` contains `AnimatedNode` instances.
* Otherwise, returns `null`.
*/
static from(value: mixed): ?AnimatedObject {
const nodes = flatAnimatedNodes(value);
if (nodes.length === 0) {
return null;
}
return new AnimatedObject(nodes, value);
}
/**
* Should only be called by `AnimatedObject.from`.
*/
constructor(
nodes: $ReadOnlyArray<AnimatedNode>,
value: mixed,
config?: ?AnimatedNodeConfig,
) {
super(config);
this._nodes = nodes;
this._value = value;
}
__getValue(): any {
return mapAnimatedNodes(this._value, node => {
return node.__getValue();
});
}
__getValueWithStaticObject(staticObject: mixed): any {
const nodes = this._nodes;
let index = 0;
// NOTE: We can depend on `this._value` and `staticObject` sharing a
// structure because of `useAnimatedPropsMemo`.
return mapAnimatedNodes(staticObject, () => nodes[index++].__getValue());
}
__getAnimatedValue(): any {
return mapAnimatedNodes(this._value, node => {
return node.__getAnimatedValue();
});
}
__attach(): void {
const nodes = this._nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const node = nodes[ii];
node.__addChild(this);
}
super.__attach();
}
__detach(): void {
const nodes = this._nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const node = nodes[ii];
node.__removeChild(this);
}
super.__detach();
}
__makeNative(platformConfig: ?PlatformConfig): void {
const nodes = this._nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const node = nodes[ii];
node.__makeNative(platformConfig);
}
super.__makeNative(platformConfig);
}
__getNativeConfig(): any {
return {
type: 'object',
value: mapAnimatedNodes(this._value, node => {
return {nodeTag: node.__getNativeTag()};
}),
debugID: this.__getDebugID(),
};
}
}

View File

@@ -0,0 +1,332 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {AnimatedNodeConfig} from './AnimatedNode';
import type {AnimatedStyleAllowlist} from './AnimatedStyle';
import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper';
import {findNodeHandle} from '../../ReactNative/RendererProxy';
import flattenStyle from '../../StyleSheet/flattenStyle';
import {AnimatedEvent} from '../AnimatedEvent';
import AnimatedNode from './AnimatedNode';
import AnimatedObject from './AnimatedObject';
import AnimatedStyle from './AnimatedStyle';
import invariant from 'invariant';
export type AnimatedPropsAllowlist = $ReadOnly<{
style?: ?AnimatedStyleAllowlist,
[key: string]: true | AnimatedStyleAllowlist,
}>;
type TargetView = {
+instance: TargetViewInstance,
connectedViewTag: ?number,
};
type TargetViewInstance = React.ElementRef<React.ElementType>;
function createAnimatedProps(
inputProps: {[string]: mixed},
allowlist: ?AnimatedPropsAllowlist,
): [$ReadOnlyArray<string>, $ReadOnlyArray<AnimatedNode>, {[string]: mixed}] {
const nodeKeys: Array<string> = [];
const nodes: Array<AnimatedNode> = [];
const props: {[string]: mixed} = {};
const keys = Object.keys(inputProps);
for (let ii = 0, length = keys.length; ii < length; ii++) {
const key = keys[ii];
const value = inputProps[key];
let staticValue = value;
if (allowlist == null || hasOwn(allowlist, key)) {
let node;
if (key === 'style') {
// Ignore `style` if it is not an object (or array).
if (typeof value === 'object' && value != null) {
// Even if we do not find any `AnimatedNode` values in `style`, we
// still need to use the flattened `style` object because static
// values can shadow `AnimatedNode` values. We need to make sure that
// we propagate the flattened `style` object to the `props` object.
const flatStyle = flattenStyle(value as $FlowFixMe);
node = AnimatedStyle.from(flatStyle, allowlist?.style, value);
staticValue = flatStyle;
}
} else if (value instanceof AnimatedNode) {
node = value;
} else {
node = AnimatedObject.from(value);
}
if (node == null) {
props[key] = staticValue;
} else {
nodeKeys.push(key);
nodes.push(node);
props[key] = node;
}
} else {
if (__DEV__) {
// WARNING: This is a potentially expensive check that we should only
// do in development. Without this check in development, it might be
// difficult to identify which props need to be allowlisted.
if (AnimatedObject.from(inputProps[key]) != null) {
console.error(
`AnimatedProps: ${key} is not allowlisted for animation, but it ` +
'contains AnimatedNode values; props allowing animation: ',
allowlist,
);
}
}
props[key] = value;
}
}
return [nodeKeys, nodes, props];
}
export default class AnimatedProps extends AnimatedNode {
_callback: () => void;
_nodeKeys: $ReadOnlyArray<string>;
_nodes: $ReadOnlyArray<AnimatedNode>;
_props: {[string]: mixed};
_target: ?TargetView = null;
constructor(
inputProps: {[string]: mixed},
callback: () => void,
allowlist?: ?AnimatedPropsAllowlist,
config?: ?AnimatedNodeConfig,
) {
super(config);
const [nodeKeys, nodes, props] = createAnimatedProps(inputProps, allowlist);
this._nodeKeys = nodeKeys;
this._nodes = nodes;
this._props = props;
this._callback = callback;
}
__getValue(): Object {
const props: {[string]: mixed} = {};
const keys = Object.keys(this._props);
for (let ii = 0, length = keys.length; ii < length; ii++) {
const key = keys[ii];
const value = this._props[key];
if (value instanceof AnimatedNode) {
props[key] = value.__getValue();
} else if (value instanceof AnimatedEvent) {
props[key] = value.__getHandler();
} else {
props[key] = value;
}
}
return props;
}
/**
* Creates a new `props` object that contains the same props as the supplied
* `staticProps` object, except with animated nodes for any props that were
* created by this `AnimatedProps` instance.
*/
__getValueWithStaticProps(staticProps: Object): Object {
const props: {[string]: mixed} = {...staticProps};
const keys = Object.keys(staticProps);
for (let ii = 0, length = keys.length; ii < length; ii++) {
const key = keys[ii];
const maybeNode = this._props[key];
if (key === 'style') {
const staticStyle = staticProps.style;
const flatStaticStyle = flattenStyle(staticStyle);
if (maybeNode instanceof AnimatedStyle) {
const mutableStyle: {[string]: mixed} =
flatStaticStyle == null
? {}
: flatStaticStyle === staticStyle
? // Copy the input style, since we'll mutate it below.
{...flatStaticStyle}
: // Reuse `flatStaticStyle` if it is a newly created object.
flatStaticStyle;
maybeNode.__replaceAnimatedNodeWithValues(mutableStyle);
props[key] = maybeNode.__getValueForStyle(mutableStyle);
} else {
props[key] = flatStaticStyle;
}
} else if (maybeNode instanceof AnimatedNode) {
props[key] = maybeNode.__getValue();
} else if (maybeNode instanceof AnimatedEvent) {
props[key] = maybeNode.__getHandler();
}
}
return props;
}
__getNativeAnimatedEventTuples(): $ReadOnlyArray<[string, AnimatedEvent]> {
const tuples = [];
const keys = Object.keys(this._props);
for (let ii = 0, length = keys.length; ii < length; ii++) {
const key = keys[ii];
const value = this._props[key];
if (value instanceof AnimatedEvent && value.__isNative) {
tuples.push([key, value]);
}
}
return tuples;
}
__getAnimatedValue(): Object {
const props: {[string]: mixed} = {};
const nodeKeys = this._nodeKeys;
const nodes = this._nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const key = nodeKeys[ii];
const node = nodes[ii];
props[key] = node.__getAnimatedValue();
}
return props;
}
__attach(): void {
const nodes = this._nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const node = nodes[ii];
node.__addChild(this);
}
super.__attach();
}
__detach(): void {
if (this.__isNative && this._target != null) {
this.#disconnectAnimatedView(this._target);
}
this._target = null;
const nodes = this._nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const node = nodes[ii];
node.__removeChild(this);
}
super.__detach();
}
update(): void {
this._callback();
}
__makeNative(platformConfig: ?PlatformConfig): void {
const nodes = this._nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const node = nodes[ii];
node.__makeNative(platformConfig);
}
if (!this.__isNative) {
this.__isNative = true;
// Since this does not call the super.__makeNative, we need to store the
// supplied platformConfig here, before calling #connectAnimatedView
// where it will be needed to traverse the graph of attached values.
super.__setPlatformConfig(platformConfig);
if (this._target != null) {
this.#connectAnimatedView(this._target);
}
}
}
setNativeView(instance: TargetViewInstance): void {
if (this._target?.instance === instance) {
return;
}
this._target = {instance, connectedViewTag: null};
if (this.__isNative) {
this.#connectAnimatedView(this._target);
}
}
#connectAnimatedView(target: TargetView): void {
invariant(this.__isNative, 'Expected node to be marked as "native"');
let viewTag: ?number = findNodeHandle(target.instance);
if (viewTag == null) {
if (process.env.NODE_ENV === 'test') {
viewTag = -1;
} else {
throw new Error('Unable to locate attached view in the native tree');
}
}
NativeAnimatedHelper.API.connectAnimatedNodeToView(
this.__getNativeTag(),
viewTag,
);
target.connectedViewTag = viewTag;
}
#disconnectAnimatedView(target: TargetView): void {
invariant(this.__isNative, 'Expected node to be marked as "native"');
const viewTag = target.connectedViewTag;
if (viewTag == null) {
return;
}
NativeAnimatedHelper.API.disconnectAnimatedNodeFromView(
this.__getNativeTag(),
viewTag,
);
target.connectedViewTag = null;
}
__restoreDefaultValues(): void {
// When using the native driver, view properties need to be restored to
// their default values manually since react no longer tracks them. This
// is needed to handle cases where a prop driven by native animated is removed
// after having been changed natively by an animation.
if (this.__isNative) {
NativeAnimatedHelper.API.restoreDefaultValues(this.__getNativeTag());
}
}
__getNativeConfig(): Object {
const platformConfig = this.__getPlatformConfig();
const propsConfig: {[string]: number} = {};
const nodeKeys = this._nodeKeys;
const nodes = this._nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const key = nodeKeys[ii];
const node = nodes[ii];
node.__makeNative(platformConfig);
propsConfig[key] = node.__getNativeTag();
}
return {
type: 'props',
props: propsConfig,
debugID: this.__getDebugID(),
};
}
}
// Supported versions of JSC do not implement the newer Object.hasOwn. Remove
// this shim when they do.
// $FlowFixMe[method-unbinding]
const _hasOwnProp = Object.prototype.hasOwnProperty;
const hasOwn: (obj: $ReadOnly<{...}>, prop: string) => boolean =
// $FlowFixMe[method-unbinding]
Object.hasOwn ?? ((obj, prop) => _hasOwnProp.call(obj, prop));

View File

@@ -0,0 +1,258 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {AnimatedNodeConfig} from './AnimatedNode';
import {validateStyles} from '../../../src/private/animated/NativeAnimatedValidation';
import * as ReactNativeFeatureFlags from '../../../src/private/featureflags/ReactNativeFeatureFlags';
import Platform from '../../Utilities/Platform';
import AnimatedNode from './AnimatedNode';
import AnimatedObject from './AnimatedObject';
import AnimatedTransform from './AnimatedTransform';
import AnimatedWithChildren from './AnimatedWithChildren';
export type AnimatedStyleAllowlist = $ReadOnly<{[string]: true}>;
type FlatStyle = {[string]: mixed};
type FlatStyleForWeb<TStyle: FlatStyle> = [mixed, TStyle];
function createAnimatedStyle(
flatStyle: FlatStyle,
allowlist: ?AnimatedStyleAllowlist,
keepUnanimatedValues: boolean,
): [$ReadOnlyArray<string>, $ReadOnlyArray<AnimatedNode>, {[string]: mixed}] {
const nodeKeys: Array<string> = [];
const nodes: Array<AnimatedNode> = [];
const style: {[string]: mixed} = {};
const keys = Object.keys(flatStyle);
for (let ii = 0, length = keys.length; ii < length; ii++) {
const key = keys[ii];
const value = flatStyle[key];
if (allowlist == null || hasOwn(allowlist, key)) {
let node;
if (value != null && key === 'transform') {
node = ReactNativeFeatureFlags.shouldUseAnimatedObjectForTransform()
? AnimatedObject.from(value)
: // $FlowFixMe[incompatible-type] - `value` is mixed.
AnimatedTransform.from(value);
} else if (value instanceof AnimatedNode) {
node = value;
} else {
node = AnimatedObject.from(value);
}
if (node == null) {
if (keepUnanimatedValues) {
style[key] = value;
}
} else {
nodeKeys.push(key);
nodes.push(node);
style[key] = node;
}
} else {
if (__DEV__) {
// WARNING: This is a potentially expensive check that we should only
// do in development. Without this check in development, it might be
// difficult to identify which styles need to be allowlisted.
if (AnimatedObject.from(flatStyle[key]) != null) {
console.error(
`AnimatedStyle: ${key} is not allowlisted for animation, but ` +
'it contains AnimatedNode values; styles allowing animation: ',
allowlist,
);
}
}
if (keepUnanimatedValues) {
style[key] = value;
}
}
}
return [nodeKeys, nodes, style];
}
export default class AnimatedStyle extends AnimatedWithChildren {
_originalStyleForWeb: ?mixed;
_nodeKeys: $ReadOnlyArray<string>;
_nodes: $ReadOnlyArray<AnimatedNode>;
_style: {[string]: mixed};
/**
* Creates an `AnimatedStyle` if `value` contains `AnimatedNode` instances.
* Otherwise, returns `null`.
*/
static from(
flatStyle: ?FlatStyle,
allowlist: ?AnimatedStyleAllowlist,
originalStyleForWeb: ?mixed,
): ?AnimatedStyle {
if (flatStyle == null) {
return null;
}
const [nodeKeys, nodes, style] = createAnimatedStyle(
flatStyle,
allowlist,
/* $FlowFixMe[invalid-compare] Error discovered during Constant Condition
* roll out. See https://fburl.com/workplace/4oq3zi07. */
Platform.OS !== 'web',
);
if (nodes.length === 0) {
return null;
}
return new AnimatedStyle(nodeKeys, nodes, style, originalStyleForWeb);
}
constructor(
nodeKeys: $ReadOnlyArray<string>,
nodes: $ReadOnlyArray<AnimatedNode>,
style: {[string]: mixed},
originalStyleForWeb: ?mixed,
config?: ?AnimatedNodeConfig,
) {
super(config);
this._nodeKeys = nodeKeys;
this._nodes = nodes;
this._style = style;
if ((Platform.OS as string) === 'web') {
// $FlowFixMe[cannot-write] - Intentional shadowing.
this.__getValueForStyle = resultStyle => [
originalStyleForWeb,
resultStyle,
];
}
}
__getValue(): FlatStyleForWeb<FlatStyle> | FlatStyle {
const style: {[string]: mixed} = {};
const keys = Object.keys(this._style);
for (let ii = 0, length = keys.length; ii < length; ii++) {
const key = keys[ii];
const value = this._style[key];
if (value instanceof AnimatedNode) {
style[key] = value.__getValue();
} else {
style[key] = value;
}
}
return this.__getValueForStyle(style);
}
/**
* See the constructor, where this is shadowed on web platforms.
*/
__getValueForStyle<TStyle: FlatStyle>(
style: TStyle,
): FlatStyleForWeb<TStyle> | TStyle {
return style;
}
/**
* Mutates the supplied `style` object such that animated nodes are replaced
* with rasterized values.
*/
__replaceAnimatedNodeWithValues(style: {[string]: mixed}): void {
const keys = Object.keys(style);
for (let ii = 0, length = keys.length; ii < length; ii++) {
const key = keys[ii];
const maybeNode = this._style[key];
if (key === 'transform' && maybeNode instanceof AnimatedTransform) {
style[key] = maybeNode.__getValueWithStaticTransforms(
// NOTE: This check should not be necessary, but the types are not
// enforced as of this writing.
Array.isArray(style[key]) ? style[key] : [],
);
} else if (maybeNode instanceof AnimatedObject) {
style[key] = maybeNode.__getValueWithStaticObject(style[key]);
} else if (maybeNode instanceof AnimatedNode) {
style[key] = maybeNode.__getValue();
}
}
}
__getAnimatedValue(): Object {
const style: {[string]: mixed} = {};
const nodeKeys = this._nodeKeys;
const nodes = this._nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const key = nodeKeys[ii];
const node = nodes[ii];
style[key] = node.__getAnimatedValue();
}
return style;
}
__attach(): void {
const nodes = this._nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const node = nodes[ii];
node.__addChild(this);
}
super.__attach();
}
__detach(): void {
const nodes = this._nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const node = nodes[ii];
node.__removeChild(this);
}
super.__detach();
}
__makeNative(platformConfig: ?PlatformConfig) {
const nodes = this._nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const node = nodes[ii];
node.__makeNative(platformConfig);
}
super.__makeNative(platformConfig);
}
__getNativeConfig(): Object {
const platformConfig = this.__getPlatformConfig();
const styleConfig: {[string]: ?number} = {};
const nodeKeys = this._nodeKeys;
const nodes = this._nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const key = nodeKeys[ii];
const node = nodes[ii];
node.__makeNative(platformConfig);
styleConfig[key] = node.__getNativeTag();
}
if (__DEV__) {
validateStyles(styleConfig);
}
return {
type: 'style',
style: styleConfig,
debugID: this.__getDebugID(),
};
}
}
// Supported versions of JSC do not implement the newer Object.hasOwn. Remove
// this shim when they do.
// $FlowFixMe[method-unbinding]
const _hasOwnProp = Object.prototype.hasOwnProperty;
const hasOwn: (obj: $ReadOnly<{...}>, prop: string) => boolean =
// $FlowFixMe[method-unbinding]
Object.hasOwn ?? ((obj, prop) => _hasOwnProp.call(obj, prop));

View File

@@ -0,0 +1,71 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {InterpolationConfigType} from './AnimatedInterpolation';
import type AnimatedNode from './AnimatedNode';
import type {AnimatedNodeConfig} from './AnimatedNode';
import AnimatedInterpolation from './AnimatedInterpolation';
import AnimatedValue from './AnimatedValue';
import AnimatedWithChildren from './AnimatedWithChildren';
export default class AnimatedSubtraction extends AnimatedWithChildren {
_a: AnimatedNode;
_b: AnimatedNode;
constructor(
a: AnimatedNode | number,
b: AnimatedNode | number,
config?: ?AnimatedNodeConfig,
) {
super(config);
this._a = typeof a === 'number' ? new AnimatedValue(a) : a;
this._b = typeof b === 'number' ? new AnimatedValue(b) : b;
}
__makeNative(platformConfig: ?PlatformConfig) {
this._a.__makeNative(platformConfig);
this._b.__makeNative(platformConfig);
super.__makeNative(platformConfig);
}
__getValue(): number {
return this._a.__getValue() - this._b.__getValue();
}
interpolate<OutputT: number | string>(
config: InterpolationConfigType<OutputT>,
): AnimatedInterpolation<OutputT> {
return new AnimatedInterpolation(this, config);
}
__attach(): void {
this._a.__addChild(this);
this._b.__addChild(this);
super.__attach();
}
__detach(): void {
this._a.__removeChild(this);
this._b.__removeChild(this);
super.__detach();
}
__getNativeConfig(): any {
return {
type: 'subtraction',
input: [this._a.__getNativeTag(), this._b.__getNativeTag()],
debugID: this.__getDebugID(),
};
}
}

View File

@@ -0,0 +1,104 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {EndCallback} from '../animations/Animation';
import type {AnimatedNodeConfig} from './AnimatedNode';
import type AnimatedValue from './AnimatedValue';
import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper';
import AnimatedNode from './AnimatedNode';
export default class AnimatedTracking extends AnimatedNode {
_value: AnimatedValue;
_parent: AnimatedNode;
_callback: ?EndCallback;
_animationConfig: Object;
_animationClass: any;
_useNativeDriver: boolean;
constructor(
value: AnimatedValue,
parent: AnimatedNode,
animationClass: any,
animationConfig: Object,
callback?: ?EndCallback,
config?: ?AnimatedNodeConfig,
) {
super(config);
this._value = value;
this._parent = parent;
this._animationClass = animationClass;
this._animationConfig = animationConfig;
this._useNativeDriver =
NativeAnimatedHelper.shouldUseNativeDriver(animationConfig);
this._callback = callback;
this.__attach();
}
__makeNative(platformConfig: ?PlatformConfig) {
this.__isNative = true;
this._parent.__makeNative(platformConfig);
super.__makeNative(platformConfig);
this._value.__makeNative(platformConfig);
}
__getValue(): Object {
return this._parent.__getValue();
}
__attach(): void {
this._parent.__addChild(this);
if (this._useNativeDriver) {
// when the tracking starts we need to convert this node to a "native node"
// so that the parent node will be made "native" too. This is necessary as
// if we don't do this `update` method will get called. At that point it
// may be too late as it would mean the JS driver has already started
// updating node values
let {platformConfig} = this._animationConfig;
this.__makeNative(platformConfig);
}
super.__attach();
}
__detach(): void {
this._parent.__removeChild(this);
super.__detach();
}
update(): void {
this._value.animate(
new this._animationClass({
...this._animationConfig,
toValue: (this._animationConfig.toValue: any).__getValue(),
}),
this._callback,
);
}
__getNativeConfig(): any {
const animation = new this._animationClass({
...this._animationConfig,
// remove toValue from the config as it's a ref to Animated.Value
toValue: undefined,
});
const animationConfig = animation.__getNativeAnimationConfig();
return {
type: 'tracking',
animationId: NativeAnimatedHelper.generateNewAnimationId(),
animationConfig,
toValue: this._parent.__getNativeTag(),
value: this._value.__getNativeTag(),
debugID: this.__getDebugID(),
};
}
}

View File

@@ -0,0 +1,202 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {AnimatedNodeConfig} from './AnimatedNode';
import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper';
import {validateTransform} from '../../../src/private/animated/NativeAnimatedValidation';
import AnimatedNode from './AnimatedNode';
import AnimatedWithChildren from './AnimatedWithChildren';
type Transform<T = AnimatedNode> = {
[string]:
| number
| string
| T
| $ReadOnlyArray<number | string | T>
| {[string]: number | string | T},
};
function flatAnimatedNodes(
transforms: $ReadOnlyArray<Transform<>>,
): Array<AnimatedNode> {
const nodes = [];
for (let ii = 0, length = transforms.length; ii < length; ii++) {
const transform = transforms[ii];
// There should be exactly one property in `transform`.
for (const key in transform) {
const value = transform[key];
if (value instanceof AnimatedNode) {
nodes.push(value);
}
}
}
return nodes;
}
export default class AnimatedTransform extends AnimatedWithChildren {
// NOTE: For potentially historical reasons, some operations only operate on
// the first level of AnimatedNode instances. This optimizes that bevavior.
_nodes: $ReadOnlyArray<AnimatedNode>;
_transforms: $ReadOnlyArray<Transform<>>;
/**
* Creates an `AnimatedTransform` if `transforms` contains `AnimatedNode`
* instances. Otherwise, returns `null`.
*/
static from(transforms: $ReadOnlyArray<Transform<>>): ?AnimatedTransform {
const nodes = flatAnimatedNodes(
// NOTE: This check should not be necessary, but the types are not
// enforced as of this writing. This check should be hoisted to
// instantiation sites.
Array.isArray(transforms) ? transforms : [],
);
if (nodes.length === 0) {
return null;
}
return new AnimatedTransform(nodes, transforms);
}
constructor(
nodes: $ReadOnlyArray<AnimatedNode>,
transforms: $ReadOnlyArray<Transform<>>,
config?: ?AnimatedNodeConfig,
) {
super(config);
this._nodes = nodes;
this._transforms = transforms;
}
__makeNative(platformConfig: ?PlatformConfig) {
const nodes = this._nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const node = nodes[ii];
node.__makeNative(platformConfig);
}
super.__makeNative(platformConfig);
}
__getValue(): $ReadOnlyArray<Transform<any>> {
return mapTransforms(this._transforms, animatedNode =>
animatedNode.__getValue(),
);
}
__getValueWithStaticTransforms(
staticTransforms: $ReadOnlyArray<Object>,
): $ReadOnlyArray<Object> {
const values = [];
mapTransforms(this._transforms, node => {
values.push(node.__getValue());
});
// NOTE: We can depend on `this._transforms` and `staticTransforms` sharing
// a structure because of `useAnimatedPropsMemo`.
return mapTransforms(staticTransforms, () => values.shift());
}
__getAnimatedValue(): $ReadOnlyArray<Transform<any>> {
return mapTransforms(this._transforms, animatedNode =>
animatedNode.__getAnimatedValue(),
);
}
__attach(): void {
const nodes = this._nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const node = nodes[ii];
node.__addChild(this);
}
super.__attach();
}
__detach(): void {
const nodes = this._nodes;
for (let ii = 0, length = nodes.length; ii < length; ii++) {
const node = nodes[ii];
node.__removeChild(this);
}
super.__detach();
}
__getNativeConfig(): any {
const transformsConfig: Array<any> = [];
const transforms = this._transforms;
for (let ii = 0, length = transforms.length; ii < length; ii++) {
const transform = transforms[ii];
// There should be exactly one property in `transform`.
for (const key in transform) {
const value = transform[key];
if (value instanceof AnimatedNode) {
transformsConfig.push({
type: 'animated',
property: key,
nodeTag: value.__getNativeTag(),
});
} else {
transformsConfig.push({
type: 'static',
property: key,
/* $FlowFixMe[incompatible-type] - `value` can be an array or an
object. This is not currently handled by `transformDataType`.
Migrating to `TransformObject` might solve this. */
value: NativeAnimatedHelper.transformDataType(value),
});
}
}
}
if (__DEV__) {
validateTransform(transformsConfig);
}
return {
type: 'transform',
transforms: transformsConfig,
debugID: this.__getDebugID(),
};
}
}
function mapTransforms<T>(
transforms: $ReadOnlyArray<Transform<>>,
mapFunction: AnimatedNode => T,
): $ReadOnlyArray<Transform<T>> {
return transforms.map(transform => {
const result: Transform<T> = {};
// There should be exactly one property in `transform`.
for (const key in transform) {
const value = transform[key];
if (value instanceof AnimatedNode) {
result[key] = mapFunction(value);
} else if (Array.isArray(value)) {
result[key] = value.map(element =>
element instanceof AnimatedNode ? mapFunction(element) : element,
);
} else if (typeof value === 'object') {
const object: {[string]: number | string | T} = {};
for (const propertyName in value) {
const propertyValue = value[propertyName];
object[propertyName] =
propertyValue instanceof AnimatedNode
? mapFunction(propertyValue)
: propertyValue;
}
result[key] = object;
} else {
result[key] = value;
}
}
return result;
});
}

View File

@@ -0,0 +1,371 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
import type {EventSubscription} from '../../vendor/emitter/EventEmitter';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type Animation from '../animations/Animation';
import type {EndCallback} from '../animations/Animation';
import type {InterpolationConfigType} from './AnimatedInterpolation';
import type AnimatedNode from './AnimatedNode';
import type {AnimatedNodeConfig} from './AnimatedNode';
import type AnimatedTracking from './AnimatedTracking';
import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper';
import AnimatedInterpolation from './AnimatedInterpolation';
import AnimatedWithChildren from './AnimatedWithChildren';
export type AnimatedValueConfig = $ReadOnly<{
...AnimatedNodeConfig,
useNativeDriver: boolean,
}>;
const NativeAnimatedAPI = NativeAnimatedHelper.API;
/**
* Animated works by building a directed acyclic graph of dependencies
* transparently when you render your Animated components.
*
* new Animated.Value(0)
* .interpolate() .interpolate() new Animated.Value(1)
* opacity translateY scale
* style transform
* View#234 style
* View#123
*
* A) Top Down phase
* When an Animated.Value is updated, we recursively go down through this
* graph in order to find leaf nodes: the views that we flag as needing
* an update.
*
* B) Bottom Up phase
* When a view is flagged as needing an update, we recursively go back up
* in order to build the new value that it needs. The reason why we need
* this two-phases process is to deal with composite props such as
* transform which can receive values from multiple parents.
*/
export function flushValue(rootNode: AnimatedNode): void {
const leaves = new Set<{update: () => void, ...}>();
function findAnimatedStyles(node: AnimatedNode) {
// $FlowFixMe[prop-missing]
if (typeof node.update === 'function') {
leaves.add((node: any));
} else {
node.__getChildren().forEach(findAnimatedStyles);
}
}
findAnimatedStyles(rootNode);
leaves.forEach(leaf => leaf.update());
}
/**
* Some operations are executed only on batch end, which is _mostly_ scheduled when
* Animated component props change. For some of the changes which require immediate execution
* (e.g. setValue), we create a separate batch in case none is scheduled.
*/
function _executeAsAnimatedBatch(id: string, operation: () => void) {
NativeAnimatedAPI.setWaitingForIdentifier(id);
operation();
NativeAnimatedAPI.unsetWaitingForIdentifier(id);
}
/**
* Standard value for driving animations. One `Animated.Value` can drive
* multiple properties in a synchronized fashion, but can only be driven by one
* mechanism at a time. Using a new mechanism (e.g. starting a new animation,
* or calling `setValue`) will stop any previous ones.
*
* See https://reactnative.dev/docs/animatedvalue
*/
export default class AnimatedValue extends AnimatedWithChildren {
_listenerCount: number;
_updateSubscription: ?EventSubscription;
_value: number;
_startingValue: number;
_offset: number;
_animation: ?Animation;
_tracking: ?AnimatedTracking;
constructor(value: number, config?: ?AnimatedValueConfig) {
super(config);
if (typeof value !== 'number') {
throw new Error('AnimatedValue: Attempting to set value to undefined');
}
this._listenerCount = 0;
this._updateSubscription = null;
this._startingValue = this._value = value;
this._offset = 0;
this._animation = null;
if (config && config.useNativeDriver) {
this.__makeNative();
}
}
__detach() {
if (this.__isNative) {
NativeAnimatedAPI.getValue(this.__getNativeTag(), value => {
this._value = value - this._offset;
});
}
this.stopAnimation();
super.__detach();
}
__getValue(): number {
return this._value + this._offset;
}
__makeNative(platformConfig: ?PlatformConfig): void {
super.__makeNative(platformConfig);
if (this._listenerCount > 0) {
this.__ensureUpdateSubscriptionExists();
}
}
addListener(callback: (value: any) => mixed): string {
const id = super.addListener(callback);
this._listenerCount++;
if (this.__isNative) {
this.__ensureUpdateSubscriptionExists();
}
return id;
}
removeListener(id: string): void {
super.removeListener(id);
this._listenerCount--;
if (this.__isNative && this._listenerCount === 0) {
this._updateSubscription?.remove();
}
}
removeAllListeners(): void {
super.removeAllListeners();
this._listenerCount = 0;
if (this.__isNative) {
this._updateSubscription?.remove();
}
}
__ensureUpdateSubscriptionExists(): void {
if (this._updateSubscription != null) {
return;
}
const nativeTag = this.__getNativeTag();
NativeAnimatedAPI.startListeningToAnimatedNodeValue(nativeTag);
const subscription: EventSubscription =
NativeAnimatedHelper.nativeEventEmitter.addListener(
'onAnimatedValueUpdate',
data => {
if (data.tag === nativeTag) {
this.__onAnimatedValueUpdateReceived(data.value, data.offset);
}
},
);
this._updateSubscription = {
remove: () => {
// Only this function assigns to `this.#updateSubscription`.
if (this._updateSubscription == null) {
return;
}
this._updateSubscription = null;
subscription.remove();
NativeAnimatedAPI.stopListeningToAnimatedNodeValue(nativeTag);
},
};
}
/**
* Directly set the value. This will stop any animations running on the value
* and update all the bound properties.
*
* See https://reactnative.dev/docs/animatedvalue#setvalue
*/
setValue(value: number): void {
if (this._animation) {
this._animation.stop();
this._animation = null;
}
this._updateValue(
value,
!this.__isNative /* don't perform a flush for natively driven values */,
);
if (this.__isNative) {
_executeAsAnimatedBatch(this.__getNativeTag().toString(), () =>
NativeAnimatedAPI.setAnimatedNodeValue(this.__getNativeTag(), value),
);
}
}
/**
* Sets an offset that is applied on top of whatever value is set, whether via
* `setValue`, an animation, or `Animated.event`. Useful for compensating
* things like the start of a pan gesture.
*
* See https://reactnative.dev/docs/animatedvalue#setoffset
*/
setOffset(offset: number): void {
this._offset = offset;
if (this.__isNative) {
NativeAnimatedAPI.setAnimatedNodeOffset(this.__getNativeTag(), offset);
}
}
/**
* Merges the offset value into the base value and resets the offset to zero.
* The final output of the value is unchanged.
*
* See https://reactnative.dev/docs/animatedvalue#flattenoffset
*/
flattenOffset(): void {
this._value += this._offset;
this._offset = 0;
if (this.__isNative) {
NativeAnimatedAPI.flattenAnimatedNodeOffset(this.__getNativeTag());
}
}
/**
* Sets the offset value to the base value, and resets the base value to zero.
* The final output of the value is unchanged.
*
* See https://reactnative.dev/docs/animatedvalue#extractoffset
*/
extractOffset(): void {
this._offset += this._value;
this._value = 0;
if (this.__isNative) {
_executeAsAnimatedBatch(this.__getNativeTag().toString(), () =>
NativeAnimatedAPI.extractAnimatedNodeOffset(this.__getNativeTag()),
);
}
}
/**
* Stops any running animation or tracking. `callback` is invoked with the
* final value after stopping the animation, which is useful for updating
* state to match the animation position with layout.
*
* See https://reactnative.dev/docs/animatedvalue#stopanimation
*/
stopAnimation(callback?: ?(value: number) => void): void {
this.stopTracking();
this._animation && this._animation.stop();
this._animation = null;
if (callback) {
if (this.__isNative) {
NativeAnimatedAPI.getValue(this.__getNativeTag(), callback);
} else {
callback(this.__getValue());
}
}
}
/**
* Stops any animation and resets the value to its original.
*
* See https://reactnative.dev/docs/animatedvalue#resetanimation
*/
resetAnimation(callback?: ?(value: number) => void): void {
this.stopAnimation(callback);
this._value = this._startingValue;
if (this.__isNative) {
NativeAnimatedAPI.setAnimatedNodeValue(
this.__getNativeTag(),
this._startingValue,
);
}
}
__onAnimatedValueUpdateReceived(value: number, offset?: number): void {
this._updateValue(value, false /*flush*/);
if (offset != null) {
this._offset = offset;
}
}
/**
* Interpolates the value before updating the property, e.g. mapping 0-1 to
* 0-10.
*/
interpolate<OutputT: number | string>(
config: InterpolationConfigType<OutputT>,
): AnimatedInterpolation<OutputT> {
return new AnimatedInterpolation(this, config);
}
/**
* Typically only used internally, but could be used by a custom Animation
* class.
*
* See https://reactnative.dev/docs/animatedvalue#animate
*/
animate(animation: Animation, callback: ?EndCallback): void {
const previousAnimation = this._animation;
this._animation && this._animation.stop();
this._animation = animation;
animation.start(
this._value,
value => {
// Natively driven animations will never call into that callback, therefore we can always
// pass flush = true to allow the updated value to propagate to native with setNativeProps
this._updateValue(value, true /* flush */);
},
result => {
this._animation = null;
callback && callback(result);
},
previousAnimation,
this,
);
}
/**
* Typically only used internally.
*/
stopTracking(): void {
this._tracking && this._tracking.__detach();
this._tracking = null;
}
/**
* Typically only used internally.
*/
track(tracking: AnimatedTracking): void {
this.stopTracking();
this._tracking = tracking;
// Make sure that the tracking animation starts executing
this._tracking && this._tracking.update();
}
_updateValue(value: number, flush: boolean): void {
if (value === undefined) {
throw new Error('AnimatedValue: Attempting to set value to undefined');
}
this._value = value;
if (flush) {
flushValue(this);
}
this.__callListeners(this.__getValue());
}
__getNativeConfig(): Object {
return {
type: 'value',
value: this._value,
offset: this._offset,
debugID: this.__getDebugID(),
};
}
}

View File

@@ -0,0 +1,240 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import type {AnimatedNodeConfig} from './AnimatedNode';
import AnimatedValue from './AnimatedValue';
import AnimatedWithChildren from './AnimatedWithChildren';
import invariant from 'invariant';
export type AnimatedValueXYConfig = $ReadOnly<{
...AnimatedNodeConfig,
useNativeDriver: boolean,
}>;
type ValueXYListenerCallback = (value: {x: number, y: number, ...}) => mixed;
let _uniqueId = 1;
/**
* 2D Value for driving 2D animations, such as pan gestures. Almost identical
* API to normal `Animated.Value`, but multiplexed.
*
* See https://reactnative.dev/docs/animatedvaluexy
*/
export default class AnimatedValueXY extends AnimatedWithChildren {
x: AnimatedValue;
y: AnimatedValue;
_listeners: {
[key: string]: {
x: string,
y: string,
...
},
...
};
constructor(
valueIn?: ?{
+x: number | AnimatedValue,
+y: number | AnimatedValue,
...
},
config?: ?AnimatedValueXYConfig,
) {
super(config);
const value: any = valueIn || {x: 0, y: 0}; // @flowfixme: shouldn't need `: any`
if (typeof value.x === 'number' && typeof value.y === 'number') {
this.x = new AnimatedValue(value.x);
this.y = new AnimatedValue(value.y);
} else {
invariant(
value.x instanceof AnimatedValue && value.y instanceof AnimatedValue,
'AnimatedValueXY must be initialized with an object of numbers or ' +
'AnimatedValues.',
);
this.x = value.x;
this.y = value.y;
}
this._listeners = {};
if (config && config.useNativeDriver) {
this.__makeNative();
}
}
/**
* Directly set the value. This will stop any animations running on the value
* and update all the bound properties.
*
* See https://reactnative.dev/docs/animatedvaluexy#setvalue
*/
setValue(value: {x: number, y: number, ...}) {
this.x.setValue(value.x);
this.y.setValue(value.y);
}
/**
* Sets an offset that is applied on top of whatever value is set, whether
* via `setValue`, an animation, or `Animated.event`. Useful for compensating
* things like the start of a pan gesture.
*
* See https://reactnative.dev/docs/animatedvaluexy#setoffset
*/
setOffset(offset: {x: number, y: number, ...}) {
this.x.setOffset(offset.x);
this.y.setOffset(offset.y);
}
/**
* Merges the offset value into the base value and resets the offset to zero.
* The final output of the value is unchanged.
*
* See https://reactnative.dev/docs/animatedvaluexy#flattenoffset
*/
flattenOffset(): void {
this.x.flattenOffset();
this.y.flattenOffset();
}
/**
* Sets the offset value to the base value, and resets the base value to
* zero. The final output of the value is unchanged.
*
* See https://reactnative.dev/docs/animatedvaluexy#extractoffset
*/
extractOffset(): void {
this.x.extractOffset();
this.y.extractOffset();
}
__getValue(): {
x: number,
y: number,
...
} {
return {
x: this.x.__getValue(),
y: this.y.__getValue(),
};
}
/**
* Stops any animation and resets the value to its original.
*
* See https://reactnative.dev/docs/animatedvaluexy#resetanimation
*/
resetAnimation(
callback?: (value: {x: number, y: number, ...}) => void,
): void {
this.x.resetAnimation();
this.y.resetAnimation();
callback && callback(this.__getValue());
}
/**
* Stops any running animation or tracking. `callback` is invoked with the
* final value after stopping the animation, which is useful for updating
* state to match the animation position with layout.
*
* See https://reactnative.dev/docs/animatedvaluexy#stopanimation
*/
stopAnimation(callback?: (value: {x: number, y: number, ...}) => void): void {
this.x.stopAnimation();
this.y.stopAnimation();
callback && callback(this.__getValue());
}
/**
* Adds an asynchronous listener to the value so you can observe updates from
* animations. This is useful because there is no way to synchronously read
* the value because it might be driven natively.
*
* Returns a string that serves as an identifier for the listener.
*
* See https://reactnative.dev/docs/animatedvaluexy#addlistener
*/
addListener(callback: ValueXYListenerCallback): string {
const id = String(_uniqueId++);
const jointCallback = ({value: number}: any) => {
callback(this.__getValue());
};
this._listeners[id] = {
x: this.x.addListener(jointCallback),
y: this.y.addListener(jointCallback),
};
return id;
}
/**
* Unregister a listener. The `id` param shall match the identifier
* previously returned by `addListener()`.
*
* See https://reactnative.dev/docs/animatedvaluexy#removelistener
*/
removeListener(id: string): void {
this.x.removeListener(this._listeners[id].x);
this.y.removeListener(this._listeners[id].y);
delete this._listeners[id];
}
/**
* Remove all registered listeners.
*
* See https://reactnative.dev/docs/animatedvaluexy#removealllisteners
*/
removeAllListeners(): void {
this.x.removeAllListeners();
this.y.removeAllListeners();
this._listeners = {};
}
/**
* Converts `{x, y}` into `{left, top}` for use in style.
*
* See https://reactnative.dev/docs/animatedvaluexy#getlayout
*/
getLayout(): {[key: string]: AnimatedValue, ...} {
return {
left: this.x,
top: this.y,
};
}
/**
* Converts `{x, y}` into a useable translation transform.
*
* See https://reactnative.dev/docs/animatedvaluexy#gettranslatetransform
*/
getTranslateTransform(): Array<
{translateX: AnimatedValue} | {translateY: AnimatedValue},
> {
return [{translateX: this.x}, {translateY: this.y}];
}
__attach(): void {
this.x.__addChild(this);
this.y.__addChild(this);
super.__attach();
}
__detach(): void {
this.x.__removeChild(this);
this.y.__removeChild(this);
super.__detach();
}
__makeNative(platformConfig: ?PlatformConfig) {
this.x.__makeNative(platformConfig);
this.y.__makeNative(platformConfig);
super.__makeNative(platformConfig);
}
}

View File

@@ -0,0 +1,85 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
'use strict';
import type {PlatformConfig} from '../AnimatedPlatformConfig';
import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper';
import AnimatedNode from './AnimatedNode';
const {connectAnimatedNodes, disconnectAnimatedNodes} =
NativeAnimatedHelper.API;
export default class AnimatedWithChildren extends AnimatedNode {
_children: Array<AnimatedNode> = [];
__makeNative(platformConfig: ?PlatformConfig) {
if (!this.__isNative) {
this.__isNative = true;
const children = this._children;
let length = children.length;
if (length > 0) {
for (let ii = 0; ii < length; ii++) {
const child = children[ii];
child.__makeNative(platformConfig);
connectAnimatedNodes(this.__getNativeTag(), child.__getNativeTag());
}
}
}
super.__makeNative(platformConfig);
}
__addChild(child: AnimatedNode): void {
if (this._children.length === 0) {
this.__attach();
}
this._children.push(child);
if (this.__isNative) {
// Only accept "native" animated nodes as children
child.__makeNative(this.__getPlatformConfig());
connectAnimatedNodes(this.__getNativeTag(), child.__getNativeTag());
}
}
__removeChild(child: AnimatedNode): void {
const index = this._children.indexOf(child);
if (index === -1) {
console.warn("Trying to remove a child that doesn't exist");
return;
}
if (this.__isNative && child.__isNative) {
disconnectAnimatedNodes(this.__getNativeTag(), child.__getNativeTag());
}
this._children.splice(index, 1);
if (this._children.length === 0) {
this.__detach();
}
}
__getChildren(): $ReadOnlyArray<AnimatedNode> {
return this._children;
}
__callListeners(value: number): void {
super.__callListeners(value);
if (!this.__isNative) {
const children = this._children;
for (let ii = 0, length = children.length; ii < length; ii++) {
const child = children[ii];
// $FlowFixMe[method-unbinding] added when improving typing for this parameters
if (child.__getValue) {
child.__callListeners(child.__getValue());
}
}
}
}
}