react-native/Libraries/Animated/Animated.js

1310 lines
34 KiB
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.
*
* @providesModule Animated
* @flow
*/
'use strict';
var Easing = require('Easing');
var Image = require('Image');
var InteractionManager = require('InteractionManager');
var Interpolation = require('Interpolation');
var React = require('React');
var Set = require('Set');
var SpringConfig = require('SpringConfig');
var Text = require('Text');
var View = require('View');
var invariant = require('invariant');
var flattenStyle = require('flattenStyle');
var requestAnimationFrame = require('requestAnimationFrame');
import type InterpolationConfigType from 'Interpolation';
type EndResult = {finished: bool};
type EndCallback = (result: EndResult) => void;
// Note(vjeux): this would be better as an interface but flow doesn't
// support them yet
class Animated {
__attach(): void {}
__detach(): void {}
__getValue(): any {}
__getAnimatedValue(): any { return this.__getValue(); }
__addChild(child: Animated) {}
__removeChild(child: Animated) {}
__getChildren(): Array<Animated> { return []; }
}
// Important note: start() and stop() will only be called at most once.
// Once an animation has been stopped or finished its course, it will
// not be reused.
class Animation {
__active: bool;
__onEnd: ?EndCallback;
start(
fromValue: number,
onUpdate: (value: number) => void,
onEnd: ?EndCallback,
previousAnimation: ?Animation,
): void {}
stop(): void {}
// Helper function for subclasses to make sure onEnd is only called once.
__debouncedOnEnd(result: EndResult) {
var onEnd = this.__onEnd;
this.__onEnd = null;
onEnd && onEnd(result);
}
}
class AnimatedWithChildren extends Animated {
_children: Array<Animated>;
constructor() {
super();
this._children = [];
}
__addChild(child: Animated): void {
if (this._children.length === 0) {
this.__attach();
}
this._children.push(child);
}
__removeChild(child: Animated): void {
var index = this._children.indexOf(child);
if (index === -1) {
console.warn('Trying to remove a child that doesn\'t exist');
return;
}
this._children.splice(index, 1);
if (this._children.length === 0) {
this.__detach();
}
}
__getChildren(): Array<Animated> {
return this._children;
}
}
/**
* 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.
*/
function _flush(rootNode: AnimatedValue): void {
var animatedStyles = new Set();
function findAnimatedStyles(node) {
if (typeof node.update === 'function') {
animatedStyles.add(node);
} else {
node.__getChildren().forEach(findAnimatedStyles);
}
}
findAnimatedStyles(rootNode);
animatedStyles.forEach(animatedStyle => animatedStyle.update());
}
type TimingAnimationConfig = {
toValue: number;
easing?: (value: number) => number;
duration?: number;
delay?: number;
};
var easeInOut = Easing.inOut(Easing.ease);
class TimingAnimation extends Animation {
_startTime: number;
_fromValue: number;
_toValue: number;
_duration: number;
_delay: number;
_easing: (value: number) => number;
_onUpdate: (value: number) => void;
_animationFrame: any;
_timeout: any;
constructor(
config: TimingAnimationConfig,
) {
super();
this._toValue = config.toValue;
this._easing = config.easing || easeInOut;
this._duration = config.duration !== undefined ? config.duration : 500;
this._delay = config.delay || 0;
}
start(
fromValue: number,
onUpdate: (value: number) => void,
onEnd: ?EndCallback,
): void {
this.__active = true;
this._fromValue = fromValue;
this._onUpdate = onUpdate;
this.__onEnd = onEnd;
var start = () => {
if (this._duration === 0) {
this._onUpdate(this._toValue);
this.__debouncedOnEnd({finished: true});
} else {
this._startTime = Date.now();
this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this));
}
};
if (this._delay) {
this._timeout = setTimeout(start, this._delay);
} else {
start();
}
}
onUpdate(): void {
var now = Date.now();
if (now >= this._startTime + this._duration) {
if (this._duration === 0) {
this._onUpdate(this._toValue);
} else {
this._onUpdate(
this._fromValue + this._easing(1) * (this._toValue - this._fromValue)
);
}
this.__debouncedOnEnd({finished: true});
return;
}
this._onUpdate(
this._fromValue +
this._easing((now - this._startTime) / this._duration) *
(this._toValue - this._fromValue)
);
if (this.__active) {
this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this));
}
}
stop(): void {
this.__active = false;
clearTimeout(this._timeout);
window.cancelAnimationFrame(this._animationFrame);
this.__debouncedOnEnd({finished: false});
}
}
type DecayAnimationConfig = {
velocity: number | {x: number, y: number};
deceleration?: number;
};
type DecayAnimationConfigSingle = {
velocity: number;
deceleration?: number;
};
class DecayAnimation extends Animation {
_startTime: number;
_lastValue: number;
_fromValue: number;
_deceleration: number;
_velocity: number;
_onUpdate: (value: number) => void;
_animationFrame: any;
constructor(
config: DecayAnimationConfigSingle,
) {
super();
this._deceleration = config.deceleration || 0.998;
this._velocity = config.velocity;
}
start(
fromValue: number,
onUpdate: (value: number) => void,
onEnd: ?EndCallback,
): void {
this.__active = true;
this._lastValue = fromValue;
this._fromValue = fromValue;
this._onUpdate = onUpdate;
this.__onEnd = onEnd;
this._startTime = Date.now();
this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this));
}
onUpdate(): void {
var now = Date.now();
var value = this._fromValue +
(this._velocity / (1 - this._deceleration)) *
(1 - Math.exp(-(1 - this._deceleration) * (now - this._startTime)));
this._onUpdate(value);
if (Math.abs(this._lastValue - value) < 0.1) {
this.__debouncedOnEnd({finished: true});
return;
}
this._lastValue = value;
if (this.__active) {
this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this));
}
}
stop(): void {
this.__active = false;
window.cancelAnimationFrame(this._animationFrame);
this.__debouncedOnEnd({finished: false});
}
}
type SpringAnimationConfig = {
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 SpringAnimationConfigSingle = {
toValue: number | AnimatedValue;
overshootClamping?: bool;
restDisplacementThreshold?: number;
restSpeedThreshold?: number;
velocity?: number;
bounciness?: number;
speed?: number;
tension?: number;
friction?: number;
};
function withDefault<T>(value: ?T, defaultValue: T): T {
if (value === undefined || value === null) {
return defaultValue;
}
return value;
}
class SpringAnimation extends Animation {
_overshootClamping: bool;
_restDisplacementThreshold: number;
_restSpeedThreshold: number;
_initialVelocity: ?number;
_lastVelocity: number;
_startPosition: number;
_lastPosition: number;
_fromValue: number;
_toValue: any;
_tension: number;
_friction: number;
_lastTime: number;
_onUpdate: (value: number) => void;
_animationFrame: any;
constructor(
config: SpringAnimationConfigSingle,
) {
super();
this._overshootClamping = withDefault(config.overshootClamping, false);
this._restDisplacementThreshold = withDefault(config.restDisplacementThreshold, 0.001);
this._restSpeedThreshold = withDefault(config.restSpeedThreshold, 0.001);
this._initialVelocity = config.velocity;
this._lastVelocity = withDefault(config.velocity, 0);
this._toValue = config.toValue;
var springConfig;
if (config.bounciness !== undefined || config.speed !== undefined) {
invariant(
config.tension === undefined && config.friction === undefined,
'You can only define bounciness/speed or tension/friction but not both',
);
springConfig = SpringConfig.fromBouncinessAndSpeed(
withDefault(config.bounciness, 8),
withDefault(config.speed, 12),
);
} else {
springConfig = SpringConfig.fromOrigamiTensionAndFriction(
withDefault(config.tension, 40),
withDefault(config.friction, 7),
);
}
this._tension = springConfig.tension;
this._friction = springConfig.friction;
}
start(
fromValue: number,
onUpdate: (value: number) => void,
onEnd: ?EndCallback,
previousAnimation: ?Animation,
): void {
this.__active = true;
this._startPosition = fromValue;
this._lastPosition = this._startPosition;
this._onUpdate = onUpdate;
this.__onEnd = onEnd;
this._lastTime = Date.now();
if (previousAnimation instanceof SpringAnimation) {
var internalState = previousAnimation.getInternalState();
this._lastPosition = internalState.lastPosition;
this._lastVelocity = internalState.lastVelocity;
this._lastTime = internalState.lastTime;
}
if (this._initialVelocity !== undefined &&
this._initialVelocity !== null) {
this._lastVelocity = this._initialVelocity;
}
this.onUpdate();
}
getInternalState(): Object {
return {
lastPosition: this._lastPosition,
lastVelocity: this._lastVelocity,
lastTime: this._lastTime,
};
}
onUpdate(): void {
var position = this._lastPosition;
var velocity = this._lastVelocity;
var tempPosition = this._lastPosition;
var tempVelocity = this._lastVelocity;
// If for some reason we lost a lot of frames (e.g. process large payload or
// stopped in the debugger), we only advance by 4 frames worth of
// computation and will continue on the next frame. It's better to have it
// running at faster speed than jumping to the end.
var MAX_STEPS = 64;
var now = Date.now();
if (now > this._lastTime + MAX_STEPS) {
now = this._lastTime + MAX_STEPS;
}
// We are using a fixed time step and a maximum number of iterations.
// The following post provides a lot of thoughts into how to build this
// loop: http://gafferongames.com/game-physics/fix-your-timestep/
var TIMESTEP_MSEC = 1;
var numSteps = Math.floor((now - this._lastTime) / TIMESTEP_MSEC);
for (var i = 0; i < numSteps; ++i) {
// Velocity is based on seconds instead of milliseconds
var step = TIMESTEP_MSEC / 1000;
// This is using RK4. A good blog post to understand how it works:
// http://gafferongames.com/game-physics/integration-basics/
var aVelocity = velocity;
var aAcceleration = this._tension * (this._toValue - tempPosition) - this._friction * tempVelocity;
var tempPosition = position + aVelocity * step / 2;
var tempVelocity = velocity + aAcceleration * step / 2;
var bVelocity = tempVelocity;
var bAcceleration = this._tension * (this._toValue - tempPosition) - this._friction * tempVelocity;
tempPosition = position + bVelocity * step / 2;
tempVelocity = velocity + bAcceleration * step / 2;
var cVelocity = tempVelocity;
var cAcceleration = this._tension * (this._toValue - tempPosition) - this._friction * tempVelocity;
tempPosition = position + cVelocity * step / 2;
tempVelocity = velocity + cAcceleration * step / 2;
var dVelocity = tempVelocity;
var dAcceleration = this._tension * (this._toValue - tempPosition) - this._friction * tempVelocity;
tempPosition = position + cVelocity * step / 2;
tempVelocity = velocity + cAcceleration * step / 2;
var dxdt = (aVelocity + 2 * (bVelocity + cVelocity) + dVelocity) / 6;
var dvdt = (aAcceleration + 2 * (bAcceleration + cAcceleration) + dAcceleration) / 6;
position += dxdt * step;
velocity += dvdt * step;
}
this._lastTime = now;
this._lastPosition = position;
this._lastVelocity = velocity;
this._onUpdate(position);
if (!this.__active) { // a listener might have stopped us in _onUpdate
return;
}
// Conditions for stopping the spring animation
var isOvershooting = false;
if (this._overshootClamping && this._tension !== 0) {
if (this._startPosition < this._toValue) {
isOvershooting = position > this._toValue;
} else {
isOvershooting = position < this._toValue;
}
}
var isVelocity = Math.abs(velocity) <= this._restSpeedThreshold;
var isDisplacement = true;
if (this._tension !== 0) {
isDisplacement = Math.abs(this._toValue - position) <= this._restDisplacementThreshold;
}
if (isOvershooting || (isVelocity && isDisplacement)) {
if (this._tension !== 0) {
// Ensure that we end up with a round value
this._onUpdate(this._toValue);
}
this.__debouncedOnEnd({finished: true});
return;
}
this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this));
}
stop(): void {
this.__active = false;
window.cancelAnimationFrame(this._animationFrame);
this.__debouncedOnEnd({finished: false});
}
}
type ValueListenerCallback = (state: {value: number}) => void;
var _uniqueId = 1;
class AnimatedValue extends AnimatedWithChildren {
_value: number;
_offset: number;
_animation: ?Animation;
_tracking: ?Animated;
_listeners: {[key: string]: ValueListenerCallback};
constructor(value: number) {
super();
this._value = value;
this._offset = 0;
this._animation = null;
this._listeners = {};
}
__detach() {
this.stopAnimation();
}
__getValue(): number {
return this._value + this._offset;
}
setValue(value: number): void {
if (this._animation) {
this._animation.stop();
this._animation = null;
}
this._updateValue(value);
}
setOffset(offset: number): void {
this._offset = offset;
}
flattenOffset(): void {
this._value += this._offset;
this._offset = 0;
}
addListener(callback: ValueListenerCallback): string {
var id = String(_uniqueId++);
this._listeners[id] = callback;
return id;
}
removeListener(id: string): void {
delete this._listeners[id];
}
removeAllListeners(): void {
this._listeners = {};
}
animate(animation: Animation, callback: ?EndCallback): void {
var handle = InteractionManager.createInteractionHandle();
var previousAnimation = this._animation;
this._animation && this._animation.stop();
this._animation = animation;
animation.start(
this._value,
(value) => {
this._updateValue(value);
},
(result) => {
this._animation = null;
InteractionManager.clearInteractionHandle(handle);
callback && callback(result);
},
previousAnimation,
);
}
stopAnimation(callback?: ?(value: number) => void): void {
this.stopTracking();
this._animation && this._animation.stop();
this._animation = null;
callback && callback(this.__getValue());
}
stopTracking(): void {
this._tracking && this._tracking.__detach();
this._tracking = null;
}
track(tracking: Animated): void {
this.stopTracking();
this._tracking = tracking;
}
interpolate(config: InterpolationConfigType): AnimatedInterpolation {
return new AnimatedInterpolation(this, Interpolation.create(config));
}
_updateValue(value: number): void {
this._value = value;
_flush(this);
for (var key in this._listeners) {
this._listeners[key]({value: this.__getValue()});
}
}
}
type ValueXYListenerCallback = (value: {x: number; y: number}) => void;
class AnimatedValueXY extends AnimatedWithChildren {
x: AnimatedValue;
y: AnimatedValue;
_listeners: {[key: string]: {x: string; y: string}};
constructor(valueIn?: ?{x: number | AnimatedValue; y: number | AnimatedValue}) {
super();
var 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 initalized with an object of numbers or ' +
'AnimatedValues.'
);
this.x = value.x;
this.y = value.y;
}
this._listeners = {};
}
setValue(value: {x: number; y: number}) {
this.x.setValue(value.x);
this.y.setValue(value.y);
}
setOffset(offset: {x: number; y: number}) {
this.x.setOffset(offset.x);
this.y.setOffset(offset.y);
}
flattenOffset(): void {
this.x.flattenOffset();
this.y.flattenOffset();
}
__getValue(): {x: number; y: number} {
return {
x: this.x.__getValue(),
y: this.y.__getValue(),
};
}
stopAnimation(callback?: ?() => number): void {
this.x.stopAnimation();
this.y.stopAnimation();
callback && callback(this.__getValue());
}
addListener(callback: ValueXYListenerCallback): string {
var id = String(_uniqueId++);
var jointCallback = ({value: number}) => {
callback(this.__getValue());
};
this._listeners[id] = {
x: this.x.addListener(jointCallback),
y: this.y.addListener(jointCallback),
};
return id;
}
removeListener(id: string): void {
this.x.removeListener(this._listeners[id].x);
this.y.removeListener(this._listeners[id].y);
delete this._listeners[id];
}
getLayout(): {[key: string]: AnimatedValue} {
return {
left: this.x,
top: this.y,
};
}
getTranslateTransform(): Array<{[key: string]: AnimatedValue}> {
return [
{translateX: this.x},
{translateY: this.y}
];
}
}
class AnimatedInterpolation extends AnimatedWithChildren {
_parent: Animated;
_interpolation: (input: number) => number | string;
constructor(parent: Animated, interpolation: (input: number) => number | string) {
super();
this._parent = parent;
this._interpolation = interpolation;
}
__getValue(): number | string {
var parentValue: number = this._parent.__getValue();
invariant(
typeof parentValue === 'number',
'Cannot interpolate an input which is not a number.'
);
return this._interpolation(parentValue);
}
interpolate(config: InterpolationConfigType): AnimatedInterpolation {
return new AnimatedInterpolation(this, Interpolation.create(config));
}
__attach(): void {
this._parent.__addChild(this);
}
__detach(): void {
this._parent.__removeChild(this);
}
}
class AnimatedTransform extends AnimatedWithChildren {
_transforms: Array<Object>;
constructor(transforms: Array<Object>) {
super();
this._transforms = transforms;
}
__getValue(): Array<Object> {
return this._transforms.map(transform => {
var result = {};
for (var key in transform) {
var value = transform[key];
if (value instanceof Animated) {
result[key] = value.__getValue();
} else {
result[key] = value;
}
}
return result;
});
}
__getAnimatedValue(): Array<Object> {
return this._transforms.map(transform => {
var result = {};
for (var key in transform) {
var value = transform[key];
if (value instanceof Animated) {
result[key] = value.__getAnimatedValue();
} else {
// All transform components needed to recompose matrix
result[key] = value;
}
}
return result;
});
}
__attach(): void {
this._transforms.forEach(transform => {
for (var key in transform) {
var value = transform[key];
if (value instanceof Animated) {
value.__addChild(this);
}
}
});
}
__detach(): void {
this._transforms.forEach(transform => {
for (var key in transform) {
var value = transform[key];
if (value instanceof Animated) {
value.__removeChild(this);
}
}
});
}
}
class AnimatedStyle extends AnimatedWithChildren {
_style: Object;
constructor(style: any) {
super();
style = flattenStyle(style) || {};
if (style.transform) {
style = {
...style,
transform: new AnimatedTransform(style.transform),
};
}
this._style = style;
}
__getValue(): Object {
var style = {};
for (var key in this._style) {
var value = this._style[key];
if (value instanceof Animated) {
style[key] = value.__getValue();
} else {
style[key] = value;
}
}
return style;
}
__getAnimatedValue(): Object {
var style = {};
for (var key in this._style) {
var value = this._style[key];
if (value instanceof Animated) {
style[key] = value.__getAnimatedValue();
}
}
return style;
}
__attach(): void {
for (var key in this._style) {
var value = this._style[key];
if (value instanceof Animated) {
value.__addChild(this);
}
}
}
__detach(): void {
for (var key in this._style) {
var value = this._style[key];
if (value instanceof Animated) {
value.__removeChild(this);
}
}
}
}
class AnimatedProps extends Animated {
_props: Object;
_callback: () => void;
constructor(
props: Object,
callback: () => void,
) {
super();
if (props.style) {
props = {
...props,
style: new AnimatedStyle(props.style),
};
}
this._props = props;
this._callback = callback;
this.__attach();
}
__getValue(): Object {
var props = {};
for (var key in this._props) {
var value = this._props[key];
if (value instanceof Animated) {
props[key] = value.__getValue();
} else {
props[key] = value;
}
}
return props;
}
__getAnimatedValue(): Object {
var props = {};
for (var key in this._props) {
var value = this._props[key];
if (value instanceof Animated) {
props[key] = value.__getAnimatedValue();
}
}
return props;
}
__attach(): void {
for (var key in this._props) {
var value = this._props[key];
if (value instanceof Animated) {
value.__addChild(this);
}
}
}
__detach(): void {
for (var key in this._props) {
var value = this._props[key];
if (value instanceof Animated) {
value.__removeChild(this);
}
}
}
update(): void {
this._callback();
}
}
function createAnimatedComponent(Component: any): any {
var refName = 'node';
class AnimatedComponent extends React.Component {
_propsAnimated: AnimatedProps;
componentWillUnmount() {
this._propsAnimated && this._propsAnimated.__detach();
}
setNativeProps(props) {
this.refs[refName].setNativeProps(props);
}
componentWillMount() {
this.attachProps(this.props);
}
attachProps(nextProps) {
var oldPropsAnimated = this._propsAnimated;
// The system is best designed when setNativeProps is implemented. It is
// able to avoid re-rendering and directly set the attributes that
// changed. However, setNativeProps can only be implemented on leaf
// native components. If you want to animate a composite component, you
// need to re-render it. In this case, we have a fallback that uses
// forceUpdate.
var callback = () => {
if (this.refs[refName].setNativeProps) {
var value = this._propsAnimated.__getAnimatedValue();
this.refs[refName].setNativeProps(value);
} else {
this.forceUpdate();
}
};
this._propsAnimated = new AnimatedProps(
nextProps,
callback,
);
// When you call detach, it removes the element from the parent list
// of children. If it goes to 0, then the parent also detaches itself
// and so on.
// An optimization is to attach the new elements and THEN detach the old
// ones instead of detaching and THEN attaching.
// This way the intermediate state isn't to go to 0 and trigger
// this expensive recursive detaching to then re-attach everything on
// the very next operation.
oldPropsAnimated && oldPropsAnimated.__detach();
}
componentWillReceiveProps(nextProps) {
this.attachProps(nextProps);
}
render() {
return (
<Component
{...this._propsAnimated.__getValue()}
ref={refName}
/>
);
}
}
return AnimatedComponent;
}
class AnimatedTracking extends Animated {
_value: AnimatedValue;
_parent: Animated;
_callback: ?EndCallback;
_animationConfig: Object;
_animationClass: any;
constructor(
value: AnimatedValue,
parent: Animated,
animationClass: any,
animationConfig: Object,
callback?: ?EndCallback,
) {
super();
this._value = value;
this._parent = parent;
this._animationClass = animationClass;
this._animationConfig = animationConfig;
this._callback = callback;
this.__attach();
}
__getValue(): Object {
return this._parent.__getValue();
}
__attach(): void {
this._parent.__addChild(this);
}
__detach(): void {
this._parent.__removeChild(this);
}
update(): void {
this._value.animate(new this._animationClass({
...this._animationConfig,
toValue: (this._animationConfig.toValue: any).__getValue(),
}), this._callback);
}
}
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;
/**
* Takes an array of mappings and extracts values from each arg accordingly,
* then calls setValue on the mapped outputs. e.g.
*
* onScroll={this.AnimatedEvent(
* [{nativeEvent: {contentOffset: {x: this._scrollX}}}]
* {listener} // optional listener invoked asynchronously
* )
* ...
* onPanResponderMove: this.AnimatedEvent([
* null, // raw event arg
* {dx: this._panX}, // gestureState arg
* ]),
*
*/
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);
}
};
};
module.exports = {
delay,
sequence,
parallel,
stagger,
decay,
timing,
spring,
event,
Value: AnimatedValue,
ValueXY: AnimatedValueXY,
__PropsOnlyForTests: AnimatedProps,
View: createAnimatedComponent(View),
Text: createAnimatedComponent(Text),
Image: createAnimatedComponent(Image),
createAnimatedComponent,
};