A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

本帖最后由 武汉分校-小舞 于 2016-3-21 16:29 编辑

【武汉校区】独家分享:安卓触摸事件的分发机制


前言
随着移动互联网的发展,手机成为了人体的除胳膊和腿之外的“第五肢”;同时手机性能越来越强大,已经远远不止最初的“电子助理”那么简单,而更像是随身电脑。越来越多的公司对移动互联网这一块重视起来,对人才的需求也越来越大,所以开发人员的薪资也是水涨船高。在移动应用开发这一领域,主要有两大阵营:iOS和Android,其他WinPhone、Ubuntu、WebOS、BlackBerry则比较小众。
在多年前,手机还有直板、翻盖、侧滑,一般还配有键盘;但现在基本上9%5以上的手机都是大屏幕加少量物理按键(用来控制开关、音量);用户与手机的主要交互都通过触摸屏来完成。因为安卓触摸事件的分发机制比较严密、复杂;本文就来从整体结构上聊一聊Android的触摸事件分发。

触摸事件概述
安卓是使用java进行开发的,java又是一种面向对象的语言,所以在硬件感知到用户的触摸后,系统就触摸行为封装成触摸事件对象了(MotionEvent)。但MotionEvent中包含的是单个触摸事件的信息。怎么理解单个呢?当你手指按到屏幕上,手指可能移动,也可能抬起。如果把从按下到抬起的所有事件都封装到MotionEvent对象中,那就意味在用户抬起手指后才能拿到触摸事件对象,进行处理,这显然是不可接受的。系统会依据一定的采样率(每隔多久从硬件获取一下触摸点)得到原始数据,再封装成单个MotionEvent对象发送给应用。所以一组触摸事件(从按下到抬起,用户想要交互)包含许多单个触摸事件。在我们开发自定义控件时,往往会处理系统分发过来的触摸事件,进行处理,完成与用户的交互。

触摸事件的基本概念
1 事件类型
MotionEvent对象有getAction方法用于判断事件的类型,本文主要介绍4种类型。
  
事件类型
  
含义
MotionEvent.ACTION_DOWN
0
手指按下
MotionEvent.ACTION_UP  
1
手指抬起
MotionEvent.ACTION_MOVE
2
手指移动
MotionEvent.ACTION_CANCEL
3
本组触摸事件结束(后面会详细说明)
2 事件发生的坐标
触摸屏有宽度(x)和高度(y);在MotionEvent这个信息可以通过MotionEvent的getRawX() 和getRawY() 方法获得出来,需要注意的是这个坐标是相对于屏幕的左上角的,单位是像素值,类型是float;但我们最常用的getX()和getY()方法,来获得相对于View的左上角的坐标,因为系统在分发触摸事件时(后面再讲)调用了setLocation()或offSetLocation()方法把坐标进行偏移。
3 事件发生的时间
MotionEvent具有geEventTime方法可以获得此事件发生的事件,单位是毫秒,类型是long,参考时间是系统开机时间减去深度睡眠时间(不重要),我们可以通过两个MotionEvent的事件差以及距离差计算出手指移动的快慢。
至此触摸事件的基本三要素就(时间、地点、人物)就齐全了。

View的事件分发的重要方法
  
方法
  
意义
返回值
boolean  dispatchTouchEvent()
分发触摸事件(给子控件或自己处理)
找到了处理此触摸事件的控件(不管是自己或是子控件)
boolean onTouchEvent()
处理触摸事件
是否消费了触摸事件
boolean  onInterceptTouchEvent()
拦截触摸事件(只有ViewGroup才有此方法)
返回值为true则交给自己的onTouchEvent方法处理,false则由dispatch继续传递
先看看我们的测试布局如下
自定义了一个MyViewGroup,继承于FrameLayout;外界可以传递boolean值,决定MyViewGroup的onIntercepterTouchEvent的返回值。

