UNPKG

react-native-image-gallery

Version:

Pure JavaScript image gallery component for iOS and Android

356 lines (296 loc) 11.1 kB
'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; } }