react-native-image-gallery
Version:
Pure JavaScript image gallery component for iOS and Android
356 lines (296 loc) • 11.1 kB
JavaScript
'use strict';
/**
* Inspired by Android Scroller
*/
import ViscousFluidInterpolator from './ViscousFluidInterpolator';
import {currentAnimationTimeMillis} from './AnimationUtils';
/**
* The coefficient of friction applied to flings/scrolls.
* @type {number}
*/
// const SCROLL_FRICTION = 0.015;
const SCROLL_FRICTION = 0.03;
const DEFAULT_DURATION = 250;
const SCROLL_MODE = 0;
const FLING_MODE = 1;
const DECELERATION_RATE = Math.log(0.78) / Math.log(0.9);
const INFLEXION = 0.35; // Tension lines cross at (INFLEXION, 1)
const START_TENSION = 0.5;
const END_TENSION = 1.0;
const P1 = START_TENSION * INFLEXION;
const P2 = 1.0 - END_TENSION * (1.0 - INFLEXION);
const NB_SAMPLES = 100;
const SPLINE_POSITION = [];
const SPLINE_TIME = [];
const GRAVITY_EARTH = 9.80665;
(function () {
var x_min = 0;
var y_min = 0;
for (let i = 0; i < NB_SAMPLES; i++) {
let alpha = i / NB_SAMPLES;
let x_max = 1;
let x, tx, coef;
while (true) {
x = x_min + (x_max - x_min) / 2.0;
coef = 3.0 * x * (1.0 - x);
tx = coef * ((1.0 - x) * P1 + x * P2) + x * x * x;
if (Math.abs(tx - alpha) < 1E-5) break;
if (tx > alpha) x_max = x;
else x_min = x;
}
SPLINE_POSITION[i] = coef * ((1.0 - x) * START_TENSION + x) + x * x * x;
let y_max = 1.0;
let y, dy;
while (true) {
y = y_min + (y_max - y_min) / 2.0;
coef = 3.0 * y * (1.0 - y);
dy = coef * ((1.0 - y) * START_TENSION + y) + y * y * y;
if (Math.abs(dy - alpha) < 1E-5) break;
if (dy > alpha) y_max = y;
else y_min = y;
}
SPLINE_TIME[i] = coef * ((1.0 - y) * P1 + y * P2) + y * y * y;
}
SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0;
})();
function signum (number) {
if (isNaN(number)) {
return NaN;
}
var sig = number;
if (number > 0) {
sig = 1;
} else if (number < 0) {
sig = -1;
}
return sig;
}
export default class Scroller {
/**
*
* @param flywheel specify whether or not to support progressive "flywheel" behavior in flinging.
*/
constructor (flywheel, onScrollCallback) {
this.mCurrX = 0;
this.mCurrY = 0;
this.mFinished = true;
this.mInterpolator = ViscousFluidInterpolator;
// this.mPpi = PixelRatio.get() * 160;
this.mPpi = 160;
this.mDeceleration = this.computeDeceleration(SCROLL_FRICTION);
this.mFlywheel = flywheel;
this.mPhysicalCoeff = this.computeDeceleration(0.84); // look and feel tuning
this.mFlingFriction = SCROLL_FRICTION;
this.onScrollCallback = onScrollCallback;
}
computeDeceleration (friction) {
return GRAVITY_EARTH * 39.37 * this.mPpi * friction;
}
/**
* Returns whether the scroller has finished scrolling.
* @returns {Boolean} True if the scroller has finished scrolling, false otherwise.
*/
isFinished () {
return this.mFinished;
}
/**
* Force the finished field to a particular value.
* @param finished The new finished value.
*/
forceFinished (finished) {
this.mFinished = finished;
}
/**
* Returns the current X offset in the scroll.
* @returns {*} The new X offset as an absolute distance from the origin.
*/
getCurrX () {
return this.mCurrX;
}
/**
* Returns the current Y offset in the scroll.
* @returns {*} The new Y offset as an absolute distance from the origin.
*/
getCurrY () {
return this.mCurrY;
}
getCurrVelocity () {
return this.mMode === FLING_MODE ?
this.mCurrVelocity : this.mVelocity - this.mDeceleration * this.timePassed() / 2000.0;
}
computeScrollOffset () {
if (this.mFinished) {
this.onScrollCallback && this.onScrollCallback(0, 0, this);
return false;
}
var timePassed = currentAnimationTimeMillis() - this.mStartTime;
if (timePassed < this.mDuration) {
switch (this.mMode) {
case SCROLL_MODE:
let x = this.mInterpolator.getInterpolation(timePassed * this.mDurationReciprocal);
this.mCurrX = this.mStartX + Math.round(x * this.mDeltaX);
this.mCurrY = this.mStartY + Math.round(x * this.mDeltaY);
break;
case FLING_MODE:
let t = timePassed / this.mDuration;
let index = parseInt(NB_SAMPLES * t);
let distanceCoef = 1;
let velocityCoef = 0;
if (index < NB_SAMPLES) {
let t_inf = index / NB_SAMPLES;
let t_sup = (index + 1) / NB_SAMPLES;
let d_inf = SPLINE_POSITION[index];
let d_sup = SPLINE_POSITION[index + 1];
velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
distanceCoef = d_inf + (t - t_inf) * velocityCoef;
}
this.mCurrVelocity = velocityCoef * this.mDistance / this.mDuration * 1000;
this.mCurrX = this.mStartX + Math.round(distanceCoef * (this.mFinalX - this.mStartX));
// Pin to mMinX <= mCurrX <= mMaxX
// this.mCurrX = Math.min(this.mCurrX, this.mMaxX);
// this.mCurrX = Math.max(this.mCurrX, this.mMinX);
this.mCurrY = this.mStartY + Math.round(distanceCoef * (this.mFinalY - this.mStartY));
// Pin to mMinY <= mCurrY <= mMaxY
this.mCurrY = Math.min(this.mCurrY, this.mMaxY);
this.mCurrY = Math.max(this.mCurrY, this.mMinY);
if (this.mCurrX == this.mFinalX && this.mCurrY == this.mFinalY) {
this.mFinished = true;
}
break;
}
} else {
this.mCurrX = this.mFinalX;
this.mCurrY = this.mFinalY;
this.mFinished = true;
}
var dx = this.mCurrX - this.mLastX;
var dy = this.mCurrY - this.mLastY;
this.mLastX = this.mCurrX;
this.mLastY = this.mCurrY;
this.onScrollCallback && this.onScrollCallback(dx, dy, this);
if (dx === 0 && dy === 0 && this.mFinished) {
return false;
}
return true;
}
startScroll (startX, startY, dx, dy, duration = DEFAULT_DURATION) {
this.mMode = SCROLL_MODE;
this.mFinished = false;
this.mDuration = duration;
this.mStartTime = currentAnimationTimeMillis();
this.mStartX = startX;
this.mStartY = startY;
this.mFinalX = startX + dx;
this.mFinalY = startY + dy;
this.mDeltaX = dx;
this.mDeltaY = dy;
this.mDurationReciprocal = 1.0 / this.mDuration;
this.mLastX = this.mStartX;
this.mLastY = this.mStartY;
this.performAnimation();
}
/**
* Start scrolling based on a fling gesture. The distance travelled will
* depend on the initial velocity of the fling.
* @param startX
* @param startY
* @param velocityX Initial velocity of the fling (X) measured in dp or pt per second
* @param velocityY Initial velocity of the fling (Y) measured in dp or pt per second
* @param minX
* @param maxX
* @param minY
* @param maxY
*/
fling (startX, startY, velocityX, velocityY,
minX, maxX, minY, maxY) {
// Continue a scroll or fling in progress
if (this.mFlywheel && !this.mFinished) {
let oldVel = this.getCurrVelocity();
let dx = this.mFinalX - this.mStartX;
let dy = this.mFinalY - this.mStartY;
let hyp = Math.hypot(dx, dy);
let ndx = dx / hyp;
let ndy = dy / hyp;
let oldVelocityX = ndx * oldVel;
let oldVelocityY = ndy * oldVel;
if (signum(velocityX) === signum(oldVelocityX) &&
signum(velocityY) === signum(oldVelocityY)) {
velocityX += oldVelocityX;
velocityY += oldVelocityY;
}
}
this.mMode = FLING_MODE;
this.mFinished = false;
let velocity = Math.hypot(velocityX, velocityY);
this.mVelocity = velocity;
this.mDuration = this.getSplineFlingDuration(velocity);
this.mStartTime = currentAnimationTimeMillis();
this.mStartX = startX;
this.mStartY = startY;
let coeffX = velocity == 0 ? 1.0 : velocityX / velocity;
let coeffY = velocity == 0 ? 1.0 : velocityY / velocity;
let totalDistance = this.getSplineFlingDistance(velocity);
this.mDistance = totalDistance * signum(velocity);
this.mMinX = minX;
this.mMaxX = maxX;
this.mMinY = minY;
this.mMaxY = maxY;
this.mFinalX = startX + Math.round(totalDistance * coeffX);
// Pin to mMinX <= mFinalX <= mMaxX
this.mFinalX = Math.min(this.mFinalX, this.mMaxX);
this.mFinalX = Math.max(this.mFinalX, this.mMinX);
this.mFinalY = startY + Math.round(totalDistance * coeffY);
// Pin to mMinY <= mFinalY <= mMaxY
this.mFinalY = Math.min(this.mFinalY, this.mMaxY);
this.mFinalY = Math.max(this.mFinalY, this.mMinY);
this.mLastX = this.mStartX;
this.mLastY = this.mStartY;
this.performAnimation();
}
getSplineDeceleration (velocity) {
return Math.log(INFLEXION * Math.abs(velocity) / (this.mFlingFriction * this.mPhysicalCoeff));
}
getSplineFlingDuration (velocity) {
var l = this.getSplineDeceleration(velocity);
var decelMinusOne = DECELERATION_RATE - 1.0;
return 1000.0 * Math.exp(l / decelMinusOne);
}
getSplineFlingDistance (velocity) {
var l = this.getSplineDeceleration(velocity);
var decelMinusOne = DECELERATION_RATE - 1.0;
return this.mFlingFriction * this.mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l);
}
performAnimation () {
if (this.computeScrollOffset()) {
requestAnimationFrame(this.performAnimation.bind(this));
} else {
}
}
abortAnimation () {
this.mCurrX = this.mFinalX;
this.mCurrY = this.mFinalY;
this.mFinished = true;
}
extendDuration (extend) {
var passed = timePassed();
this.mDuration = passed + extend;
this.mDurationReciprocal = 1.0 / this.mDuration;
this.mFinished = false;
}
timePassed () {
return currentAnimationTimeMillis() - this.mStartTime;
}
setFinalX (newX) {
this.mFinalX = newX;
this.mDeltaX = this.mFinalX - this.mStartX;
this.mFinished = false;
}
setFinalY (newY) {
this.mFinalY = newY;
this.mDeltaY = this.mFinalY - this.mStartY;
this.mFinished = false;
}
debugInfo () {
return 'cur=' + this.mCurrX + ' ' + this.mCurrY + ', final=' + this.mFinalX + ' ' + this.mFinalY;
}
}