序言

在移动端开发中,手势操作非常常见,本篇文章主要讲解常见的 9 种手势操作原理,期间会穿插一些数学知识,将数学运用到实际问题中,数学部分可能会比较枯燥,但希望大家坚持读完,相信会收益良多。

  • 点按:tap
  • 长按:longTap
  • 双击:doubleTap
  • 双指缩放:pinch
  • 双指旋转:rotate
  • 单指缩放:singlePinch
  • 单指旋转:singleRotate
  • 滑动:swipe
  • 拖拽:drag

原理分析

所有的手势操作都是基于浏览器原生事件touchstart, touchmove, touchend, touchcancel进行上层封装。

TouchEvent 对象上有以下几个属性值,在封装手势库时会用到

  • touches 当前屏幕上的手指列表
  • targetTouches 当前元素上的手指列表
  • changedTouches 触发当前事件的手指列表
  • clientX 和 clientY 手指相对于可视区的一个坐标
  • pageX 和 pageY 手指相对于页面的一个坐标

点按

为什需要封装tap事件,而不用clcik事件?

因为click事件在移动端会有 300ms 延迟,在早期由于移动端会有双击缩放的这个操作,因此浏览器在 click 之后要等待 300ms,看用户有没有下一次点击,判断这次操作是不是双击。

为什么不用touchstarttouchend做点按操作?

因为touchstarttouchend在部分 android 机下会造成滑屏误触(在做滑动操作时touchmove会触发touchend事件)。

所以需要自定义tap事件。

原理:在点击时,记录手指坐标。抬起时,判断手指坐标和摁下的手指坐标的差值,这个差值,小于一定值时我们就认定它是点击。也就是以start时手指的坐标画一个单位圆,如果end时手指的坐标在此单位圆中,说明是点击操作)

