touch事件为h5为移动端提供的事件。
touchstart
手指开始触摸的时候触发,类似于mousedowm
touchmove
手指移动的时候触发,类似于mousemove
touchend
手指离开时触发,类似于mouseup
touch事件的event对象里面的touches是一个坐标对象组成的数组client,screen,page
Touch事件
最基本的touch事件包括4个事件:
touchstart:
当在屏幕上按下手指时触发touchmove:
当在屏幕上移动手指时触发touchend:
当在屏幕上抬起手指时触发touchcancel:
当一些更高级别的事件发生的时候(如电话接入或者弹出信息)会取消当前的touch操作,即触发touchcancel。一般会在touchcancel时暂停游戏、存档等操作。 与移动端相关的interface主要有三个:
TouchEvent:
表示触摸状态发生改变时触发的eventTouch:
表示用户和触屏设备之间接触时单独的交互点(a single point of contact)TouchList:
表示一组touches。当发生多点触摸的时候才用的到。touchstart:
当用户手指触摸到的触摸屏的时候触发。事件对象的 target 就是touch 发生位置的那个元素。touchmove:
当用户在触摸屏上移动触点(手指)的时候,触发这个事件。一定是先要触发touchstart事件,再有可能触发 touchmove 事件。
touchmove 事件的target 与最先触发的 touchstart 的 target 保持一致。touchmove事件和鼠标的mousemove事件一样都会多次重复调用,所以,事件处理时不能有太多耗时操作。不同的设备,移动同样的距离 touchmove 事件的触发频率是不同的。
有一点需要注意:即使手指移出了 原来的target 元素,则 touchmove 仍然会被一直触发,而且 target 仍然是原来的 target 元素。touchend:
当用户的手指抬起的时候,会触发 touchend 事件。如何用户的手指从触屏设备的边缘移出了触屏设备,也会触发 touchend 事件。
touchend 事件的 target 也是与 touchstart 的 target 一致,即使已经移出了元素。touchcancel:
当触点由于某些原因被中断时触发。有几种可能的原因如下(具体的原因根据不同的设备和浏览器有所不同):
由于某个事件取消了触摸:例如触摸过程被一个模态的弹出框打断。
触点离开了文档窗口,而进入了浏览器的界面元素、插件或者其他外部内容区域。
当用户产生的触点个数超过了设备支持的个数,从而导致 TouchList 中最早的 Touch对象被取消
touchcancel 事件一般用于保存现场数据。比如:正在玩游戏,如果发生了 touchcancel 事件,则应该把游戏当前状态相关的一些数据保存起来。TouchList详解
一个TouchList代表一个触摸屏幕上所有触点的列表。
举例来讲, 如果一个用户用三根手指接触屏幕(或者触控板), 与之相关的TouchList 对于每根手指都会生成一个 Touch对象, 共计 3 个.只读属性:length:
返回这个TouchList中Touch对的个数。(就是有几个手指接触到了屏幕)方法:identifiedTouch()
返回值是在TouchList中的第 1 个 Touch 对象,已经被标记为过时,虽然有的浏览器还可以使用,但是不建议使用。方法:item(index)
返回TouchList中指定索引的Touch对象。changedTouches:(只读)
这个 TouchList对象列出了和这个触摸事件对应的那些发生了变化的 Touch 对象。
对于 touchstart 事件, 这个 TouchList 对象列出在此次事件中新增加的触点。
对于 touchmove 事件,列出和上一次事件相比较,发生了变化的触点。
对于 touchend 列出离开触摸平面的触点(这些触点对应已经不接触触摸平面的手指)targetTouches:(只读)
这个TouchList列出了那些 touchstart发生在这个元素,并且还没有离开 touch surface 的touch point(手指)。是touches的一个严格子集。touches`:(只读)
这个 TouchList 列出了事件触发时: touch suface上所有的 touch point。identifier:
表示每 1 个 Touch 对象 的独一无二的 identifier。有了这个 identifier 可以确保你总能追踪到这个 Touch对象。
screenX:
触摸点相对于屏幕左边缘的 x 坐标。
screenY:
触摸点相对于屏幕上边缘的 y 坐标。
clientX:
触摸点相对于浏览器的 viewport左边缘的 x 坐标。不会包括左边的滚动距离。
clientY:
触摸点相对于浏览器的 viewport上边缘的 y 坐标。不会包括上边的滚动距离。
pageX:
触摸点相对于 document的左边缘的 x 坐标。 与 clientX 不同的是,他包括左边滚动的距离,如果有的话。
pageY:
触摸点相对于 document的左边缘的 y 坐标。 与 clientY 不同的是,他包括上边滚动的距离,如果有的话。
target:
总是表示 手指最开始放在触摸设备上的触发点所在位置的 element。 即使已经移出了元素甚至移出了document, 他表示的element仍然不变封装移动端事件
(function (window){ //传入window,提高变量的查找效率 function myQuery(selector){ //这个函数就是对外提供的接口。 //调用这个函数的原型对象上的_init方法,并返回 return myQuery.prototype._init(selector); } myQuery.prototype = { /*初始化方法,获取当前query对象的方法*/ _init: function (selector){ if (typeof selector == "string"){ //把查找到的元素存入到这个原型对象上。 this.ele = window.document.querySelector(selector); //返回值其实就是原型对象。 return this; } }, /*单击事件: * 为了规避click的300ms的延迟,自定义一个单击事件 * 触发时间: * 当抬起手指的时候触发 * 需要判断手指落下和手指抬起的事件间隔,如果小于500ms表示单击时间。 * * 如果是大于等于500ms,算是长按时间 * */ tap: function (handler){ this.ele.addEventListener("touchstart", touchFn); this.ele.addEventListener("touchend", touchFn); var startTime, endTime; function touchFn(e){ e.preventDefault() switch (e.type){ case "touchstart": startTime = new Date().getTime(); break; case "touchend": endTime = new Date().getTime(); if (endTime - startTime < 500){ handler.call(this, e); } break; } } }, /** * 长按 * @param handler */ longTag: function (handler){ this.ele.addEventListener("touchstart", touchFn); this.ele.addEventListener("touchmove", touchFn); this.ele.addEventListener("touchend", touchFn); var timerId; function touchFn(e){ switch (e.type){ case "touchstart" : //500ms之后执行 timerId = setTimeout(function (){ handler.call(this, e); }, 500) break; case "touchmove" : //如果中间有移动也清除定时器 clearTimeout(timerId) break; case "touchend" : //如果在500ms之内抬起了手指,则需要定时器 clearTimeout(timerId); break; } } }, /** * 左侧滑动。 记录手指按下的左边,在离开的时候计算 deltaX是否满足左滑的条件 * */ slideLeft: function (handler){ this.ele.addEventListener("touchstart", touchFn); this.ele.addEventListener("touchend", touchFn); var startX, startY, endX, endY; function touchFn(e){ e.preventDefault(); var firstTouch = e.changedTouches[0]; switch (e.type){ case "touchstart": startX = firstTouch.pageX; startY = firstTouch.pageY; break; case "touchend": endX = firstTouch.pageX; endY = firstTouch.pageY; //x方向移动大于y方向的移动,并且x方向的移动大于25个像素,表示在向左侧滑动 if (Math.abs(endX - startX) >= Math.abs(endY - startY) && startX - endX >= 25){ handler.call(this, e); } break; } } }, /** * 右侧滑动。 * */ rightLeft: function (e){ //TODO: } } window.$ = window.myQuery = myQuery; })(window); $("div").tap(function (e){ console.log("单击事件") }) $("div").longTag(function (){ console.log("长按事件"); }) $("div").slideLeft(function (e){ console.log(this); this.innerHTML = "左侧滑动了....." })
touch事件为h5为移动端提供的事件。
touchstart
手指开始触摸的时候触发,类似于mousedowm
touchmove
手指移动的时候触发,类似于mousemove
touchend
手指离开时触发,类似于mouseup
touch事件的event对象里面的touches是一个坐标对象组成的数组client,screen,page
转载于:https://www.cnblogs.com/chairs/p/7056627.html
Cocos Creator Touch 事件
cc.Node 有一套完整的事件监听和分发机制。在这套机制之上,提供了一些基础的节点相关的系统事件。因此,在cocos creator中touch事件是与Node节点息息相关的。
// 使用枚举类型来注册 node.on(cc.Node.EventType.TOUCH_START, function (event) { console.log('Touch Start'); event.getID(); //Touch事件的ID event.getLocation(); //Touch事件的手指位置 event.getLocationX(); //获取X轴位置 event.getLocationY(); //获取触点的 Y 轴位置 event.getPreviousLocation(); //获取触点上一次触发事件时的位置对象,对象包含 x 和 y 属性 event.getStartLocation(); //获取触点初始时的位置对象,对象包含 x 和 y 属性 event.getDelta(); //获取触点距离上一次事件移动的距离对象,对象包含 x 和 y 属性 }, this); // 使用事件名来注册 node.on('touchstart', function (event) { console.log('Touch Start'); event.getID(); //Touch事件的ID event.getLocation(); //Touch事件的手指位置 event.getLocationX(); //获取X轴位置 event.getLocationY(); //获取触点的 Y 轴位置 event.getPreviousLocation(); //获取触点上一次触发事件时的位置对象,对象包含 x 和 y 属性 event.getStartLocation(); //获取触点初始时的位置对象,对象包含 x 和 y 属性 event.getDelta(); //获取触点距离上一次事件移动的距离对象,对象包含 x 和 y 属性 }, this);
系统提供的触摸事件类型如下:
Unity3D中Touch事件是以屏幕为基准的,与某个节点无关,全局的Touch事件都统计在Input中,
可以在update 中监听事件,Unity3D商店中有不少再次封装的Touch事件插件,比如EasyTouch。示例代码来自官方文档
void Update()
{
// Handle screen touches.
if (Input.touchCount > 0)
{
Touch touch = Input.GetTouch(0);
// Move the cube if the screen has the finger moving.
if (touch.phase == TouchPhase.Moved)
{
Vector2 pos = touch.position;
pos.x = (pos.x - width) / width;
pos.y = (pos.y - height) / height;
position = new Vector3(-pos.x, pos.y, 0.0f);
// Position the cube.
transform.position = position;
}
if (Input.touchCount == 2)
{
touch = Input.GetTouch(1);
if (touch.phase == TouchPhase.Began)
{
// Halve the size of the cube.
transform.localScale = new Vector3(0.75f, 0.75f, 0.75f);
}
if (touch.phase == TouchPhase.Ended)
{
// Restore the regular size of the cube.
transform.localScale = new Vector3(1.0f, 1.0f, 1.0f);
}
}
}
}
创建了一个小游戏交流群,加群或者有问题交流 可以加我微信 备注“微信小游戏”
推广一下自己做的简单的小游戏
点击上方“蓝字”关注我们
1,touch 事件是如何从驱动层传递给
Framework 层的 InputManagerService;
2,WMS 是如何通过 ViewRooImple 将事件传递到目标窗口;
3,touch 事件到达 DecorView 后,是如何一步步传递到内部的子 View 中的。
其中与上层软件开发息息相关的就是第 3 条,也是本课时的重点关注内容。
注意:不同版本间的代码会有区别,本文是基于 Android-28 的源码上进行分析。
思路梳理
在深入分析事件分发源码之前,需要先弄清楚 2 个概念。
ViewGroup
ViewGroup 是一组 View 的组合,在其内部有可能包含多个子 View,当手指触摸屏幕上时,手指所在的区域既能在 ViewGroup 显示范围内,也可能在其内部 View 控件上。
因此它内部的事件分发的重心是处理当前 Group 和子 View 之间的逻辑关系:
1,当前 Group 是否需要拦截 touch 事件;
2,是否需要将 touch 事件继续分发给子 View;
3,如何将 touch 事件分发给子 View。
View
View 是一个单纯的控件,不能再被细分,内部也并不会存在子 View,所以它的事件分发的重点在于当前 View 如何去处理 touch 事件,并根据相应的手势逻辑进行一些列的效果展示(比如滑动,放大,点击,长按等)。
1,是否存在 TouchListener;
2,是否自己接收处理 touch 事件(主要逻辑在 onTouchEvent 方法中)。
事件分发核心 dispatchTouchEvent
整个 View 之间的事件分发,实质上就是一个大的递归函数,而这个递归函数就是 dispatchTouchEvent 方法。在这个递归的过程中会适时调用 onInterceptTouchEvent 来拦截事件,或者调用 onTouchEvent 方法来处理事件。
先从宏观角度,纵览整个 dispatch 的源码如下:
如代码中的注释,dispatch 主要分为 3 大步骤:
步骤 1:判断当前 ViewGroup 是否需要拦截此 touch 事件,如果拦截则此次 touch 事件不再会传递给子 View(或者以 CANCEL 的方式通知子 View)。
步骤 2:如果没有拦截,则将事件分发给子 View 继续处理,如果子 View 将此次事件捕获,则将 mFirstTouchTarget 赋值给捕获 touch 事件的 View。
步骤 3:根据 mFirstTouchTarget 重新分发事件。
接下来详细的看下每一个步骤:
步骤 1 的具体代码如下
图中红框标出了是否需要拦截的条件:
如果事件为 DOWN 事件,则调用 onInterceptTouchEvent 进行拦截判断;
或者 mFirstTouchTarget 不为 null,代表已经有子 View 捕获了这个事件,子 View 的 dispatchTouchEvent 返回 true 就是代表捕获 touch 事件。
如果在上面步骤 1 中,当前 ViewGroup 并没有对事件进行拦截,则执行步骤 2。
步骤 2 具体代码如下
仔细看上述的代码可以看出:
图中 ① 处表明事件主动分发的前提是事件为 DOWN 事件;
图中 ② 处遍历所有子 View;
图中 ③ 处判断事件坐标是否在子 View 坐标范围内,并且子 View 并没有处在动画状态;
图中 ④ 处调用 dispatchTransformedTouchEvent 方法将事件分发给子 View,如果子 View 捕获事件成功,则将 mFirstTouchTarget 赋值给子 View。
步骤 3 具体代码如下
步骤 3 有 2 个分支判断。
分支 1:
如果此时 mFirstTouchTarget 为 null,说明在上述的事件分发中并没有子 View对事件进行了捕获操作。这种情况下,直接调用 dispatchTransformedTouchEvent 方法,并传入 child 为 null,最终会调用 super.dispatchTouchEvent 方法。实际上最终会调用自身的 onTouchEvent 方法,进行处理 touch 事件。也就是说:如果没有子 View 捕获处理 touch 事件,ViewGroup 会通过自身的 onTouchEvent 方法进行处理。
分支 2:
mFirstTouchTarget 不为 null,说明在上面步骤 2 中有子 View 对 touch
事件进行了捕获,则直接将当前以及后续的事件交给 mFirstTouchTarget 指向的 View 进行处理。
事件分发流程代码演示
定义如下布局文件:
DownInterceptedGroup 和 CaptureTouchView 是两个自定义 View,它们的源码分别如下:
手指触摸 CaptureTouchView 并滑动一段距离后抬起,最终打印 log 如下:
上图中在 DOWN 事件中,DownInterceptGroup 的 onInterceptTouchEvent 被触发一次;然后在子 View CaptureTouchView 的 dispatchTouchEvent 中返回 true,代表它捕获消费了这个 DOWN 事件。这种情况下 CaptureTouchView 会被添加到父视图(DownInterceptGroup)中的 mFirstTouchTarget 中。因此后续的 MOVE 和 UP 事件都会经过 DownInterceptGroup 的 onInterceptTouchEvent 进行拦截判断。详细源码可以参考:CaptureTouchView.java
为什么 DOWN 事件特殊
所有 touch 事件都是从 DOWN 事件开始的,这是 DOWN 事件比较特殊的原因之一。另一个原因是 DOWN 事件的处理结果会直接影响后续 MOVE、UP 事件的逻辑。
在步骤 2 中,只有 DOWN 事件会传递给子 View 进行捕获判断,一旦子 View 捕获成功,后续的 MOVE 和 UP 事件是通过遍历 mFirstTouchTarget 链表,查找之前接受 ACTION_DOWN 的子 View,并将触摸事件分配给这些子 View。也就是说后续的 MOVE、UP 等事件的分发交给谁,取决于它们的起始事件 Down 是由谁捕获的。
mFirstTouchTarget 有什么作用
mFirstTouchTarget 的部分源码如下:
可以看出其实 mFirstTouchTarget 是一个 TouchTarget 类型的链表结构。而这个 TouchTarget 的作用就是用来记录捕获了 DOWN 事件的 View,具体保存在上图中的 child 变量。可是为什么是链表类型的结构呢?因为 Android 设备是支持多指操作的,每一个手指的 DOWN 事件都可以当做一个 TouchTarget 保存起来。在步骤 3 中判断如果 mFirstTouchTarget 不为 null,则再次将事件分发给相应的 TouchTarget。
容易被遗漏的 CANCEL 事件
在上面的步骤 3 中,继续向子 View 分发事件的代码中,有一段比较有趣的逻辑:
上图红框中表明已经有子 View 捕获了 touch 事件,但是蓝色框中的 intercepted boolean 变量又是 true。这种情况下,事件主导权会重新回到父视图 ViewGroup 中,并传递给子 View 的分发事件中传入一个 cancelChild == true。
看一下 dispatchTransformedTouchEvent 方法的部分源码如下:
因为之前传入参数 cancel 为 true,并且 child 不为 null,最终这个事件会被包装为一个 ACTION_CANCEL 事件传给 child。
什么情况下会触发这段逻辑呢?
总结一下就是:当父视图的 onInterceptTouchEvent 先返回 false,然后在子 View 的 dispatchTouchEvent 中返回 true(表示子 View 捕获事件),关键步骤就是在接下来的 MOVE 的过程中,父视图的 onInterceptTouchEvent 又返回 true,intercepted 被重新置为 true,此时上述逻辑就会被触发,子控件就会收到 ACTION_CANCEL 的 touch 事件。
实际上有个很经典的例子可以用来演示这种情况:
当在 Scrollview 中添加自定义 View 时,ScrollView 默认在 DOWN 事件中并不会进行拦截,事件会被传递给 ScrollView 内的子控件。只有当手指进行滑动并到达一定的距离之后,onInterceptTouchEvent 方法返回 true,并触发 ScrollView 的滚动效果。当 ScrollView 进行滚动的瞬间,内部的子 View 会接收到一个 CANCEL 事件,并丢失touch焦点。
比如以下代码:
CaptureTouchView 是一个自定义的 View,其源码如下:
CaptureTouchView 的 onTouchEvent 返回 true,表示它会将接收到的 touch 事件进行捕获消费。
上述代码执行后,当手指点击屏幕时 DOWN 事件会被传递给 CaptureTouchView,手指滑动屏幕将 ScrollView 上下滚动,刚开始 MOVE 事件还是由 CaptureTouchView 来消费处理,但是当 ScrollView 开始滚动时,CaptureTouchView 会接收一个 CANCEL 事件,并不再接收后续的 touch 事件。具体打印 log 如下:
因此,我们平时自定义View时,尤其是有可能被ScrollView或者ViewPager嵌套使用的控件,不要遗漏对CANCEL事件的处理,否则有可能引起UI显示异常。
总结
判断是否需要拦截 —> 主要是根据 onInterceptTouchEvent 方法的返回值来决定是否拦截;
在 DOWN 事件中将 touch 事件分发给子 View —> 这一过程如果有子 View 捕获消费了 touch 事件,会对 mFirstTouchTarget 进行赋值;
最后一步,DOWN、MOVE、UP 事件都会根据 mFirstTouchTarget 是否为 null,决定是自己处理 touch 事件,还是再次分发给子 View。
然后介绍了整个事件分发中的几个特殊的点。
DOWN 事件的特殊之处:事件的起点;决定后续事件由谁来消费处理;
mFirstTouchTarget 的作用:记录捕获消费 touch 事件的 View,是一个链表结构;
CANCEL 事件的触发场景:当父视图先不拦截,然后在 MOVE 事件中重新拦截,此时子 View 会接收到一个 CANCEL 事件。