TASK:#109167 (feature)(bugfix)(improve)allow setOption dynamically & fix swipe trigger & optimize functional code

This commit is contained in:
wangchangqi 2022-09-29 16:29:19 +08:00
parent b3631c7965
commit f64ace31c3
6 changed files with 345 additions and 117 deletions

View File

@ -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 = <T extends Touch>(
})
}
/**
* swipe
*
* swipe
* dxdydtvc direction
*
*
* 1.( swipe )
*
* !------>
* 2.( swipe )
*
* !-------
* <---|
* 3.( swipe )
*
* !----
* <------|
* 4.1~3
* 5.1~3
*/
const checkIsSwipeAction = <D extends PanOrSwipeDirection, N extends number>(
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 = <T extends Touch>(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 = <T extends number>(dx: T, dy: T) =>
(Math.atan2(dy, dx) * 180) / Math.PI
/**
*
*
@ -340,10 +416,9 @@ const computeDistance = <T extends Touch>(t1: T, t2: T): number => {
* : (-180, 180]
*/
const computeDirection = <T extends Touch>(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 = <T extends GestureCoordinate>(
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<GestureOptions>) {
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<GestureEventType>
): 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
*

View File

@ -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

View File

@ -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() {

View File

@ -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`
<star-ul type=${UlType.BASE}>
<star-button id="swipeLeftRight" label="左右轻扫"></star-button>
<hr />
<star-button id="swipeUpDown" label="上下轻扫"></star-button>
<hr />
<star-button id="swipeAll" label="上下左右轻扫"></star-button>
<hr />
<star-button
id="swipeTest"
label="向右轻扫再折返一小段/一大段, 观察this)"
></star-button>
</star-ul>
`
}
public static override get styles(): CSSResultArray {
return [
sharedStyles,
css`
star-button {
height: 20vh;
}
`,
]
}
}
declare global {
interface HTMLElementTagNameMap {
'panel-test-swipe': PanelTestSwipe
}
}

View File

@ -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() {

View File

@ -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"
></star-li>
<hr />
<star-li
type=${LiType.ONLY_LABEL}
label="swipe轻扫"
href="#test-swipe"
></star-li>
<hr />
<star-li
type=${LiType.ONLY_LABEL}
label="手指组合"