From f64ace31c3a258626b4b7780adc98e085de8309a Mon Sep 17 00:00:00 2001 From: wangchangqi Date: Thu, 29 Sep 2022 16:29:19 +0800 Subject: [PATCH] TASK:#109167 (feature)(bugfix)(improve)allow setOption dynamically & fix swipe trigger & optimize functional code --- src/lib/gesture/gesture-detector.ts | 275 +++++++++++++----- .../gesture/examples/test-multitouches.ts | 71 ++--- src/test/panels/gesture/examples/test-pan.ts | 22 +- .../panels/gesture/examples/test-swipe.ts | 80 +++++ src/test/panels/gesture/examples/test-tap.ts | 7 +- src/test/panels/gesture/gesture.ts | 7 + 6 files changed, 345 insertions(+), 117 deletions(-) create mode 100644 src/test/panels/gesture/examples/test-swipe.ts diff --git a/src/lib/gesture/gesture-detector.ts b/src/lib/gesture/gesture-detector.ts index 943a723..38c2b23 100644 --- a/src/lib/gesture/gesture-detector.ts +++ b/src/lib/gesture/gesture-detector.ts @@ -138,6 +138,7 @@ export interface PinchEvent { * Event: holdstart/holdmove/holdend */ export interface HoldEvent { + readonly touches?: TouchList // holdmove maybe needs readonly absolute?: { readonly dx: number readonly dy: number @@ -270,15 +271,16 @@ type EmbededGestureHTMLElement = HTMLElement & { } type GestureOptions = { - panThreshold: number - velocityThreshold: number + get holdThreshold(): number + set holdThreshold(t: number) + get panThreshold(): number + set panThreshold(t: number) + get velocityThreshold(): number + set velocityThreshold(t: number) } const DEBUG = 0 -const debug = (...args: any[]) => { - DEBUG && console.log.call(null, ...args) -} const debugInfo = (...args: any[]) => { DEBUG && console.info.call(null, ...args) } @@ -321,6 +323,64 @@ const generateMidCoordinate = ( }) } +/** + * 判定是否构成 swipe 行为 + * + * 当手指抬起时发出 swipe 事件,报告以下内容: + * 起点、重点、dx、dy、dt、vc 和 direction + * + * 轻扫行为的判定: + * 1.常规向右滑动(判定存在向右 swipe 轻扫行为) + * 条件:结束时方向同初始时方向一致 + * !------> + * 2.向右滑动又折返一小段(判定不存在 swipe 轻扫行为) + * 条件:结束时方向同初始时方向不一致,且折返距离小于在初始方向上移动的距离 + * !------- + * <---| + * 3.向右滑动又折返一大段(判定存在向左 swipe 轻扫行为) + * 条件:结束时方向同初始时方向不一致,且折返距离大于在初始方向上移动的距离 + * !---- + * <------| + * 4.其他方向(向左、向上、向下)同样按照1~3步推理 + * 5.向某个方向来回移动,只根据开始那一段和结束那一段进行判定,按照1~3步推理 + */ +const checkIsSwipeAction = ( + firstDirec: D, + lastDirec: D, + dx: N, + dy: N +): boolean => { + if (lastDirec === firstDirec) return true + else { + switch (lastDirec) { + case 'left': + return dx < 0 + case 'right': + return dx > 0 + case 'down': + return dy > 0 + case 'up': + return dy < 0 + default: + return false + } + } +} + +/** + * 检查是否超出阈值 + */ +const checkIfOverThreshold = ( + t1: Touch, + g2: GestureCoordinate, + threshold: number +): boolean => { + return ( + Math.abs(t1.screenX - g2.screenX) > threshold || + Math.abs(t1.screenY - g2.screenY) > threshold + ) +} + /** * 距离计算函数 * @@ -332,6 +392,22 @@ const computeDistance = (t1: T, t2: T): number => { return Math.sqrt(deltaX * deltaX + deltaY * deltaY) } +/** + * 计算坐标点与原点间的方向夹角,返回夹角区间:[-180°,180°]。 + * 如: + * 传入 (1,0),返回 0° + * 传入 (1,1),返回 45° + * 传入 (0,1),返回 90° + * 传入 (1,-1),返回 135° + * 传入 (0,-1),返回 180° + * 传入 (-1,1),返回 -45° + * 传入 (-1,0),返回 -90° + * 传入 (-1,-1),返回 -135° + * 传入 (-0,-1),返回 -180° + */ +const computeDirectionAngle = (dx: T, dy: T) => + (Math.atan2(dy, dx) * 180) / Math.PI + /** * 方向角计算函数 * @@ -340,10 +416,9 @@ const computeDistance = (t1: T, t2: T): number => { * 平面角区间: (-180, 180] */ const computeDirection = (t1: T, t2: T): number => { - return ( - (Math.atan2(t2.screenY - t1.screenY, t2.screenX - t1.screenX) * 180) / - Math.PI - ) + const dx = t2.screenX - t1.screenX + const dy = t2.screenY - t1.screenY + return computeDirectionAngle(dx, dy) } /** @@ -373,15 +448,20 @@ const computeDirectionFromAngle = (angle: number): PanOrSwipeDirection => { } } -const computeDirectionFromDxDy = ( - dx: number, - dy: number -): [number, PanOrSwipeDirection] => { - // angle 是一个正的角度数,从x轴正方向开始,顺时针递增 - let angle = (Math.atan2(dy, dx) * 180) / Math.PI +/** + * 计算触摸点的运动方向 + */ +const computeTouchDirection = ( + g1: T, + g2: T +): [PanOrSwipeDirection, number, number, number] => { + const dx = g2.screenX - g1.screenX + const dy = g2.screenY - g1.screenY + + let angle = computeDirectionAngle(dx, dy) if (angle < 0) angle += 360 - return [angle, computeDirectionFromAngle(angle)] + return [computeDirectionFromAngle(angle), angle, dx, dy] } /** @@ -397,19 +477,14 @@ const getTouchFromTouches = ( })[0] } -/** - * 思路:传入要监听的元素,要监听的事件, - * - * 用法: - * const myel = document.body; - * const myGestureFrame = new GestureDector(myel); - * myel.addEventListener('tap', (e) => debug(e)) - * - */ export default class GestureDector { + // 进入长按状态的阈值 + static HOLD_THRESHOLD = 500 + + // 进入滑动状态的阈值 static PAN_THRESHOLD = 20 - // 识别swipe事件所需的最小速度,单位为px/ms + // 判定是swipe事件的速度阈值,单位: px/ms static VELOCITY_THRESHOLD = 0.3 static THRESHOLD_SMOOTHING = 0.9 @@ -418,42 +493,61 @@ export default class GestureDector { // 连续两次点触(双击或三击)允许的x和y坐标的最大偏移量 static DOUBLE_TAP_DISTANCE = 50 - // 连续两次点触(双击或三击)允许的最大间隔时间 - static DOUBLE_TAP_TIME = 500 - // 进入长按状态的阈值 - static HOLD_INTERVAL = 1000 + // 连续两次点触(双击或三击)允许的最大间隔时间 + static DOUBLE_TAP_TIME = 300 // scale 触发阈值, 单位: px static SCALE_THRESHOLD = 20 + // rotate 触发阈值, 单位: 度° static ROTATE_THRESHOLD = 22.5 + // _options 是 option 的备份,用于重置 + _options!: GestureOptions | null + options!: GestureOptions target!: HTMLElement element: HTMLElement state: FSMGestureState - /* 触摸坐标 */ + // 开始触摸点 start!: GestureCoordinate + + // 最后一次触摸点,注意:最后一次的touchmove和touchend的触摸点相同 last!: GestureCoordinate + + // 最后一次的上一次触摸点,用于实时计算临近坐标点的方向夹角 + lastlast!: GestureCoordinate + + // 开始 pan 的触摸点,用于区分第1指和第2指的 start panstart!: GestureCoordinate + + // 结束 pan 的触摸点,用于区分第1指和第2指的 last panlast!: GestureCoordinate // 用于 doubletap lastTap: GestureCoordinate | null = null + firstTapTouches: TouchList | null = null + // 用于 tripletap lastlastTap: GestureCoordinate | null = null + secondTapTouches: TouchList | null = null - // x轴、y轴方向上的速度及合成速度 - // 单位: px/ms + // x轴、y轴方向上的速度及合成速度,单位: px/ms vx!: number vy!: number vc!: number + // 刚进入 hold 状态的指示值 + justEnteredHoldState = false + + // 已进入 pan 状态的指示值 + hasEnteredPanState = false + private touch1ID: Touch['identifier'] = -1 private touch2ID: Touch['identifier'] = -1 private lastPanTouchID: Touch['identifier'] = -1 @@ -480,16 +574,21 @@ export default class GestureDector { angle!: number + // 当前 pan/swipe 状态下事件的方向指向 direction!: PanOrSwipeDirection + // 当前 pan/swipe 状态下首次出现的方向指向 + firstDirection: PanOrSwipeDirection | null = null - constructor(el: HTMLElement, options?: GestureOptions) { - debug(el) + constructor(el: HTMLElement, options?: Partial) { this.element = el - this.options = options || { - panThreshold: GestureDector.PAN_THRESHOLD, - velocityThreshold: GestureDector.VELOCITY_THRESHOLD, + this.options = { + ...{ + holdThreshold: GestureDector.HOLD_THRESHOLD, + panThreshold: GestureDector.PAN_THRESHOLD, + velocityThreshold: GestureDector.VELOCITY_THRESHOLD, + }, + ...options, } - this.state = this.initialState } @@ -551,7 +650,7 @@ export default class GestureDector { this.start = this.last = generateCoordinate(evt.timeStamp, touch) if (this.getSeriesEvent('hold').length > 0) { - this.startTimer('holdtimeout', GestureDector.HOLD_INTERVAL) + this.startTimer('holdtimeout', this.options.holdThreshold) } }, touchstart: (evt, touch) => { @@ -880,9 +979,6 @@ export default class GestureDector { * * TODO: 考虑运动偏移量 */ - justEnteredHoldState = false - hasEnteredPanState = false - private holdState: FSMGestureState = { name: 'holdState', init: () => { @@ -915,12 +1011,12 @@ export default class GestureDector { this.getSeriesEvent('swipe').length || this.getSeriesEvent('pan').length ) { - const touch = evt.touches[1] if ( - Math.abs(touch.screenX - this.start.screenX) > - this.options.panThreshold || - Math.abs(touch.screenY - this.start.screenY) > + checkIfOverThreshold( + evt.touches[1], + this.start, this.options.panThreshold + ) ) { this.panState.touchmove?.(evt, evt.touches[1]) } @@ -935,6 +1031,7 @@ export default class GestureDector { this.emitEvent( 'holdmove', Object.freeze({ + touches: evt.changedTouches, absolute: { dx: current.screenX - this.start.screenX, dy: current.screenY - this.start.screenY, @@ -1040,11 +1137,15 @@ export default class GestureDector { if (touch?.identifier !== this.lastPanTouchID) return const current = generateCoordinate(evt.timeStamp, touch) - if (this.listenEvents.has('panmove')) { - const dx = current.screenX - this.panstart.screenX - const dy = current.screenY - this.panstart.screenY - const [_, direction] = computeDirectionFromDxDy(dx, dy) + const [direction] = computeTouchDirection(this.panstart, current) + // 更新 lastlast 值,用于触摸点间实时比对 + this.lastlast = this.last + if (this.firstDirection === null) { + this.firstDirection = direction + } + + if (this.listenEvents.has('panmove')) { this.emitEvent( 'panmove', Object.freeze({ @@ -1100,12 +1201,11 @@ export default class GestureDector { // 检测是否是第二指 const isSecondFinger = touch.identifier === 1 - // 当手指抬起时发出 swipe 事件 - // 事件中报告 起点、重点、dx、dy、dt、速度和方向 - const current = generateCoordinate(evt.timeStamp, touch) - const dx = current.screenX - this.panstart.screenX - const dy = current.screenY - this.panstart.screenY - const [angle, direction] = computeDirectionFromDxDy(dx, dy) + // touchend 中传入的 touch 和最后一次 touchmove 的 touch 触摸点是相同的 + const [direction, angle, dx, dy] = computeTouchDirection( + this.panstart, + this.last + ) if (this.listenEvents.has('panend')) { this.emitEvent( @@ -1144,8 +1244,11 @@ export default class GestureDector { } }) + // 作用域暂存 + const firstDirection = this.firstDirection // 清理资源 this.lastPanTouchID = -1 + this.firstDirection = null // 如果检测到是第2指,提前退出 if (isSecondFinger) { @@ -1165,12 +1268,23 @@ export default class GestureDector { this.getSeriesEvent('swipe').length && this.vc > this.options.velocityThreshold ) { - this.angle = angle - this.direction = direction - this.switchTo(this.swipeState, evt, touch) - } else { - this.switchTo(this.initialState) + // currentDirection 代表触摸点间的实时运动方向 + const [currentDirection] = computeTouchDirection( + this.lastlast, + this.last + ) + if ( + firstDirection && + checkIsSwipeAction(firstDirection, currentDirection, dx, dy) + ) { + this.angle = angle + this.direction = direction + this.switchTo(this.swipeState, evt, touch) + return + } } + + this.switchTo(this.initialState) }, touchcancel: null, } @@ -1360,12 +1474,10 @@ export default class GestureDector { private emitEvent( type: GestureEventType, detail?: GestureEventTypeDetail - ): void { - if (this.target === undefined) { - console.error('Attempt to emit event with no target') - return - } - // debug(type, this.target) + ) { + if (this.target === undefined) + throw new Error('Attempt to emit event with no target') + this.target.dispatchEvent( new CustomEvent(type, { bubbles: true, @@ -1427,6 +1539,33 @@ export default class GestureDector { // 使用 weakmap 默认的引用计数来清理 } + /** + * 在 GestureDector 运行时可传入可选项参数,即动态修改一些手势状态的阈值 + * + * 设置新的 option 时,会转存当前运行初始设置的 options,用于未来重置 + */ + public setOption(option: keyof GestureOptions, value: number) { + if (!this.options.hasOwnProperty(option)) throw new Error('禁止设置该参数') + this._options = this._options || this.options + this.options = {...this.options, ...{[option]: value}} + } + + /** + * 与上方 setOption 相对应,取消动态传入的参数,使用运行时初始设定的参数 + * + * 传入 option 时,可重置单个 option + * 未传入 option,则重置所有 options + */ + public unsetOption(option?: keyof GestureOptions) { + if (this._options === null) throw new Error('不存在该参数') + if (option) { + this.options[option] = this._options[option] + } else { + this.options = this._options || this.options + this._options = null + } + } + /** * Factory Function * diff --git a/src/test/panels/gesture/examples/test-multitouches.ts b/src/test/panels/gesture/examples/test-multitouches.ts index 3cd67b3..d52b212 100644 --- a/src/test/panels/gesture/examples/test-multitouches.ts +++ b/src/test/panels/gesture/examples/test-multitouches.ts @@ -11,51 +11,56 @@ export class PanelTestMultiTouches extends StarBaseElement { super() this.startGestureDetector() this.updateComplete.then(() => { - const log = (e: Event) => console.log(e.type) - const preventContextMenu = (e: Event) => { - e.stopImmediatePropagation() - if (navigator.vendor === 'Google Inc.') { - e.preventDefault() - } - } - this.holdtapButton.startGestureDetector() this.holdpanButton.startGestureDetector() this.holdswipeButton.startGestureDetector() this.holdallButton.startGestureDetector() - this.holdtapButton.addEventListener('contextmenu', preventContextMenu) - this.holdpanButton.addEventListener('contextmenu', preventContextMenu) - this.holdswipeButton.addEventListener('contextmenu', preventContextMenu) - this.holdallButton.addEventListener('contextmenu', preventContextMenu) + this.holdtapButton.addEventListener('contextmenu', this) + this.holdpanButton.addEventListener('contextmenu', this) + this.holdswipeButton.addEventListener('contextmenu', this) + this.holdallButton.addEventListener('contextmenu', this) - this.holdtapButton.addEventListener('holdstart', log) - this.holdtapButton.addEventListener('holdmove', log) - this.holdtapButton.addEventListener('holdend', log) - this.holdtapButton.addEventListener('tap', log) + this.holdtapButton.addEventListener('holdstart', this) + this.holdtapButton.addEventListener('holdmove', this) + this.holdtapButton.addEventListener('holdend', this) + this.holdtapButton.addEventListener('tap', this) - this.holdpanButton.addEventListener('holdstart', log) - this.holdpanButton.addEventListener('holdmove', log) - this.holdpanButton.addEventListener('holdend', log) - this.holdpanButton.addEventListener('panleft', log) - this.holdpanButton.addEventListener('panright', log) + this.holdpanButton.addEventListener('holdstart', this) + this.holdpanButton.addEventListener('holdmove', this) + this.holdpanButton.addEventListener('holdend', this) + this.holdpanButton.addEventListener('panleft', this) + this.holdpanButton.addEventListener('panright', this) - this.holdswipeButton.addEventListener('holdstart', log) - this.holdswipeButton.addEventListener('holdmove', log) - this.holdswipeButton.addEventListener('holdend', log) - this.holdswipeButton.addEventListener('swipeleft', log) - this.holdswipeButton.addEventListener('swiperight', log) + this.holdswipeButton.addEventListener('holdstart', this) + this.holdswipeButton.addEventListener('holdmove', this) + this.holdswipeButton.addEventListener('holdend', this) + this.holdswipeButton.addEventListener('swipeleft', this) + this.holdswipeButton.addEventListener('swiperight', this) - this.holdallButton.addEventListener('holdstart', log) - this.holdallButton.addEventListener('holdmove', log) - this.holdallButton.addEventListener('holdend', log) - this.holdallButton.addEventListener('panleft', log) - this.holdallButton.addEventListener('panright', log) - this.holdallButton.addEventListener('swipeleft', log) - this.holdallButton.addEventListener('swiperight', log) + this.holdallButton.addEventListener('holdstart', this) + this.holdallButton.addEventListener('holdmove', this) + this.holdallButton.addEventListener('holdend', this) + this.holdallButton.addEventListener('panleft', this) + this.holdallButton.addEventListener('panright', this) + this.holdallButton.addEventListener('swipeleft', this) + this.holdallButton.addEventListener('swiperight', this) }) } + handleEvent(e: Event) { + switch (e.type) { + case 'contextmenu': + e.stopImmediatePropagation() + if (navigator.vendor === 'Google Inc.') { + e.preventDefault() + } + break + default: + console.log(e.type) + } + } + @query('#holdtap') holdtapButton!: StarButton @query('#holdpan') holdpanButton!: StarButton @query('#holdswipe') holdswipeButton!: StarButton diff --git a/src/test/panels/gesture/examples/test-pan.ts b/src/test/panels/gesture/examples/test-pan.ts index 5a5ff1d..cdb6777 100644 --- a/src/test/panels/gesture/examples/test-pan.ts +++ b/src/test/panels/gesture/examples/test-pan.ts @@ -11,24 +11,20 @@ export class PanelTestPan extends StarBaseElement { super() this.startGestureDetector() this.updateComplete.then(() => { - const log = (e: Event) => console.log(e.type) - this.panlifecycleButton.startGestureDetector() this.panalldirectionButton.startGestureDetector() this.pansingledirectionButton.startGestureDetector() - this.panlifecycleButton.addEventListener('panstart', log) - this.panlifecycleButton.addEventListener('panmove', log) - this.panlifecycleButton.addEventListener('panend', log) + this.panlifecycleButton.addEventListener('panstart', this) + this.panlifecycleButton.addEventListener('panmove', this) + this.panlifecycleButton.addEventListener('panend', this) - this.panalldirectionButton.addEventListener('pan', (e: Event) => - console.log(e) - ) + this.panalldirectionButton.addEventListener('pan', this) - this.pansingledirectionButton.addEventListener('panleft', log) - this.pansingledirectionButton.addEventListener('panright', log) - this.pansingledirectionButton.addEventListener('pandown', log) - this.pansingledirectionButton.addEventListener('panup', log) + this.pansingledirectionButton.addEventListener('panleft', this) + this.pansingledirectionButton.addEventListener('panright', this) + this.pansingledirectionButton.addEventListener('pandown', this) + this.pansingledirectionButton.addEventListener('panup', this) }) } @@ -37,7 +33,7 @@ export class PanelTestPan extends StarBaseElement { @query('#pansingledirection') pansingledirectionButton!: StarButton handleEvent(e: Event) { - console.log(e) + console.log(e.type) } render() { diff --git a/src/test/panels/gesture/examples/test-swipe.ts b/src/test/panels/gesture/examples/test-swipe.ts new file mode 100644 index 0000000..f779034 --- /dev/null +++ b/src/test/panels/gesture/examples/test-swipe.ts @@ -0,0 +1,80 @@ +import {html, css, CSSResultArray} from 'lit' +import {customElement, query} from 'lit/decorators.js' +import {sharedStyles} from './shared-styles' +import {StarBaseElement} from '../../../../components/base/star-base-element' +import {StarButton} from '../../../../components/button/button' +import {UlType} from '../../../../components/ul/ul' + +@customElement('panel-test-swipe') +export class PanelTestSwipe extends StarBaseElement { + constructor() { + super() + this.startGestureDetector() + + this.updateComplete.then(() => { + this.swipeLeftRightButton.startGestureDetector() + this.swipeUpDownButton.startGestureDetector() + this.swipeAllButton.startGestureDetector() + this.swipeTestButton.startGestureDetector() + + this.swipeLeftRightButton.addEventListener('swipeleft', this) + this.swipeLeftRightButton.addEventListener('swiperight', this) + + this.swipeUpDownButton.addEventListener('swipeup', this) + this.swipeUpDownButton.addEventListener('swipedown', this) + + this.swipeAllButton.addEventListener('swipeleft', this) + this.swipeAllButton.addEventListener('swiperight', this) + this.swipeAllButton.addEventListener('swipeup', this) + this.swipeAllButton.addEventListener('swipedown', this) + + this.swipeTestButton.addEventListener('swipeleft', this) + this.swipeTestButton.addEventListener('swiperight', this) + this.swipeTestButton.addEventListener('swipeup', this) + this.swipeTestButton.addEventListener('swipedown', this) + }) + } + + @query('#swipeLeftRight') swipeLeftRightButton!: StarButton + @query('#swipeUpDown') swipeUpDownButton!: StarButton + @query('#swipeAll') swipeAllButton!: StarButton + @query('#swipeTest') swipeTestButton!: StarButton + + handleEvent(e: Event) { + console.log(e.type) + } + + render() { + return html` + + +
+ +
+ +
+ +
+ ` + } + + public static override get styles(): CSSResultArray { + return [ + sharedStyles, + css` + star-button { + height: 20vh; + } + `, + ] + } +} + +declare global { + interface HTMLElementTagNameMap { + 'panel-test-swipe': PanelTestSwipe + } +} diff --git a/src/test/panels/gesture/examples/test-tap.ts b/src/test/panels/gesture/examples/test-tap.ts index 97e0c15..efa4c5e 100644 --- a/src/test/panels/gesture/examples/test-tap.ts +++ b/src/test/panels/gesture/examples/test-tap.ts @@ -16,9 +16,9 @@ export class PanelTestTap extends StarBaseElement { this.doubletapButton.startGestureDetector() this.tripletapButton.startGestureDetector() - this.tapButton.addEventListener('tap', (e) => alert(e.type)) - this.doubletapButton.addEventListener('doubletap', (e) => alert(e.type)) - this.tripletapButton.addEventListener('tripletap', (e) => alert(e.type)) + this.tapButton.addEventListener('tap', this) + this.doubletapButton.addEventListener('doubletap', this) + this.tripletapButton.addEventListener('tripletap', this) }) } @@ -28,6 +28,7 @@ export class PanelTestTap extends StarBaseElement { handleEvent(e: Event) { console.log(e.type, (e as GestureEvent<'tap'>).detail.start) + alert(e.type) } render() { diff --git a/src/test/panels/gesture/gesture.ts b/src/test/panels/gesture/gesture.ts index 46041e9..6874279 100644 --- a/src/test/panels/gesture/gesture.ts +++ b/src/test/panels/gesture/gesture.ts @@ -6,6 +6,7 @@ import {sharedStyles} from '../shared-styles' import './examples/test-tap' import './examples/test-pan' +import './examples/test-swipe' import './examples/test-multitouches' @customElement('panel-gesture') @@ -25,6 +26,12 @@ export class PanelGesture extends LitElement { href="#test-pan" >
+ +