UNPKG

react-flexigrid

Version:

A React table component designed to allow presenting millions of rows of data.

205 lines (165 loc) 5.75 kB
import { requestAnimationFrame, getJudgeFunction } from '../utils' const MOVE_AMPLITUDE = 1.6 const DECELERATION_AMPLITUDE = 1.6 const DECELERATION_FACTOR = 325 const TRACKER_TIMEOUT = 100 export default class TouchHandler { constructor( onTouchScroll: Function, handleScrollX: Boolean | Function, handleScrollY: Boolean | Function, stopPropagation: ?Boolean | ?Function, ) { this.trackerId = null this.dragAnimationId = null this.deltaX = 0 this.deltaY = 0 this.lastTouchX = 0 this.lastTouchY = 0 this.velocityX = 0 this.velocityY = 0 this.accumulatedDeltaX = 0 this.accumulatedDeltaY = 0 this.lastFrameTimestamp = Date.now() this.autoScrollTimestamp = Date.now() this.handleScrollX = getJudgeFunction(handleScrollX) this.handleScrollY = getJudgeFunction(handleScrollY) this.stopPropagation = getJudgeFunction(stopPropagation) this.onTouchScrollCallback = onTouchScroll } onTouchStart(e: Object) { this.lastTouchX = e.touches[0].pageX this.lastTouchY = e.touches[0].pageY this.velocityX = 0 this.velocityY = 0 this.accumulatedDeltaX = 0 this.accumulatedDeltaY = 0 this.lastFrameTimestamp = Date.now() clearInterval(this.trackerId) this.trackerId = setInterval(this.track, TRACKER_TIMEOUT) if (this.stopPropagation()) { e.stopPropagation() } } onTouchEnd(e: Object) { this.onTouchCancel(e) requestAnimationFrame(this.startAutoScroll) } onTouchCancel(e: Object) { clearInterval(this.trackerId) this.trackerId = null if (this.stopPropagation()) { e.stopPropagation() } } onTouchMove(e: Object) { const moveX = e.touches[0].pageX const moveY = e.touches[0].pageY // Compute delta scrolled since last drag // Mobile, scrolling is inverted this.deltaX = MOVE_AMPLITUDE * (this.lastTouchX - moveX) this.deltaY = MOVE_AMPLITUDE * (this.lastTouchY - moveY) const handleScrollX = this.handleScrollX(this.deltaX, this.deltaY) const handleScrollY = this.handleScrollY(this.deltaY, this.deltaX) if (!handleScrollX && !handleScrollY) { return } // If we can handle scroll update last touch for computing delta if (handleScrollX) { this.lastTouchX = moveX } else { this.deltaX = 0 } if (handleScrollY) { this.lastTouchY = moveY } else { this.deltaY = 0 } e.preventDefault() // ensure minimum delta magnitude is met to avoid jitter let changed = false if (Math.abs(this.deltaX) > 2 || Math.abs(this.deltaY) > 2) { if (this.stopPropagation()) { e.stopPropagation() } changed = true } // Request animation frame to trigger scroll of computed delta if (changed === true && this.dragAnimationId === null) { this.dragAnimationId = requestAnimationFrame(this.didTouchMove) } } didTouchMove = () => { // Fire scroll callback based on computed drag delta. // Also track accummulated delta so we can calculate velocity this.dragAnimationId = null this.onTouchScrollCallback(this.deltaX, this.deltaY) this.accumulatedDeltaX += this.deltaX this.accumulatedDeltaY += this.deltaY this.deltaX = 0 this.deltaY = 0 } track = () => { // Compute velocity based on a weighted average of drag over // last 100ms and previous velocity. Combining into a moving average // results in a smoother scroll. const now = Date.now() const elapsed = now - this.lastFrameTimestamp const oldVelocityX = this.velocityX const oldVelocityY = this.velocityY // We compute velocity using a weighted average of the current // velocity and the previous velocity. If the previous velocity // is 0, put the full weight on the last 100ms let weight = 0.8 if (elapsed < TRACKER_TIMEOUT) { weight *= (elapsed / TRACKER_TIMEOUT) } if (oldVelocityX === 0 && oldVelocityY === 0) { weight = 1 } // Formula for computing weighted average of velocity this.velocityX = weight * (TRACKER_TIMEOUT * this.accumulatedDeltaX / (1 + elapsed)) if (weight < 1) { this.velocityX += (1 - weight) * oldVelocityX } this.velocityY = weight * (TRACKER_TIMEOUT * this.accumulatedDeltaY / (1 + elapsed)) if (weight < 1) { this.velocityY += (1 - weight) * oldVelocityY } this.accumulatedDeltaX = 0 this.accumulatedDeltaY = 0 this.lastFrameTimestamp = now } startAutoScroll = () => { // To kick off deceleration / momentum scrolling, handle any // scrolling from a drag which was waiting for an animation // frame. Then update our velocity. // Finally start the momentum scrolling handler (autoScroll) this.autoScrollTimestamp = Date.now() if (this.deltaX > 0 || this.deltaY > 0) { this.didTouchMove() } this.track() this.autoScroll() } autoScroll = () => { // Compute a scroll delta with an exponential decay based on // time elapsed since drag was released. This is called // recursively on animation frames until the delta is below // a threshold (5 pixels) const elapsed = Date.now() - this.autoScrollTimestamp const factor = DECELERATION_AMPLITUDE * Math.exp(-elapsed / DECELERATION_FACTOR) let deltaX = factor * this.velocityX let deltaY = factor * this.velocityY if (Math.abs(deltaX) <= 5 || !this.handleScrollX(deltaX, deltaY)) { deltaX = 0 } if (Math.abs(deltaY) <= 5 || !this.handleScrollY(deltaY, deltaX)) { deltaY = 0 } if (deltaX !== 0 || deltaY !== 0) { this.onTouchScrollCallback(deltaX, deltaY) requestAnimationFrame(this.autoScroll) } } }