UNPKG

@antv/x6-react-components

Version:

React components for building x6 editors

254 lines (198 loc) 6.94 kB
import { getJudgeFunction } from '../fn' import { requestAnimationFrame } from './animationFrame' export class TouchHandler { private deltaX: number private deltaY: number private lastTouchX: number private lastTouchY: number private velocityX: number private velocityY: number private accumulatedDeltaX: number private accumulatedDeltaY: number private lastFrameTimestamp: number private autoScrollTimestamp: number private trackerId: number | null private dragAnimationId: number | null private handleScrollX: (deltaX: number, deltaY: number) => boolean private handleScrollY: (deltaX: number, deltaY: number) => boolean private callback: (deltaX: number, deltaY: number) => void private stopPropagation: () => boolean constructor(options: TouchHandler.Options) { 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.callback = options.onTouchScroll this.handleScrollX = getJudgeFunction(options.shouldHandleScrollX) this.handleScrollY = getJudgeFunction(options.shouldHandleScrollY) this.stopPropagation = getJudgeFunction(options.stopPropagation) } onTouchStart(e: TouchEvent) { 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() if (this.trackerId != null) { clearInterval(this.trackerId) } this.trackerId = window.setInterval( this.track, TouchHandler.TRACKER_TIMEOUT, ) if (this.stopPropagation()) { e.stopPropagation() } } onTouchEnd(e: TouchEvent) { this.onTouchCancel(e) requestAnimationFrame(this.startAutoScroll) } onTouchCancel(e: TouchEvent) { if (this.trackerId != null) { clearInterval(this.trackerId) this.trackerId = null } if (this.stopPropagation()) { e.stopPropagation() } } onTouchMove(e: TouchEvent) { const moveX = e.touches[0].pageX const moveY = e.touches[0].pageY // Compute delta scrolled since last drag // Mobile, scrolling is inverted this.deltaX = TouchHandler.MOVE_AMPLITUDE * (this.lastTouchX - moveX) this.deltaY = TouchHandler.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 && 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.callback(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 < TouchHandler.TRACKER_TIMEOUT) { weight *= elapsed / TouchHandler.TRACKER_TIMEOUT } if (oldVelocityX === 0 && oldVelocityY === 0) { weight = 1 } // Formula for computing weighted average of velocity this.velocityX = weight * ((TouchHandler.TRACKER_TIMEOUT * this.accumulatedDeltaX) / (1 + elapsed)) if (weight < 1) { this.velocityX += (1 - weight) * oldVelocityX } this.velocityY = weight * ((TouchHandler.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 = TouchHandler.DECELERATION_AMPLITUDE * Math.exp(-elapsed / TouchHandler.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.callback(deltaX, deltaY) requestAnimationFrame(this.autoScroll) } } } export namespace TouchHandler { export const MOVE_AMPLITUDE = 1.6 export const DECELERATION_AMPLITUDE = 1.6 export const DECELERATION_FACTOR = 325 export const TRACKER_TIMEOUT = 100 export interface Options { onTouchScroll: (deltaX: number, deltaY: number) => void shouldHandleScrollX: boolean | ((deltaX: number, deltaY: number) => boolean) shouldHandleScrollY: boolean | ((deltaX: number, deltaY: number) => boolean) stopPropagation?: boolean | (() => boolean) } }