移动端手势封装

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/riddle1981/article/details/82760968

目录概要

  • 手势封装的兼容问题与解决方案
  • TouchEvent事件详解(译自W3C)
  • 手势封装的实现

一、手势封装的兼容实验
在微信浏览器及手机自带浏览器当中的自带手势会影响事件的触发
微信的自带手势包括但不仅限于

  • 页面顶部下拉刷新
  • 长按弹出菜单
  • 上下滑动翻动页面
  • 双指缩放

在有默认手势时可能出现的问题有无法正常触发touchend事件,使用小米、vivo、oppo、苹果进行实验,实验结果如下:
1、下滑手势中小米和oppo不触发touchend事件触发下拉刷新,苹果和vivo均触发touchend,但vivo不触发下拉刷新。
2、上滑、左滑、右滑手势中均正常触发touchend事件
3、长按时oppo和小米不触发touchend并弹出系统菜单,vivo不触发touchend且不弹出系统菜单,苹果触发touchend且不弹出系统菜单但默认选中最后一行文字并弹出复制菜单
4、缩小时四款机型均正常触发touchend,,苹果有缩小再弹开的动画,其他机型无
5、放大和放大后再缩小四款机型均可以获取到touchend且缩放效果正常
6、旋转时touchend触发正常,苹果有缩小再弹开的动画,其他机型无
7、以单击形式触屏时四款手机均以touchstart、touchend、mousedown、mouseup的顺序触发事件。再加入mousemove监听后小米、vivo、oppo以touchstart、touchend、mousemove、mousedown、mouseup的顺序触发事件。苹果触发touchstart、touchend、mousemove顺序触发事件且不再触发mousedown和mouseup
8、长按时触发touchcancel的timeStamp与该触点的touchstart的timeStamp相同,下滑触发cancel时timeStamp为实际值

此时的解决方式有两种,一是禁止默认事件,但是这样影响比较大,而是使用touchcancel使得在浏览器触发默认事件后仍可以监听自己封装的手势

1、禁止浏览器默认事件
默认情况下,平移(滚动)和缩放手势由浏览器专门处理。 使用 Pointer_events 的应用程序将在浏览器开始处理触摸手势时收到一个 pointercancel 事件。 通过明确指定浏览器应该处理哪些手势,应用程序可以在 pointermove 和 pointerup 监听器中为其余的手势提供自己的行为。 使用 Touch_events 的应用程序通过调用 preventDefault() 禁用浏览器处理手势,但也应使用触摸操作确保浏览器在调用任何事件侦听器之前,了解应用程序的意图。

当手势开始时,浏览器与触摸的元素及其所有祖先的触摸动作值相交直到一个实现手势(换句话说,第一个包含滚动元素)的触摸动作值。 这意味着在实践中,触摸动作通常仅适用于具有某些自定义行为的单个元素,而无需在该元素的任何后代上明确指定触摸动作。 手势开始之后,触摸动作值的更改将不会对当前手势的行为产生任何影响。

  • css方面的默认事件禁用
// 禁止用户选择复制
user-select: none;
// 指定某个给定的区域是否允许用户操作
touch-action: none;
  • js方面的禁用

在需要禁用事件上使用preventDefault()来禁止默认事件

  target.addEventListener('touchstart', e => {
     e.preventDefault()
  })

2、使用touchcancel的方式结局兼容性问题
先来看看w3c中关于touchcancel的描述

用户代理必须发送此事件类型, 以指示触点在特定于实现的方式中中断时, 例如来自 UA 取消触摸的同步事件或操作, 或将文档窗口保留到可以处理用户交互的非文档区域。(例如, UA 的本地用户界面、插件)当用户在触摸面上放置更多触摸点时, 用户代理也可以发送此事件类型, 而不是将设备或实现配置为存储, 在这种情况下, TouchList中最早的触摸对象应删除。
该事件的目标必须是与第一次放置在接触面上的触摸点相同的元素,即使触摸点已经移动到目标元素的交互区域之外。
任何被移除的触点都应当包含在TouchEvent的changedTouches属性当中。且不应当包含在touches属性和targetTouches属性当中。