Down事件
当我们手指按到Button所在的位置上时,就会有一个MotionEvent对象传递给了Activity的dispatchTouchEvent方法,而activity的此方法会继续分发,经过一些传递后,发送到ScrollView的dispatchToucheEvent方法中,ScrollView会根据MotionEvent的坐标,去找对应位置的子控件,把事件传递到它的dispatchTouchEvent方法(此时会把MotionEvent中的x和y根据孩子的位置进行偏移)中,基本上所有的ViewGroup都会这样执行操作,直到最底层的Button,Button只是一个View(而不是ViewGroup),它的dispatchTouchEvent会调用onTouchEvent方法,得到一个boolean值,因为button是需要触摸事件的(来响应click),所以返回了true。
这时,dispatchTouchEvent方法,就返回了true,表明此事件找到控件来处理了。
简单来说触摸事件的分发,像一个U型管,会经历从上往下流动和从下往上流动两个过程,除非中间的某个控件的onTouchEvent返回了true。而这一切都还在activity的dispatchTouchEvent方法中,以下是debug的方法栈:


再看看下图,触摸事件机制就像U型管一样,从左侧流入,经过一级一级向下传递,来找到处理它的View,此例中,button的onTouchEvent方法返回了true就把触摸事件消费了,相当于U型管在红色处有漏洞,水在这里就停止了。


但如果是触摸到了不处理触摸事件的TextView,则触摸事件经过dispatchTouchEvent分发到TextView,而TextView的onToucheEvent返回了false,没处理触摸事件。在MyViewGroup的dispatchTouchEvent方法中发现孩子不处理,就把触摸事件交给自己的onTouchEvent方法处理,结果又返回了false,那他得分发结果是false,代表没有找到子控件来处理触摸事件,那MyViewGroup的父控件也没有办法,也是去调用onTouchEvent尝试处理,如果不处理,就以此类推,到达了activity的onTouchEvent方法中。

UP事件
当用户点击的Button后,静止不动,马上抬起,因为Button的onTouchEvent方法返回了true,就代表感兴趣,触摸事件经由dispatchTouchEvent(此时没有根据触摸位置查找处理触摸事件的View的逻辑了,父控件都记住Down时,返回true的子控件),又到达了Button,在Button的onTouchEvent方法中发现刚才的两个触摸事件应该要表达Click的意图,就调用了performClick方法,导致了点击监听中的onClick方法调用。

MOVE事件
当用户点击的Button后,没有马上抬起,开始移动,因为Button的onTouchEvent方法返回了true,就代表感兴趣,触摸事件经由dispatchTouchEvent(此时没有根据触摸位置查找处理触摸事件的View的逻辑了,父控件都记住Down时,返回true的子控件),又到达了Button。 如果用户是上下移动的话,进行事件分发的ScrollView发现,垂直方向的移动想要表达滚动的意图,就把事件停止分发,自己处理。但ScrollView很好心的分发了一个类型为Cancel的事件,告诉子控件,事件取消了,接下来的事件我来处理,你们不用管了。Cancel事件,会一直传递到Button。以上被称为触摸事件的抢夺机制。
如果点击Button了,不管手指怎么移动,也不想让ScrollView移动,那么就可以调用Button的父控件的disallowInterceptTouchEvent(true)方法,要求父控件不要抢夺触摸事件,这个方法内部会一层一层向上调用。但值得注意的是,这个只是要求,霸道的父控件(比如SwipeRefreshLayout)还是会抢夺触摸事件的。
接下来我们介绍一下在MyViewGroup中定义的onInterceptTouchEvent方法,当dispatchTouchEvent方法看到这个方法返回true的时候,触摸事件就不会向下传递了,而是直接传递给了自己的onTouchEvent方法,就像把U形管对应位置打了一个通道。这时不管手指在Button所在位置怎么点击、移动,Button都不会得到触摸事件。
最后,提一下触摸事件相关的帮助类。
GestureDetector:可以用来分析手势
VelocityTracker:可以用来计算手指移动的速度
ViewDragHelper:可以简化View的移动拖拽开发
Scroller:可以简化View的惯性滑动(先快后慢)

想获取最新传智播客武汉中心分享技术文章请加QQ  1641907557 ,后期会分享更多与实体班同步教程,助你冲击月薪20K!


0 个回复

您需要登录后才可以回帖 登录 | 加入黑马