mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-01-12 22:50:10 +08:00
Summary: [General] [Changed] - If `isInteraction` is not specified in the config, it would always default to `true` which would block interactions like VirtualizedList updates. This is generally not what you want with useNativeDriver since the animation won't be interrupted by JS. If something does end up interfering with an animation and causes frame drops, `isInteraction` can be set manually. Reviewed By: yungsters Differential Revision: D14988087 fbshipit-source-id: 791b5cc327ffef6d2720c647a228cf3134a27cda
329 lines
10 KiB
JavaScript
329 lines
10 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its 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';
|
|
|
|
const AnimatedValue = require('../nodes/AnimatedValue');
|
|
const AnimatedValueXY = require('../nodes/AnimatedValueXY');
|
|
const Animation = require('./Animation');
|
|
const SpringConfig = require('../SpringConfig');
|
|
|
|
const invariant = require('invariant');
|
|
const {shouldUseNativeDriver} = require('../NativeAnimatedHelper');
|
|
|
|
import type {AnimationConfig, EndCallback} from './Animation';
|
|
|
|
export type SpringAnimationConfig = AnimationConfig & {
|
|
toValue: number | AnimatedValue | {x: number, y: number} | AnimatedValueXY,
|
|
overshootClamping?: boolean,
|
|
restDisplacementThreshold?: number,
|
|
restSpeedThreshold?: number,
|
|
velocity?: number | {x: number, y: number},
|
|
bounciness?: number,
|
|
speed?: number,
|
|
tension?: number,
|
|
friction?: number,
|
|
stiffness?: number,
|
|
damping?: number,
|
|
mass?: number,
|
|
delay?: number,
|
|
};
|
|
|
|
export type SpringAnimationConfigSingle = AnimationConfig & {
|
|
toValue: number | AnimatedValue,
|
|
overshootClamping?: boolean,
|
|
restDisplacementThreshold?: number,
|
|
restSpeedThreshold?: number,
|
|
velocity?: number,
|
|
bounciness?: number,
|
|
speed?: number,
|
|
tension?: number,
|
|
friction?: number,
|
|
stiffness?: number,
|
|
damping?: number,
|
|
mass?: number,
|
|
delay?: number,
|
|
};
|
|
|
|
class SpringAnimation extends Animation {
|
|
_overshootClamping: boolean;
|
|
_restDisplacementThreshold: number;
|
|
_restSpeedThreshold: number;
|
|
_lastVelocity: number;
|
|
_startPosition: number;
|
|
_lastPosition: number;
|
|
_fromValue: number;
|
|
_toValue: any;
|
|
_stiffness: number;
|
|
_damping: number;
|
|
_mass: number;
|
|
_initialVelocity: number;
|
|
_delay: number;
|
|
_timeout: any;
|
|
_startTime: number;
|
|
_lastTime: number;
|
|
_frameTime: number;
|
|
_onUpdate: (value: number) => void;
|
|
_animationFrame: any;
|
|
_useNativeDriver: boolean;
|
|
|
|
constructor(config: SpringAnimationConfigSingle) {
|
|
super();
|
|
|
|
this._overshootClamping = config.overshootClamping ?? false;
|
|
this._restDisplacementThreshold = config.restDisplacementThreshold ?? 0.001;
|
|
this._restSpeedThreshold = config.restSpeedThreshold ?? 0.001;
|
|
this._initialVelocity = config.velocity ?? 0;
|
|
this._lastVelocity = config.velocity ?? 0;
|
|
this._toValue = config.toValue;
|
|
this._delay = config.delay ?? 0;
|
|
this._useNativeDriver = shouldUseNativeDriver(config);
|
|
this.__isInteraction = config.isInteraction ?? !this._useNativeDriver;
|
|
this.__iterations = config.iterations ?? 1;
|
|
|
|
if (
|
|
config.stiffness !== undefined ||
|
|
config.damping !== undefined ||
|
|
config.mass !== undefined
|
|
) {
|
|
invariant(
|
|
config.bounciness === undefined &&
|
|
config.speed === undefined &&
|
|
config.tension === undefined &&
|
|
config.friction === undefined,
|
|
'You can define one of bounciness/speed, tension/friction, or stiffness/damping/mass, but not more than one',
|
|
);
|
|
this._stiffness = config.stiffness ?? 100;
|
|
this._damping = config.damping ?? 10;
|
|
this._mass = config.mass ?? 1;
|
|
} else if (config.bounciness !== undefined || config.speed !== undefined) {
|
|
// Convert the origami bounciness/speed values to stiffness/damping
|
|
// We assume mass is 1.
|
|
invariant(
|
|
config.tension === undefined &&
|
|
config.friction === undefined &&
|
|
config.stiffness === undefined &&
|
|
config.damping === undefined &&
|
|
config.mass === undefined,
|
|
'You can define one of bounciness/speed, tension/friction, or stiffness/damping/mass, but not more than one',
|
|
);
|
|
const springConfig = SpringConfig.fromBouncinessAndSpeed(
|
|
config.bounciness ?? 8,
|
|
config.speed ?? 12,
|
|
);
|
|
this._stiffness = springConfig.stiffness;
|
|
this._damping = springConfig.damping;
|
|
this._mass = 1;
|
|
} else {
|
|
// Convert the origami tension/friction values to stiffness/damping
|
|
// We assume mass is 1.
|
|
const springConfig = SpringConfig.fromOrigamiTensionAndFriction(
|
|
config.tension ?? 40,
|
|
config.friction ?? 7,
|
|
);
|
|
this._stiffness = springConfig.stiffness;
|
|
this._damping = springConfig.damping;
|
|
this._mass = 1;
|
|
}
|
|
|
|
invariant(this._stiffness > 0, 'Stiffness value must be greater than 0');
|
|
invariant(this._damping > 0, 'Damping value must be greater than 0');
|
|
invariant(this._mass > 0, 'Mass value must be greater than 0');
|
|
}
|
|
|
|
__getNativeAnimationConfig() {
|
|
return {
|
|
type: 'spring',
|
|
overshootClamping: this._overshootClamping,
|
|
restDisplacementThreshold: this._restDisplacementThreshold,
|
|
restSpeedThreshold: this._restSpeedThreshold,
|
|
stiffness: this._stiffness,
|
|
damping: this._damping,
|
|
mass: this._mass,
|
|
initialVelocity: this._initialVelocity ?? this._lastVelocity,
|
|
toValue: this._toValue,
|
|
iterations: this.__iterations,
|
|
};
|
|
}
|
|
|
|
start(
|
|
fromValue: number,
|
|
onUpdate: (value: number) => void,
|
|
onEnd: ?EndCallback,
|
|
previousAnimation: ?Animation,
|
|
animatedValue: AnimatedValue,
|
|
): void {
|
|
this.__active = true;
|
|
this._startPosition = fromValue;
|
|
this._lastPosition = this._startPosition;
|
|
|
|
this._onUpdate = onUpdate;
|
|
this.__onEnd = onEnd;
|
|
this._lastTime = Date.now();
|
|
this._frameTime = 0.0;
|
|
|
|
if (previousAnimation instanceof SpringAnimation) {
|
|
const internalState = previousAnimation.getInternalState();
|
|
this._lastPosition = internalState.lastPosition;
|
|
this._lastVelocity = internalState.lastVelocity;
|
|
// Set the initial velocity to the last velocity
|
|
this._initialVelocity = this._lastVelocity;
|
|
this._lastTime = internalState.lastTime;
|
|
}
|
|
|
|
const start = () => {
|
|
if (this._useNativeDriver) {
|
|
this.__startNativeAnimation(animatedValue);
|
|
} else {
|
|
this.onUpdate();
|
|
}
|
|
};
|
|
|
|
// If this._delay is more than 0, we start after the timeout.
|
|
if (this._delay) {
|
|
this._timeout = setTimeout(start, this._delay);
|
|
} else {
|
|
start();
|
|
}
|
|
}
|
|
|
|
getInternalState(): Object {
|
|
return {
|
|
lastPosition: this._lastPosition,
|
|
lastVelocity: this._lastVelocity,
|
|
lastTime: this._lastTime,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* This spring model is based off of a damped harmonic oscillator
|
|
* (https://en.wikipedia.org/wiki/Harmonic_oscillator#Damped_harmonic_oscillator).
|
|
*
|
|
* We use the closed form of the second order differential equation:
|
|
*
|
|
* x'' + (2ζ⍵_0)x' + ⍵^2x = 0
|
|
*
|
|
* where
|
|
* ⍵_0 = √(k / m) (undamped angular frequency of the oscillator),
|
|
* ζ = c / 2√mk (damping ratio),
|
|
* c = damping constant
|
|
* k = stiffness
|
|
* m = mass
|
|
*
|
|
* The derivation of the closed form is described in detail here:
|
|
* http://planetmath.org/sites/default/files/texpdf/39745.pdf
|
|
*
|
|
* This algorithm happens to match the algorithm used by CASpringAnimation,
|
|
* a QuartzCore (iOS) API that creates spring animations.
|
|
*/
|
|
onUpdate(): void {
|
|
// 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.
|
|
const MAX_STEPS = 64;
|
|
let now = Date.now();
|
|
if (now > this._lastTime + MAX_STEPS) {
|
|
now = this._lastTime + MAX_STEPS;
|
|
}
|
|
|
|
const deltaTime = (now - this._lastTime) / 1000;
|
|
this._frameTime += deltaTime;
|
|
|
|
const c: number = this._damping;
|
|
const m: number = this._mass;
|
|
const k: number = this._stiffness;
|
|
const v0: number = -this._initialVelocity;
|
|
|
|
const zeta = c / (2 * Math.sqrt(k * m)); // damping ratio
|
|
const omega0 = Math.sqrt(k / m); // undamped angular frequency of the oscillator (rad/ms)
|
|
const omega1 = omega0 * Math.sqrt(1.0 - zeta * zeta); // exponential decay
|
|
const x0 = this._toValue - this._startPosition; // calculate the oscillation from x0 = 1 to x = 0
|
|
|
|
let position = 0.0;
|
|
let velocity = 0.0;
|
|
const t = this._frameTime;
|
|
if (zeta < 1) {
|
|
// Under damped
|
|
const envelope = Math.exp(-zeta * omega0 * t);
|
|
position =
|
|
this._toValue -
|
|
envelope *
|
|
(((v0 + zeta * omega0 * x0) / omega1) * Math.sin(omega1 * t) +
|
|
x0 * Math.cos(omega1 * t));
|
|
// This looks crazy -- it's actually just the derivative of the
|
|
// oscillation function
|
|
velocity =
|
|
zeta *
|
|
omega0 *
|
|
envelope *
|
|
((Math.sin(omega1 * t) * (v0 + zeta * omega0 * x0)) / omega1 +
|
|
x0 * Math.cos(omega1 * t)) -
|
|
envelope *
|
|
(Math.cos(omega1 * t) * (v0 + zeta * omega0 * x0) -
|
|
omega1 * x0 * Math.sin(omega1 * t));
|
|
} else {
|
|
// Critically damped
|
|
const envelope = Math.exp(-omega0 * t);
|
|
position = this._toValue - envelope * (x0 + (v0 + omega0 * x0) * t);
|
|
velocity =
|
|
envelope * (v0 * (t * omega0 - 1) + t * x0 * (omega0 * omega0));
|
|
}
|
|
|
|
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
|
|
let isOvershooting = false;
|
|
if (this._overshootClamping && this._stiffness !== 0) {
|
|
if (this._startPosition < this._toValue) {
|
|
isOvershooting = position > this._toValue;
|
|
} else {
|
|
isOvershooting = position < this._toValue;
|
|
}
|
|
}
|
|
const isVelocity = Math.abs(velocity) <= this._restSpeedThreshold;
|
|
let isDisplacement = true;
|
|
if (this._stiffness !== 0) {
|
|
isDisplacement =
|
|
Math.abs(this._toValue - position) <= this._restDisplacementThreshold;
|
|
}
|
|
|
|
if (isOvershooting || (isVelocity && isDisplacement)) {
|
|
if (this._stiffness !== 0) {
|
|
// Ensure that we end up with a round value
|
|
this._lastPosition = this._toValue;
|
|
this._lastVelocity = 0;
|
|
this._onUpdate(this._toValue);
|
|
}
|
|
|
|
this.__debouncedOnEnd({finished: true});
|
|
return;
|
|
}
|
|
this._animationFrame = requestAnimationFrame(this.onUpdate.bind(this));
|
|
}
|
|
|
|
stop(): void {
|
|
super.stop();
|
|
this.__active = false;
|
|
clearTimeout(this._timeout);
|
|
global.cancelAnimationFrame(this._animationFrame);
|
|
this.__debouncedOnEnd({finished: false});
|
|
}
|
|
}
|
|
|
|
module.exports = SpringAnimation;
|