因此如果浏览器默认事件使得touchend事件没有触发则会发送touchcancel用来表示原油touch事件的监听实现中段。然后将可能会监听不到的touchend的处理放入touchcancel当中就可以判定用户操作了

再解决完以上问题后就可以再各种情况下都能够监听到用户的手势了,具体封装代码请点击 手势封装方法


二、TouchEvent
1、touchEvent规范(译自W3C的TouchEvent部分)
Touches接口
此接口描述触摸事件的单个触点。触摸对象是不可变的;创建一个后,其属性不能更改。

只读属性长标识符
每个触点的标识号。当触点变为活动状态时, 必须为其分配一个与任何其他活动触点不同的标识符。当触点保持活动状态时, 引用它的所有事件都必须为其分配相同的标识符.

只读属性 EventTarget 目标
当触点在第一次放置在表面上时开始的 EventTarget, 即使在该元素的交互式区域之外,触头也已移动。

只读属性双 screenX
相对于屏幕的点的水平坐标 (以像素为单位)

只读属性双 screenY
点相对于屏幕的垂直坐标 (以像素为单位)

只读属性双 clientX
点相对于视区的水平坐标 (以像素为单位), 不包括任何滚动偏移量

只读属性双 clientY
点相对于视区的垂直坐标 (以像素为单位), 不包括任何滚动偏移量

只读属性双 pageX
点相对于视区的水平坐标 (以像素为单位), 包括任何滚动偏移量

只读属性双 pageY
点相对于视区的垂直坐标 (以像素为单位), 包括任何滚动偏移量

TouchList接口
此接口定义touchstart、touchend、touchmove、touchcancel事件类型。TouchEvent对象是不可变的;创建并初始化一个后,其属性不能更改。

只读属性touches
接触面上每一个触点的Touches对象信息列表

只读属性targetTouches
接触面上每一个开始于目标元素的触点的Touches对象信息列表

只读属性changedTouches
与本事件触发相关的触点的Touches对象信息列表
对于touchstart事件, 这必须是刚刚成为当前事件激活状态的触点列表。对于touchmove事件, 这必须是自上次事件以来移动的触摸点列表。对于touchend和touchcancel事件, 这必须是刚刚从曲面中移除的触摸点的列表。

touchEvent事件
1、touchstart事件
用户代理必须发送此事件类型,以芝士用户何时在触摸屏上放置触摸点。
此事件的目标必须是元素,如果触点谓语框架内,则应将该事件分派到子浏览上下文的框架
如果在此事件上调用preventDefault方法则它应该防止与同一个活动触摸点关联的任何触摸事件导致的任何默认操作,包括鼠标事件或滚动

2、touchend事件
用户代理必须发送此事件类型,以指示用户合适从触摸屏上删除触点,也包括点物理离开触屏的情况,如被拖离屏幕。
该事件的目标必须是与第一次放置在接触面上的触摸点相同的元素,即使触摸点已经移动到目标元素的交互区域之外。
任何被移除的触点都应当包含在TouchEvent的changedTouches属性当中。且不应当包含在touches属性和targetTouches属性当中。
如果此事件被取消,则任何包含此事件的事件序列都不应该作为click被解释。

3、touchmove事件
用户代理必须发送此事件类型,以只是用户合适沿触摸屏移动触摸点。
该事件的目标必须是与第一次放置在接触面上的触摸点相同的元素,即使触摸点已经移动到目标元素的交互区域之外。
注意,用户代理发送触摸移动事件的速率是由实现定义的,并且可能取决于硬件能力和其他实现细节。
用户代理应当阻止由于任何touchmove事件触发的默认行为直到至少一个与相同活动触点关联的touchmove事件不被取消为止。在至少一个与相同活动触点相关联的touchmove事件没有被取消之后,touchmove事件的的默认事件是否被取消取决于实现。

