react-native
Version:
A framework for building native apps using React
757 lines (693 loc) • 22 kB
JavaScript
/**
* 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 {EventConfig, Mapping} from './AnimatedEvent';
import type {
AnimationConfig,
EndCallback,
EndResult,
} from './animations/Animation';
import type {DecayAnimationConfig} from './animations/DecayAnimation';
import type {SpringAnimationConfig} from './animations/SpringAnimation';
import type {TimingAnimationConfig} from './animations/TimingAnimation';
import NativeAnimatedHelper from '../../src/private/animated/NativeAnimatedHelper';
import {AnimatedEvent, attachNativeEventImpl} from './AnimatedEvent';
import DecayAnimation from './animations/DecayAnimation';
import SpringAnimation from './animations/SpringAnimation';
import TimingAnimation from './animations/TimingAnimation';
import createAnimatedComponent from './createAnimatedComponent';
import AnimatedAddition from './nodes/AnimatedAddition';
import AnimatedColor from './nodes/AnimatedColor';
import AnimatedDiffClamp from './nodes/AnimatedDiffClamp';
import AnimatedDivision from './nodes/AnimatedDivision';
import AnimatedInterpolation from './nodes/AnimatedInterpolation';
import AnimatedModulo from './nodes/AnimatedModulo';
import AnimatedMultiplication from './nodes/AnimatedMultiplication';
import AnimatedNode from './nodes/AnimatedNode';
import AnimatedSubtraction from './nodes/AnimatedSubtraction';
import AnimatedTracking from './nodes/AnimatedTracking';
import AnimatedValue from './nodes/AnimatedValue';
import AnimatedValueXY from './nodes/AnimatedValueXY';
export type CompositeAnimation = {
/**
* Animations are started by calling start() on your animation.
* start() takes a completion callback that will be called when the
* animation is done or when the animation is done because stop() was
* called on it before it could finish.
*
* @param callback - Optional function that will be called
* after the animation finished running normally or when the animation
* is done because stop() was called on it before it could finish
*
* @example
* Animated.timing({}).start(({ finished }) => {
* // completion callback
* });
*/
start: (callback?: ?EndCallback, isLooping?: boolean) => void,
/**
* Stops any running animation.
*/
stop: () => void,
/**
* Stops any running animation and resets the value to its original.
*/
reset: () => void,
_startNativeLoop: (iterations?: number) => void,
_isUsingNativeDriver: () => boolean,
...
};
const addImpl = function (
a: AnimatedNode | number,
b: AnimatedNode | number,
): AnimatedAddition {
return new AnimatedAddition(a, b);
};
const subtractImpl = function (
a: AnimatedNode | number,
b: AnimatedNode | number,
): AnimatedSubtraction {
return new AnimatedSubtraction(a, b);
};
const divideImpl = function (
a: AnimatedNode | number,
b: AnimatedNode | number,
): AnimatedDivision {
return new AnimatedDivision(a, b);
};
const multiplyImpl = function (
a: AnimatedNode | number,
b: AnimatedNode | number,
): AnimatedMultiplication {
return new AnimatedMultiplication(a, b);
};
const moduloImpl = function (a: AnimatedNode, modulus: number): AnimatedModulo {
return new AnimatedModulo(a, modulus);
};
const diffClampImpl = function (
a: AnimatedNode,
min: number,
max: number,
): AnimatedDiffClamp {
return new AnimatedDiffClamp(a, min, max);
};
const _combineCallbacks = function (
callback: ?EndCallback,
config: Readonly<{...AnimationConfig, ...}>,
) {
if (callback && config.onComplete) {
return (...args: Array<EndResult>) => {
config.onComplete && config.onComplete(...args);
/* $FlowFixMe[constant-condition] Error discovered during Constant
* Condition roll out. See https://fburl.com/workplace/1v97vimq. */
callback && callback(...args);
};
} else {
return callback || config.onComplete;
}
};
const maybeVectorAnim = function (
value: AnimatedValue | AnimatedValueXY | AnimatedColor,
config: Object,
anim: (value: AnimatedValue, config: Object) => CompositeAnimation,
): ?CompositeAnimation {
if (value instanceof AnimatedValueXY) {
const configX = {...config};
const configY = {...config};
for (const key in config) {
const {x, y} = config[key];
if (x !== undefined && y !== undefined) {
configX[key] = x;
configY[key] = y;
}
}
const aX = anim((value as AnimatedValueXY).x, configX);
const aY = anim((value as AnimatedValueXY).y, configY);
// We use `stopTogether: false` here because otherwise tracking will break
// because the second animation will get stopped before it can update.
return parallelImpl([aX, aY], {stopTogether: false});
} else if (value instanceof AnimatedColor) {
const configR = {...config};
const configG = {...config};
const configB = {...config};
const configA = {...config};
for (const key in config) {
const {r, g, b, a} = config[key];
if (
r !== undefined &&
g !== undefined &&
b !== undefined &&
a !== undefined
) {
configR[key] = r;
configG[key] = g;
configB[key] = b;
configA[key] = a;
}
}
const aR = anim((value as AnimatedColor).r, configR);
const aG = anim((value as AnimatedColor).g, configG);
const aB = anim((value as AnimatedColor).b, configB);
const aA = anim((value as AnimatedColor).a, configA);
// We use `stopTogether: false` here because otherwise tracking will break
// because the second animation will get stopped before it can update.
return parallelImpl([aR, aG, aB, aA], {stopTogether: false});
}
return null;
};
const springImpl = function (
value: AnimatedValue | AnimatedValueXY | AnimatedColor,
config: SpringAnimationConfig,
): CompositeAnimation {
const start = function (
animatedValue: AnimatedValue | AnimatedValueXY | AnimatedColor,
configuration: SpringAnimationConfig,
callback?: ?EndCallback,
): void {
callback = _combineCallbacks(callback, configuration);
const singleValue: any = animatedValue;
const singleConfig: any = configuration;
singleValue.stopTracking();
if (configuration.toValue instanceof AnimatedNode) {
singleValue.track(
new AnimatedTracking(
singleValue,
configuration.toValue,
SpringAnimation,
singleConfig,
callback,
),
);
} else {
singleValue.animate(new SpringAnimation(singleConfig), callback);
}
};
return (
maybeVectorAnim(value, config, springImpl) || {
start: function (callback?: ?EndCallback): void {
start(value, config, callback);
},
stop: function (): void {
value.stopAnimation();
},
reset: function (): void {
value.resetAnimation();
},
_startNativeLoop: function (iterations?: number): void {
const singleConfig = {...config, iterations};
start(value, singleConfig);
},
_isUsingNativeDriver: function (): boolean {
return (
NativeAnimatedHelper.isNativeDriverForced() ||
config.useNativeDriver ||
false
);
},
}
);
};
const timingImpl = function (
value: AnimatedValue | AnimatedValueXY | AnimatedColor,
config: TimingAnimationConfig,
): CompositeAnimation {
const start = function (
animatedValue: AnimatedValue | AnimatedValueXY | AnimatedColor,
configuration: TimingAnimationConfig,
callback?: ?EndCallback,
): void {
callback = _combineCallbacks(callback, configuration);
const singleValue: any = animatedValue;
const singleConfig: any = configuration;
singleValue.stopTracking();
if (configuration.toValue instanceof AnimatedNode) {
singleValue.track(
new AnimatedTracking(
singleValue,
configuration.toValue,
TimingAnimation,
singleConfig,
callback,
),
);
} else {
singleValue.animate(new TimingAnimation(singleConfig), callback);
}
};
return (
maybeVectorAnim(value, config, timingImpl) || {
start: function (callback?: ?EndCallback, isLooping?: boolean): void {
start(value, {...config, isLooping}, callback);
},
stop: function (): void {
value.stopAnimation();
},
reset: function (): void {
value.resetAnimation();
},
_startNativeLoop: function (iterations?: number): void {
const singleConfig = {...config, iterations};
start(value, singleConfig);
},
_isUsingNativeDriver: function (): boolean {
return (
NativeAnimatedHelper.isNativeDriverForced() ||
config.useNativeDriver ||
false
);
},
}
);
};
const decayImpl = function (
value: AnimatedValue | AnimatedValueXY | AnimatedColor,
config: DecayAnimationConfig,
): CompositeAnimation {
const start = function (
animatedValue: AnimatedValue | AnimatedValueXY | AnimatedColor,
configuration: DecayAnimationConfig,
callback?: ?EndCallback,
): void {
callback = _combineCallbacks(callback, configuration);
const singleValue: any = animatedValue;
const singleConfig: any = configuration;
singleValue.stopTracking();
singleValue.animate(new DecayAnimation(singleConfig), callback);
};
return (
maybeVectorAnim(value, config, decayImpl) || {
start: function (callback?: ?EndCallback): void {
start(value, config, callback);
},
stop: function (): void {
value.stopAnimation();
},
reset: function (): void {
value.resetAnimation();
},
_startNativeLoop: function (iterations?: number): void {
const singleConfig = {...config, iterations};
start(value, singleConfig);
},
_isUsingNativeDriver: function (): boolean {
return (
NativeAnimatedHelper.isNativeDriverForced() ||
config.useNativeDriver ||
false
);
},
}
);
};
const sequenceImpl = function (
animations: Array<CompositeAnimation>,
): CompositeAnimation {
let current = 0;
return {
start: function (callback?: ?EndCallback, isLooping?: boolean) {
const onComplete = function (result: EndResult) {
if (!result.finished) {
callback && callback(result);
return;
}
current++;
if (current === animations.length) {
// if the start is called, even without a reset, it should start from the beginning
current = 0;
callback && callback(result);
return;
}
animations[current].start(onComplete, isLooping);
};
if (animations.length === 0) {
callback && callback({finished: true});
} else {
animations[current].start(onComplete, isLooping);
}
},
stop: function () {
if (current < animations.length) {
animations[current].stop();
}
},
reset: function () {
animations.forEach((animation, idx) => {
if (idx <= current) {
animation.reset();
}
});
current = 0;
},
_startNativeLoop: function () {
throw new Error(
'Loops run using the native driver cannot contain Animated.sequence animations',
);
},
_isUsingNativeDriver: function (): boolean {
return false;
},
};
};
type ParallelConfig = {
// If one is stopped, stop all. default: true
stopTogether?: boolean,
...
};
const parallelImpl = function (
animations: Array<CompositeAnimation>,
config?: ?ParallelConfig,
): CompositeAnimation {
let doneCount = 0;
// Make sure we only call stop() at most once for each animation
const hasEnded: {[number]: boolean} = {};
const stopTogether = !(config && config.stopTogether === false);
const result: CompositeAnimation = {
start: function (callback?: ?EndCallback, isLooping?: boolean) {
if (doneCount === animations.length) {
callback && callback({finished: true});
return;
}
animations.forEach((animation, idx) => {
const cb = function (endResult: EndResult) {
hasEnded[idx] = true;
doneCount++;
if (doneCount === animations.length) {
doneCount = 0;
callback && callback(endResult);
return;
}
if (!endResult.finished && stopTogether) {
result.stop();
}
};
if (!animation) {
cb({finished: true});
} else {
animation.start(cb, isLooping);
}
});
},
stop: function (): void {
animations.forEach((animation, idx) => {
!hasEnded[idx] && animation.stop();
hasEnded[idx] = true;
});
},
reset: function (): void {
animations.forEach((animation, idx) => {
animation.reset();
hasEnded[idx] = false;
doneCount = 0;
});
},
_startNativeLoop: function (): empty {
throw new Error(
'Loops run using the native driver cannot contain Animated.parallel animations',
);
},
_isUsingNativeDriver: function (): boolean {
return false;
},
};
return result;
};
const delayImpl = function (time: number): CompositeAnimation {
// Would be nice to make a specialized implementation
return timingImpl(new AnimatedValue(0), {
toValue: 0,
delay: time,
duration: 0,
useNativeDriver: false,
});
};
const staggerImpl = function (
time: number,
animations: Array<CompositeAnimation>,
): CompositeAnimation {
return parallelImpl(
animations.map((animation, i) => {
return sequenceImpl([delayImpl(time * i), animation]);
}),
);
};
type LoopAnimationConfig = {
iterations: number,
resetBeforeIteration?: boolean,
...
};
const loopImpl = function (
animation: CompositeAnimation,
// $FlowFixMe[incompatible-type]
{iterations = -1, resetBeforeIteration = true}: LoopAnimationConfig = {},
): CompositeAnimation {
let isFinished = false;
let iterationsSoFar = 0;
return {
start: function (callback?: ?EndCallback) {
const restart = function (result: EndResult = {finished: true}): void {
if (
isFinished ||
iterationsSoFar === iterations ||
result.finished === false
) {
callback && callback(result);
} else {
iterationsSoFar++;
resetBeforeIteration && animation.reset();
animation.start(restart, iterations === -1);
}
};
if (!animation || iterations === 0) {
callback && callback({finished: true});
} else {
if (animation._isUsingNativeDriver()) {
animation._startNativeLoop(iterations);
} else {
restart(); // Start looping recursively on the js thread
}
}
},
stop: function (): void {
isFinished = true;
animation.stop();
},
reset: function (): void {
iterationsSoFar = 0;
isFinished = false;
animation.reset();
},
_startNativeLoop: function () {
throw new Error(
'Loops run using the native driver cannot contain Animated.loop animations',
);
},
_isUsingNativeDriver: function (): boolean {
return animation._isUsingNativeDriver();
},
};
};
function forkEventImpl(
event: ?AnimatedEvent | ?Function,
listener: Function,
): AnimatedEvent | Function {
if (!event) {
return listener;
} else if (event instanceof AnimatedEvent) {
event.__addListener(listener);
return event;
} else {
return (...args) => {
typeof event === 'function' && event(...args);
listener(...args);
};
}
}
function unforkEventImpl(
event: ?AnimatedEvent | ?Function,
listener: Function,
): void {
if (event && event instanceof AnimatedEvent) {
event.__removeListener(listener);
}
}
// NOTE: With `useNativeDriver: true` this returns an `AnimatedEvent` instance
// rather than a callable handler. That object is only ever meant to be handed
// straight back to an animated component's event prop, so the declared type
// describes the handler shape both branches are consumed as.
const eventImpl: <T>(
argMapping: ReadonlyArray<?Mapping>,
config: EventConfig<T>,
) => (...args: Array<any>) => void = function (argMapping, config): any {
const animatedEvent = new AnimatedEvent(argMapping, config);
if (animatedEvent.__isNative) {
return animatedEvent;
} else {
return animatedEvent.__getHandler();
}
};
// All types of animated nodes that represent scalar numbers and can be interpolated (etc)
type AnimatedNumeric =
| AnimatedAddition
| AnimatedDiffClamp
| AnimatedDivision
| AnimatedInterpolation<number>
| AnimatedModulo
| AnimatedMultiplication
| AnimatedSubtraction
| AnimatedValue;
export type {AnimatedNumeric as Numeric};
/**
* The `Animated` library is designed to make animations fluid, powerful, and
* easy to build and maintain. `Animated` focuses on declarative relationships
* between inputs and outputs, with configurable transforms in between, and
* simple `start`/`stop` methods to control time-based animation execution.
* If additional transforms are added, be sure to include them in
* AnimatedMock.js as well.
*
* See https://reactnative.dev/docs/animated
*/
export default {
/**
* Standard value class for driving animations. Typically initialized with
* `new Animated.Value(0);`
*
* See https://reactnative.dev/docs/animated#value
*/
Value: AnimatedValue,
/**
* 2D value class for driving 2D animations, such as pan gestures.
*
* See https://reactnative.dev/docs/animatedvaluexy
*/
ValueXY: AnimatedValueXY,
/**
* Value class for driving color animations.
*/
Color: AnimatedColor,
/**
* Exported to use the Interpolation type in flow.
*
* See https://reactnative.dev/docs/animated#interpolation
*/
Interpolation: AnimatedInterpolation,
/**
* Exported for ease of type checking. All animated values derive from this
* class.
*
* See https://reactnative.dev/docs/animated#node
*/
Node: AnimatedNode,
/**
* Animates a value from an initial velocity to zero based on a decay
* coefficient.
*/
decay: decayImpl,
/**
* Animates a value along a timed easing curve. The `Easing` module has tons
* of pre-defined curves, or you can use your own function.
*/
timing: timingImpl,
/**
* Spring animation based on Rebound and Origami. Tracks velocity state to
* create fluid motions as the `toValue` updates, and can be chained together.
*/
spring: springImpl,
/**
* Creates a new Animated value composed from two Animated values added
* together.
*/
add: addImpl,
/**
* Creates a new Animated value composed by subtracting the second Animated
* value from the first Animated value.
*/
subtract: subtractImpl,
/**
* Creates a new Animated value composed by dividing the first Animated
* value by the second Animated value.
*/
divide: divideImpl,
/**
* Creates a new Animated value composed from two Animated values multiplied
* together.
*/
multiply: multiplyImpl,
/**
* Creates a new Animated value that is the (non-negative) modulo of the
* provided Animated value
*/
modulo: moduloImpl,
/**
* Create a new Animated value that is limited between 2 values. It uses the
* difference between the last value so even if the value is far from the bounds
* it will start changing when the value starts getting closer again.
* (`value = clamp(value + diff, min, max)`).
*
* This is useful with scroll events, for example, to show the navbar when
* scrolling up and to hide it when scrolling down.
*/
diffClamp: diffClampImpl,
/**
* Starts an animation after the given delay.
*/
delay: delayImpl,
/**
* Starts an array of animations in order, waiting for each to complete
* before starting the next. If the current running animation is stopped, no
* following animations will be started.
*/
sequence: sequenceImpl,
/**
* Starts an array of animations all at the same time. By default, if one
* of the animations is stopped, they will all be stopped. You can override
* this with the `stopTogether` flag.
*/
parallel: parallelImpl,
/**
* Array of animations may run in parallel (overlap), but are started in
* sequence with successive delays. Nice for doing trailing effects.
*/
stagger: staggerImpl,
/**
* Loops a given animation continuously, so that each time it reaches the end,
* it resets and begins again from the start. Can specify number of times to
* loop using the key 'iterations' in the config. Will loop without blocking
* the UI thread if the child animation is set to 'useNativeDriver'.
*/
loop: loopImpl,
/**
* Takes an array of mappings and extracts values from each arg accordingly,
* then calls `setValue` on the mapped outputs. e.g.
*
*```javascript
* onScroll={Animated.event(
* [{nativeEvent: {contentOffset: {x: this._scrollX}}}]
* {listener}, // Optional async listener
* )
* ...
* onPanResponderMove: Animated.event([
* null, // raw event arg ignored
* {dx: this._panX}, // gestureState arg
* ]),
*```
*/
event: eventImpl,
/**
* Make any React component Animatable. Used to create `Animated.View`, etc.
*/
createAnimatedComponent,
attachNativeEvent: attachNativeEventImpl,
forkEvent: forkEventImpl,
unforkEvent: unforkEventImpl,
/**
* Expose Event class, so it can be used as a type for type checkers.
*/
Event: AnimatedEvent,
};