function tap(el, fn) {let startPoint = {};el.addEventListener('touchstart', function (e) {startPoint = {x: e.changedTouches[0].pageX,y: e.changedTouches[0].pageY}});el.addEventListener('touchend', function (e) {let nowPoint = {x: e.changedTouches[0].pageX,y: e.changedTouches[0].pageY}if (Math.abs(nowPoint.x - startPoint.x) < 10&& Math.abs(nowPoint.y - startPoint.y) < 10) {fn && fn.call(el, e);}});

长按

原理:touchstart 时开启一个750毫秒的定时器,如果 750ms 内有 touchmove 或者 touchend 都会清除掉该定时器。超过 750ms 没有 touchmove 或者 touchend 就会触发 longTap

function langTap(el, fn) {let longTapTimeout = nullel.addEventListener('touchstart', function (e) {e.preventDefault()//手指数量if (e.touches.length == 1) {longTapTimeout = setTimeout(() => {fn && fn.call(el, e)}, 750)}})el.addEventListener('touchmove', function (e) {clearInterval(longTapTimeout)})el.addEventListener('touchend', function (e) {clearInterval(longTapTimeout)});}

双击

原理:在touchstart中判断两次点击的时间间隔0<time<250ms 并且判断两次按下的手指坐标的差值是否小于某个定值(此逻辑和 tap 事件一样)。如果都满足,那么就在touchend事件中触发双击。

function doubleTap(el, fn) {let last, prePoint = { X: 0, Y: 0 }, isDoubleTap = falseel.addEventListener('touchstart', function (e) {e.preventDefault()let now = Date.now()let time = now - (last || now)let currentPoint = {X: e.touches[0].pageX,Y: e.touches[0].pageY}// 判断时间差和坐标位置是否小于某个定值isDoubleTap = time > 0 && time < 250 && Math.abs(currentPoint.X - prePoint.X) < 30 && Math.abs(currentPoint.X - prePoint.Y < 30)last = Date.now()prePoint.X = currentPoint.XprePoint.Y = currentPoint.Y});el.addEventListener('touchend', function (e) {if (isDoubleTap) {// 重置状态prePoint = { X: 0, Y: 0 }isDoubleTap = falsefn && fn.call(el, e)}});
}

双指缩放

原理:在捏的过程中求两点之间的距离比值,就是缩放scale。 这个 scale 会挂载在 event 上,让用户反馈给 dom 的 transform 或者其他元素的 scale 属性

勾股定理求两点之间距离

勾股定理

已知 A,B两点的坐标(x1,y1),(x2,y2),即可根据勾股定理求出c边的长度

用代码表示

Math.sqrt((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1))

完整代码:

// 计算手指距离function getLen(v) {return Math.sqrt(v.x * v.x + v.y * v.y)}function pinch(el, fn) {let preV = { x: null, y: null }el.addEventListener('touchstart', function (e) {if (e.touches.length > 1) {preV = { x: e.touches[1].pageX - e.touches[0].pageX, y: e.touches[1].pageY - e.touches[0].pageY }}})el.addEventListener('touchmove', function (e) {e.preventDefault()if (e.touches.length > 1) {v = { x: e.touches[1].pageX - e.touches[0].pageX, y: e.touches[1].pageY - e.touches[0].pageY }if (preV.x !== null) {// 距离比值e.scale = getLen(v) / getLen(preV)fn && fn.call(el, e)}}})}let box = document.getElementById('box')pinch(box, e => {box.innerHTML = e.scale})

双指旋转

原理:双指旋转也就是求两次手势状态之间的夹角θ,和旋转方向。 那怎么求夹角和旋转方向呢?可以用向量的数量积叉乘来求夹角和方向。 先复习一下向量相关的知识。

向量的基本概念

向量:既有大小又有方向的量叫向量,记作:

或 a

单位向量: 长度为 1 的向量叫做单位向量

向量的模: 是一个标量,只有大小,没有方向,可用勾股定理求出,记为|a|

向量的坐标运算

加法运算:若a=(x1,y1),b=(x2,y2),则a+b=(x1+x2,y1+y2)

减法运算:若a=(x1,y1),b=(x2,y2),则a-b=(x1-x2,y1-y2)

数乘运算:若a=(x1,y1),b=(x2,y2),则 λa=(λx1,λy1)

向量坐标的求法:若a=(x1,y1),b=(x2,y2),则

=(x2-x1,y2-y1)

即一个向量的坐标等于此向量的有向线段的终点坐标减去始点坐标

获取向量的函数:

/**@params {Object} 始点坐标A@params {Object} 终点坐标B@returns {Object} 向量:{x,y}*/function getVector(A, B) {return { x: B.x - A.x, y: B.y - A.y }}

想更深入的了解向量运算可参考这篇文章

线性代数学习点(三):向量相加的几何表示_深入理解数字信号处理-CSDN博客_向量相加​blog.csdn.net

向量相乘

两个向量相乘得到的不是一个坐标,而是一个确定的数:a*b=x1*x2+y1*y2

向量的数乘(叉乘)

概念:一般的,规定实数λ与向量a的积是一个向量,这种运算叫做向量的数乘,记作λa,它的长度与方向规定如下:

  • |λa|=λ|a|
  • 当 λ>0 时,λa 的方向与 a 的方向相同
  • 当 λ<0 时,λa 的方向与 a 的方向方向相反

向量共线定理

概念:当且仅当有唯一一个实数λ,是b=λa,那么向量ab共线

向量共线的坐标推导

  • x1·y2-x2·y1>0,b 向量相对于 a 向量顺时针旋转
  • x1·y2-x2·y1<0,b 向量相对于 a 向量逆时针旋转
  • x1·y2-x2·y1=0,共线

通过共线定理我们可以判断出旋转的方向

向量的数量积(内积)

概念:已知两个非零向量a,b,a=(x1,y1),b=(x2,y2)。我们把数量|a||b|·cosθ叫做ab的数量积(或内积),记作a*b,即a·b=|a|·|b|·cosθ=x1*x2+y1*y2,其中θab的夹角

数量积可根据三角形的余弦定理推导出来:

由此我们可以得出

cosθ=(x1·x2+y1·y2)/(|a|·|b|)

通过向量的数量积我们可以求出旋转的角度。

完整代码为:

//根据共线定理判断方向function cross(v1, v2) {return v1.x * v2.y - v2.x * v1.y}// 勾股定理计算长度function getLen(v) {return Math.sqrt(v.x * v.x + v.y * v.y)}// 计算向量积function dot(v1, v2) {return v1.x * v2.x + v1.y * v2.y;}// 计算弧度function getAngle(v1, v2) {let mr = getLen(v1) * getLen(v2)if (mr === 0) return 0let r = dot(v1, v2) / mr  //得到弧度if (r > 1) r = 1   // Math.acos(1)=0return Math.acos(r)}// 传入两个向量function getRotateAngle(v1, v2) {let angle = getAngle(v1, v2)if (cross(v1, v2) > 0) {angle *= -1}return angle * 180 / Math.PI //弧度转角度}function rotate(el, fn) {let preV = { x: null, y: null }el.addEventListener('touchmove', function (e) {if (e.touches.length > 1) {let currentX = e.touches[0].pageX,currentY = e.touches[0].pageY,// 计算向量v = { x: e.touches[1].pageX - currentX, y: e.touches[1].pageY - currentY }// 拿到旋转角度 因为每次计算的旋转角度是上一次和当前旋转的差值,所以的到的旋转角度会比较小,注意与Math.atan2()区分e.angle = getRotateAngle(v, preV)if (preV.x !== null) {fn && fn.call(el, e)}preV.x = v.xpreV.y = v.y}})}let box = document.getElementById('box')rotate(box, e => {box.innerHTML = e.angle})

单指缩放单指旋转都需要依赖于操作元素的基准点(操作元素的中心点)进行计算

单指缩放

由 a 向量单指放大到 b 向量,对元素进行了中心放大,此时缩放值即为 b 向量的模 / a 向量的模。和缩放手势原理相同。

单指旋转

和双指旋转一样,θ就是我们要求的角度

单指缩放,单指旋转,多用于处理图片场景当中,比如说在 canvas 画布当中给图片添加水印或文字。

滑动(swipe)

滑动这个操作很有意思,它正好和 点按手势相反,需要当 touchstart 的手的坐标和 touchend 时候手的坐标 x、y 方向偏移要大于 30,然后再去判断用户到底是从上到下,还是从下到上,或者从左到右、从右到左滑动。

比较横纵坐标的绝对值,然后再根据某以方向的坐标判断出上下滑动还是左右滑动

Math.abs(x1 - x2) >= Math.abs(y1 - y2) ? (x1 - x2 > 0 ? 'Left' : 'Right') : (y1 - y2 > 0 ? 'Up' : 'Down')

完整代码:

function swipeDirection(x1, x2, y1, y2) {return Math.abs(x1 - x2) >= Math.abs(y1 - y2) ? (x1 - x2 > 0 ? 'Left' : 'Right') : (y1 - y2 > 0 ? 'Up' : 'Down')}function swipe(el, fn) {let startPoint = {};el.addEventListener('touchstart', function (e) {e.preventDefault();startPoint = {x: e.changedTouches[0].pageX,y: e.changedTouches[0].pageY}});el.addEventListener('touchend', function (e) {let nowPoint = {x: e.changedTouches[0].pageX,y: e.changedTouches[0].pageY}if (Math.abs(nowPoint.x - startPoint.x) > 10 && Math.abs(nowPoint.y - startPoint.y) > 10) {e.direction = swipeDirection(startPoint.x, nowPoint.x, startPoint.y, nowPoint.y);fn && fn.call(el, e);}});}let box = document.getElementById('box')swipe(box, e => {box.innerHTML = e.direction})

拖拽

touchmove把每次移动的距离deltaX,deltaY,挂载在 event 上。拿到移动距离+=就能实现一个简单的拖拽

div {width: 200px;height: 200px;border: 1px solid sienna;background: saddlebrown;}
<div id="box">拖拽此物体</div>
function drag(el, fn) {let x2 = null, y2 = nullel.addEventListener('touchmove', function (e) {e.preventDefault()let currentX = e.touches[0].pageX, currentY = e.touches[0].pageYif (x2 !== null || y2 !== null) {e.deltaX = currentX - x2e.deltaY = currentY - y2fn && fn.call(el, e)}x2 = currentXy2 = currentY})el.addEventListener('touchend', function (e) {x2 = nully2 = null})}let box = document.getElementById('box')box.style.transform = `translate3d(0,0,0)`drag(box, e => {let translates = getComputedStyle(box, null).transformlet x = parseFloat(translates.substring(6).split(',')[4]) //解析x轴数值let y = parseFloat(translates.substring(6).split(',')[5]); //解析y轴数值box.style.transform = `translate3d(${x += e.deltaX}px,${y += e.deltaY}px,0)`})

同时支持触摸事件和鼠标事件

虽然说触摸事件和鼠标事件很相似,不过二者仍然需要分开处理。假如想让应用程序同时运行在桌面浏览器与手机浏览器之中,那么必须将触摸事件于鼠标事件同等对待。把事件处理逻辑封装在同一系列方法当中。这些方法不需要知道到底是触摸事件还是鼠标事件。

ul {width: 200px;height: 361px;cursor: pointer;position: absolute;top: 0px;left: 0px;background: #787878;}
<ul id="ul1">拖拽 </ul>
let oUl = document.getElementById('ul1')let disX = 0;let offsetLeft = 0oUl.onmousedown = function (ev) {mouseDownOrTouchStart(ev.pageX)oUl.onmousemove = function (ev) {mouseMoveOrTouchMove(ev.pageX)}oUl.onmouseup = function (ev) {oUl.onmousemove = nulloUl.onmouseup = null}}oUl.ontouchstart = function (ev) {mouseDownOrTouchStart(ev.touches[0].pageX)}oUl.ontouchmove = function (ev) {mouseMoveOrTouchMove(ev.touches[0].pageX)}// 代理function mouseDownOrTouchStart(pageX) {disX = pageXoffsetLeft = oUl.offsetLeft}function mouseMoveOrTouchMove(pageX) {oUl.style.left = pageX - disX + offsetLeft + 'px'}

以上demo都放到github上啦,感兴趣的可以加个star~

https://github.com/wensiyuanseven/gesture-dem

ios 滑动手势事件 与cell touchevent事件_深入浅出~手势操作原理分析相关推荐

  1. Android 利用源码调试 详解TouchEvent 事件分发机制

    1.如果有触摸事件,首先会调用到Activity 的dispatchTouchEvent 方法. public boolean dispatchTouchEvent(MotionEvent ev) { ...

  2. iOS事件全面解析 (触摸事件、手势识别、摇晃事件、耳机线控)

    -- iOS事件全面解析 概览 iPhone的成功很大一部分得益于它多点触摸的强大功能,乔布斯让人们认识到手机其实是可以不用按键和手写笔直接操作的,这不愧为一项伟大的设计.今天我们就针对iOS的触摸事 ...

  3. 转载大神IOS开发系列【9】--触摸事件、手势识别、摇晃事件、耳机线控

    转载自:http://www.cnblogs.com/kenshincui/p/3950646.html 概览 iPhone的成功很大一部分得益于它多点触摸的强大功能,乔布斯让人们认识到手机其实是可以 ...

  4. vue移动端点击事件延迟_解决Vue 界面在苹果手机上滑动点击事件等卡顿问题_莺语_前端开发者...

    用 (1).滑动页面卡顿, (2).点击事件响应缓慢,百度才发现在苹果手机上有300ms的延迟. 一.滑动页面卡顿 //页面布局 页面内容 在对应的组件的最外层div上加上这样的样式: .conten ...

  5. 用 JavaScript 实现手势库 — 事件派发与 Flick 事件【前端组件化】

    前端<组件化系列>目录 「一」用 JSX 建立组件 Parser(解析器) 「二」使用 JSX 建立 Markup 组件风格 「三」用 JSX 实现 Carousel 轮播组件 「四」用 ...

  6. 完全理解Android TouchEvent事件分发机制(一)

    本文能给你带来和解决一些你模糊的Touch事件概念及用法 1.掌握View及ViewGroup的TouchEvent事件分发机制 2.为解决View滑动冲突及点击事件消费提供支持 3.为你解决面试中的 ...

  7. ionic3中的gestures 手势事件- ioni3c长按事件

    原文出处: http://www.ionic.wang/article-index-id-83.html ionic3中的gestures 手势事件如下: ionic3中的gestures 手势事件包 ...

  8. iOS:触摸控件UITouch、事件类UIEvent

    UITouch:触摸控件类   UIEvent:事件类 ❤️❤️❤️UITouch的介绍❤️❤️❤️ 一.触摸状态类型枚举 typedef NS_ENUM(NSInteger, UITouchPhas ...

  9. Android官方开发文档Training系列课程中文版:手势处理之ViewGroup的事件管理

    原文地址:https://developer.android.com/training/gestures/viewgroup.html 在ViewGroup中处理触摸事件要格外小心,因为在ViewGr ...

最新文章

  1. springboot整合Druid使用
  2. Openldap部署LDAP服务器平台
  3. Nginx报错:nginx: [emerg] open() “/usr/local/nginx/../conf/nginx.conf“ failed (2: No such file or direc
  4. python pip配置镜像源:douban不能下载aliyun可以下载
  5. SqlConnetction类
  6. 整理几个常用的sql和其他代码
  7. python如何调用阿里云接口_Python调用阿里云API接口实现自定义功能【二】——DescribeInstance窗口操作...
  8. js控制ctrl+p
  9. ikuai与AZ 组SDWAN
  10. matlab怎么定义矩阵变量_MATLAB符号计算(收藏版)
  11. 在机自学院自强队的这一年
  12. QPM-PHP多进程开发-Supervisor配置参考
  13. [转载]Michael Peng:北美求职记
  14. Could not find resource xxx/xxxx/xxx.xml报错解决
  15. 12月DB-Engines数据库排名,你猜谁会是第一?
  16. Google Chrome浏览器调整分辨率的插件
  17. 高德导航显示白屏的问题
  18. Servlet[SpringMVC]的Servlet.init()引发异常
  19. Windows下进程占用CPU过大的解决方案
  20. mysql二进制日志

热门文章

  1. 软考信息安全工程师备考笔记3:第三章网络安全基础备考要点
  2. Kali Linux 从入门到精通(三)-入侵系统定制
  3. JAVA8的新特性之函数式接口
  4. 转载:GBDT算法梳理
  5. uva 436 Arbitrage (II)
  6. Hadoop ecosystem
  7. 使用Tesseract (OCR)实现简单的验证码识别(C#)+窗体淡入淡出效果
  8. 卷积神经网络之AlexNet
  9. jQuery DOM/属性/CSS操作
  10. OpenStack 集群部署工具:ProStack