animated
Version:
Declarative Animations Library for React and React Native
514 lines (470 loc) • 16 kB
JavaScript
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
;
var invariant = require('invariant');
var Animated = require('./Animated');
var AnimatedValue = require('./AnimatedValue');
var AnimatedValueXY = require('./AnimatedValueXY');
var AnimatedAddition = require('./AnimatedAddition');
var AnimatedMultiplication = require('./AnimatedMultiplication');
var AnimatedModulo = require('./AnimatedModulo');
var AnimatedTemplate = require('./AnimatedTemplate');
var AnimatedTracking = require('./AnimatedTracking');
var isAnimated = require('./isAnimated');
var Animation = require('./Animation');
var TimingAnimation = require('./TimingAnimation');
var DecayAnimation = require('./DecayAnimation');
var SpringAnimation = require('./SpringAnimation');
import type { InterpolationConfigType } from './Interpolation';
import type { AnimationConfig, EndResult, EndCallback } from './Animation';
type TimingAnimationConfig = AnimationConfig & {
toValue: number | AnimatedValue | {x: number, y: number} | AnimatedValueXY;
easing?: (value: number) => number;
duration?: number;
delay?: number;
};
type DecayAnimationConfig = AnimationConfig & {
velocity: number | {x: number, y: number};
deceleration?: number;
};
type SpringAnimationConfig = AnimationConfig & {
toValue: number | AnimatedValue | {x: number, y: number} | AnimatedValueXY;
overshootClamping?: bool;
restDisplacementThreshold?: number;
restSpeedThreshold?: number;
velocity?: number | {x: number, y: number};
bounciness?: number;
speed?: number;
tension?: number;
friction?: number;
};
type CompositeAnimation = {
start: (callback?: ?EndCallback) => void;
stop: () => void;
};
var maybeVectorAnim = function(
value: AnimatedValue | AnimatedValueXY,
config: Object,
anim: (value: AnimatedValue, config: Object) => CompositeAnimation
): ?CompositeAnimation {
if (value instanceof AnimatedValueXY) {
var configX = {...config};
var configY = {...config};
for (var key in config) {
var {x, y} = config[key];
if (x !== undefined && y !== undefined) {
configX[key] = x;
configY[key] = y;
}
}
var aX = anim((value: AnimatedValueXY).x, configX);
var aY = anim((value: 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 parallel([aX, aY], {stopTogether: false});
}
return null;
};
var spring = function(
value: AnimatedValue | AnimatedValueXY,
config: SpringAnimationConfig,
): CompositeAnimation {
return maybeVectorAnim(value, config, spring) || {
start: function(callback?: ?EndCallback): void {
var singleValue: any = value;
var singleConfig: any = config;
singleValue.stopTracking();
if (config.toValue instanceof Animated) {
singleValue.track(new AnimatedTracking(
singleValue,
config.toValue,
SpringAnimation,
singleConfig,
callback
));
} else {
singleValue.animate(new SpringAnimation(singleConfig), callback);
}
},
stop: function(): void {
value.stopAnimation();
},
};
};
var timing = function(
value: AnimatedValue | AnimatedValueXY,
config: TimingAnimationConfig,
): CompositeAnimation {
return maybeVectorAnim(value, config, timing) || {
start: function(callback?: ?EndCallback): void {
var singleValue: any = value;
var singleConfig: any = config;
singleValue.stopTracking();
if (config.toValue instanceof Animated) {
singleValue.track(new AnimatedTracking(
singleValue,
config.toValue,
TimingAnimation,
singleConfig,
callback
));
} else {
singleValue.animate(new TimingAnimation(singleConfig), callback);
}
},
stop: function(): void {
value.stopAnimation();
},
};
};
var decay = function(
value: AnimatedValue | AnimatedValueXY,
config: DecayAnimationConfig,
): CompositeAnimation {
return maybeVectorAnim(value, config, decay) || {
start: function(callback?: ?EndCallback): void {
var singleValue: any = value;
var singleConfig: any = config;
singleValue.stopTracking();
singleValue.animate(new DecayAnimation(singleConfig), callback);
},
stop: function(): void {
value.stopAnimation();
},
};
};
var sequence = function(
animations: Array<CompositeAnimation>,
): CompositeAnimation {
var current = 0;
return {
start: function(callback?: ?EndCallback) {
var onComplete = function(result) {
if (!result.finished) {
callback && callback(result);
return;
}
current++;
if (current === animations.length) {
callback && callback(result);
return;
}
animations[current].start(onComplete);
};
if (animations.length === 0) {
callback && callback({finished: true});
} else {
animations[current].start(onComplete);
}
},
stop: function() {
if (current < animations.length) {
animations[current].stop();
}
}
};
};
type ParallelConfig = {
stopTogether?: bool; // If one is stopped, stop all. default: true
}
var parallel = function(
animations: Array<CompositeAnimation>,
config?: ?ParallelConfig,
): CompositeAnimation {
var doneCount = 0;
// Make sure we only call stop() at most once for each animation
var hasEnded = {};
var stopTogether = !(config && config.stopTogether === false);
var result = {
start: function(callback?: ?EndCallback) {
if (doneCount === animations.length) {
callback && callback({finished: true});
return;
}
animations.forEach((animation, idx) => {
var cb = function(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);
}
});
},
stop: function(): void {
animations.forEach((animation, idx) => {
!hasEnded[idx] && animation.stop();
hasEnded[idx] = true;
});
}
};
return result;
};
var delay = function(time: number): CompositeAnimation {
// Would be nice to make a specialized implementation
return timing(new AnimatedValue(0), {toValue: 0, delay: time, duration: 0});
};
var stagger = function(
time: number,
animations: Array<CompositeAnimation>,
): CompositeAnimation {
return parallel(animations.map((animation, i) => {
return sequence([
delay(time * i),
animation,
]);
}));
};
type Mapping = {[key: string]: Mapping} | AnimatedValue;
type EventConfig = {listener?: ?Function};
var event = function(
argMapping: Array<?Mapping>,
config?: ?EventConfig,
): () => void {
return function(...args): void {
var traverse = function(recMapping, recEvt, key) {
if (typeof recEvt === 'number') {
invariant(
recMapping instanceof AnimatedValue,
'Bad mapping of type ' + typeof recMapping + ' for key ' + key +
', event value must map to AnimatedValue'
);
recMapping.setValue(recEvt);
return;
}
invariant(
typeof recMapping === 'object',
'Bad mapping of type ' + typeof recMapping + ' for key ' + key
);
invariant(
typeof recEvt === 'object',
'Bad event of type ' + typeof recEvt + ' for key ' + key
);
for (var key in recMapping) {
traverse(recMapping[key], recEvt[key], key);
}
};
argMapping.forEach((mapping, idx) => {
traverse(mapping, args[idx], 'arg' + idx);
});
if (config && config.listener) {
config.listener.apply(null, args);
}
};
};
/**
* Animations are an important part of modern UX, and the `Animated`
* library is designed to make them fluid, powerful, and easy to build and
* maintain.
*
* The simplest workflow is to create an `Animated.Value`, hook it up to one or
* more style attributes of an animated component, and then drive updates either
* via animations, such as `Animated.timing`, or by hooking into gestures like
* panning or scrolling via `Animated.event`. `Animated.Value` can also bind to
* props other than style, and can be interpolated as well. Here is a basic
* example of a container view that will fade in when it's mounted:
*
*```javascript
* class FadeInView extends React.Component {
* constructor(props) {
* super(props);
* this.state = {
* fadeAnim: new Animated.Value(0), // init opacity 0
* };
* }
* componentDidMount() {
* Animated.timing( // Uses easing functions
* this.state.fadeAnim, // The value to drive
* {toValue: 1}, // Configuration
* ).start(); // Don't forget start!
* }
* render() {
* return (
* <Animated.View // Special animatable View
* style={{opacity: this.state.fadeAnim}}> // Binds
* {this.props.children}
* </Animated.View>
* );
* }
* }
*```
*
* Note that only animatable components can be animated. `View`, `Text`, and
* `Image` are already provided, and you can create custom ones with
* `createAnimatedComponent`. These special components do the magic of binding
* the animated values to the properties, and do targeted native updates to
* avoid the cost of the react render and reconciliation process on every frame.
* They also handle cleanup on unmount so they are safe by default.
*
* Animations are heavily configurable. Custom and pre-defined easing
* functions, delays, durations, decay factors, spring constants, and more can
* all be tweaked depending on the type of animation.
*
* A single `Animated.Value` can drive any number of properties, and each
* property can be run through an interpolation first. An interpolation maps
* input ranges to output ranges, typically using a linear interpolation but
* also supports easing functions. By default, it will extrapolate the curve
* beyond the ranges given, but you can also have it clamp the output value.
*
* For example, you may want to think about your `Animated.Value` as going from
* 0 to 1, but animate the position from 150px to 0px and the opacity from 0 to
* 1. This can easily be done by modifying `style` in the example above like so:
*
*```javascript
* style={{
* opacity: this.state.fadeAnim, // Binds directly
* transform: [{
* translateY: this.state.fadeAnim.interpolate({
* inputRange: [0, 1],
* outputRange: [150, 0] // 0 : 150, 0.5 : 75, 1 : 0
* }),
* }],
* }}>
*```
*
* Animations can also be combined in complex ways using composition functions
* such as `sequence` and `parallel`, and can also be chained together simply
* by setting the `toValue` of one animation to be another `Animated.Value`.
*
* `Animated.ValueXY` is handy for 2D animations, like panning, and there are
* other helpful additions like `setOffset` and `getLayout` to aid with typical
* interaction patterns, like drag-and-drop.
*
* You can see more example usage in `AnimationExample.js`, the Gratuitous
* Animation App, and [Animations documentation guide](docs/animations.html).
*
* Note that `Animated` is designed to be fully serializable so that animations
* can be run in a high performance way, independent of the normal JavaScript
* event loop. This does influence the API, so keep that in mind when it seems a
* little trickier to do something compared to a fully synchronous system.
* Checkout `Animated.Value.addListener` as a way to work around some of these
* limitations, but use it sparingly since it might have performance
* implications in the future.
*/
module.exports = {
/**
* Standard value class for driving animations. Typically initialized with
* `new Animated.Value(0);`
*/
Value: AnimatedValue,
/**
* 2D value class for driving 2D animations, such as pan gestures.
*/
ValueXY: AnimatedValueXY,
/**
* Animates a value from an initial velocity to zero based on a decay
* coefficient.
*/
decay,
/**
* 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,
/**
* Spring animation based on Rebound and Origami. Tracks velocity state to
* create fluid motions as the `toValue` updates, and can be chained together.
*/
spring,
/**
* Creates a new Animated value composed from two Animated values added
* together.
*/
add: function add(a: Animated, b: Animated): AnimatedAddition {
return new AnimatedAddition(a, b);
},
/**
* Creates a new Animated value composed from two Animated values multiplied
* together.
*/
multiply: function multiply(a: Animated, b: Animated): AnimatedMultiplication {
return new AnimatedMultiplication(a, b);
},
/**
* Creates a new Animated value that is the (non-negative) modulo of the
* provided Animated value
*/
modulo: function modulo(a: Animated, modulus: number): AnimatedModulo {
return new AnimatedModulo(a, modulus);
},
/**
* Creates a new Animated value that is the specified string, with each
* substitution expression being separately animated and interpolated.
*/
template: function template(strings, ...values) {
return new AnimatedTemplate(strings, values);
},
/**
* Starts an animation after the given delay.
*/
delay,
/**
* 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,
/**
* 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,
/**
* Array of animations may run in parallel (overlap), but are started in
* sequence with successive delays. Nice for doing trailing effects.
*/
stagger,
/**
* 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,
/**
* Existential test to figure out if an object is an instance of the Animated
* class or not.
*/
isAnimated,
/**
* Make any React component Animatable. Used to create `Animated.View`, etc.
*/
createAnimatedComponent: require('./createAnimatedComponent'),
inject: {
ApplyAnimatedValues: require('./injectable/ApplyAnimatedValues').inject,
InteractionManager: require('./injectable/InteractionManager').inject,
FlattenStyle: require('./injectable/FlattenStyle').inject,
RequestAnimationFrame: require('./injectable/RequestAnimationFrame').inject,
CancelAnimationFrame: require('./injectable/CancelAnimationFrame').inject,
},
__PropsOnlyForTests: require('./AnimatedProps'),
};