4、touchcancel事件
用户代理必须发送此事件类型, 以指示触点在特定于实现的方式中中断时, 例如来自 UA 取消触摸的同步事件或操作, 或将文档窗口保留到可以处理用户交互的非文档区域。(例如, UA 的本地用户界面、插件)当用户在触摸面上放置更多触摸点时, 用户代理也可以发送此事件类型, 而不是将设备或实现配置为存储, 在这种情况下, TouchList中最早的触摸对象应删除。
该事件的目标必须是与第一次放置在接触面上的触摸点相同的元素,即使触摸点已经移动到目标元素的交互区域之外。
任何被移除的触点都应当包含在TouchEvent的changedTouches属性当中。且不应当包含在touches属性和targetTouches属性当中。

5、与鼠标事件的交互
用户代理可能会在用户操作想用的时候同时调度touch事件和mouse事件。如果用户代理在单一的用户操作中同时调度touch事件和mouse事件,那么touchstart应当在任何鼠标事件调度之前被调度。如果调用了touchstart、touchmove或touchend的prevebtDefault方法,那么用户代理不应当调度任何作为被阻止作为touch事件结果的mouse事件
如果用户代理以单击方式触发一系列touch事件,那么它应当在触摸事件相应的位置触发mousemove, mousedown, mouseup, 和click 事件(以此顺序)。如果在触摸事件期间文档内容发生改变,则用户代理可以将鼠标事件分派到与touch事件不同的目标。
默认行为和touch事件和鼠标事件的的序列都由实现定义,除非另有指定。


三、手势封装实现
因为第一次做封装所以感觉在代码易用性和可复用性方面都有待提高,会在后续不断完善和改进。目前支持的手势包括长按,双击、滑动(四个方向的滑动)、缩放(放大、缩小)、旋转,使用方法为:
将代码加载后使用gesture()函数来进行事件的绑定和监听。以下为使用方法示例

let target = document.getElementById('test')
//绑定并监听目标target的左滑事件,并传入回调函数
gesture('slideLeft', target, (e) => {
    console.log(e)
})
//绑定并监听目标target的所有已支持的手势事件,并传入回调函数
gesture('all', target, (e) => {
    console.log(e)
})
//输出错误提示:undefined eventName a, Please input correct eventName
gesture('a', target, (e) => {
    console.log(e)
})

目前支持的手势名为:
slideLeft:左滑
slideRight:右滑
slideTop:上滑
slideBottom:下滑
longTouch:长按 (时长认定为350ms)
dbTouch:连点(手机不支持原生的dblclick)
largeScale:放大
smallScale:缩小
rotate:旋转

代码实现如下:

let dbTime = 200
let longTime = 350
let slideDistance = 10
let start = []
let end = []
let startCount = 0
let finger =    null
let startStamp = null
let endStamp = null
let info = {
    //save what you want to get
    timeStart: null,
    timeEnd: null,
    rotate: false,
    scale: false
}
let slideLeft = new CustomEvent('slideLeft', {
    detail: info
})
let slideRight = new CustomEvent('slideRight', {
    detail: info
})
let slideTop = new CustomEvent('slideTop', {
    detail: info
})
let slideBottom = new CustomEvent('slideBottom', {
    detail: info
})
let longTouch = new CustomEvent('longTouch', {
    detail: info
})
let dbTouch = new CustomEvent('dbTouch', {
    detail: info
})
let largeScale = new CustomEvent('largeScale', {
    detail: info
})
let smallScale = new CustomEvent('smallScale', {
    detail: info
})
let rotate = new CustomEvent('rotate', {
    detail: info
})
function gestureDefine(target) {
    target.addEventListener('touchstart', e => {
        let tempStart = [...e.changedTouches]
        //timeStamp
        startStamp = e.timeStamp
        if(!info.timeStart) {
            info.timeStart = Date.now()
        }
        if(info.timeEnd && startStamp - endStamp < dbTime) {
            target.dispatchEvent(dbTouch)
        }
        //init start
        let temps = {}
        temps.x = tempStart[0].screenX
        temps.y = tempStart[0].screenY
        temps.id = tempStart[0].identifier
        start.push(temps)
        //init end
        end = []
        startCount++
    })
    target.addEventListener('touchend', e => {
        startCount--
        endStamp = e.timeStamp
        //get event information
        info.timeEnd = Date.now()
        //static valuation
        let tempEnd = [...e.changedTouches]
        let tempe = {}
        tempe.x = tempEnd[0].screenX
        tempe.y = tempEnd[0].screenY
        tempe.id = tempEnd[0].identifier
        end.push(tempe)
        if(end.length == start.length && end.length == 2) {
            let arrStart = []
            let arrEnd = []
            for(let i = 0; i < end.length; i++) {
                arrStart[start[i].id] = start[i]
                arrEnd[end[i].id] = end[i]
            }
            let startDistance = getDistance(arrStart[0], arrStart[1])
            let endDistance = getDistance(arrEnd[0], arrEnd[1])
            info.scale = endDistance / startDistance
            info.rotate = getRotate(arrStart, arrEnd)
            //judge rotate
            if(info.rotate) {
                target.dispatchEvent(rotate)
            } 
            if(info.scale > 1.15) {
                target.dispatchEvent(largeScale)
            } else if(info.scale < 0.95) {
                target.dispatchEvent(smallScale)
            }
            if(startCount == 0) {
                start = []
            }
            return
        }
        if(startCount !== 0) {
            return 
        }    
        //slideEvent
        info.rotate = false
        info.scale = false
        if(tempEnd.length === 1) {
            let x = tempEnd[0].screenX
            let y = tempEnd[0].screenY
            let direction = Math.abs(x - start[0].x) > Math.abs(y - start[0].y) ? 'x' : 'y'
            if(Math.abs(x - start[0].x) < 5 && Math.abs(y - start[0].y) < 5) {
                if(endStamp - startStamp > longTime) {
                    target.dispatchEvent(longTouch)
                }
                return
            }
            if (direction == 'x') {
                if(x > start[0].x) {
                    target.dispatchEvent(slideRight)
                } else {
                    target.dispatchEvent(slideLeft)
                }
            } else if (direction == 'y') {
                if(y > start[0].y) {
                    target.dispatchEvent(slideBottom)
                } else {
                    target.dispatchEvent(slideTop)
                }
            }
        } 
        if(startCount == 0) {
            start = []
        }
    })
   
} )
}
function getRotate(start, end) {
    let core = getCore(start, end)
    if(!core.contain){
        return false
    }
    return core.rotate
    
}

function getCore(start, end) {
    let trans = 180 / Math.PI
    let k1 = (start[0].y - start[1].y) / (start[0].x - start[1].x)
    let b1 = start[0].y - k1 * start[0].x
    let k2 = (end[0].y - end[1].y) / (end[0].x - end[1].x)
    let b2 = end[0].y - k2 * end[0].x
    let a = Math.atan(-k1) * trans
    let b = Math.atan(-k2) * trans
    let core = {}
    core.x = (b2 - b1) / (k1 - k2)
    core.y = (k2 * b1 - k2 * b2) / (k2 - k1)
    core.rotate = Math.round(a - b)
    let direct = (start[0].x - core.x) / (core.x - start[1].x)
    if(direct > 0) {
        core.contain = true
    } else {
        core.contain = false
    }
    return core
}

function getDistance(start, end) {
    return Math.pow(Math.pow(start.x - end.x, 2) + Math.pow(start.y - end.y, 2), 1/2)
}

function gesture(name, target, callback) {
    let eventList = [
        'slideLeft',
        'slideRight',
        'slideTop',
        'slideBottom',
        'longTouch',
        'dbTouch',
        'largeScale',
        'smallScale',
        'rotate',
        'all'
    ]
    if(eventList.indexOf(name) === -1) {
        console.log('undefined eventName "' + name + '", Please input correct eventName')
        return
    }
    gestureDefine(target)
    if(name == 'all') {
        target.addEventListener('slideLeft', callback)
        target.addEventListener('slideRight',callback)
        target.addEventListener('slideTop', callback)
        target.addEventListener('slideBottom', callback)
        target.addEventListener('longTouch', callback)
        target.addEventListener('dbTouch', callback)
        target.addEventListener('largeScale', callback)
        target.addEventListener('smallScale', callback)
        target.addEventListener('rotate', callback)
    } else {
        target.addEventListener(name, callback)
    }
}

猜你喜欢

转载自blog.csdn.net/riddle1981/article/details/82760968