1. Touch事件和绘制事件的异同之处
Touch事件和绘制事件很类似,都是由ViewRoot派发下来的,但是不同之处在绘制事件是由应用中的某个View发起请求,一层一层上传到ViewRoot,再有ViewRoot下发绘制,传递canvas给所有子View让其绘制自身,绘制好后,再通知WMS进行画到屏幕上。而Touch事件是由硬件捕获到触摸后由系统传递给应用的ViewRoot,再由ViewRoot往下一层一层传递。
他们的处理过程都是自上而下的分发,但是绘制多了一层自下往上的请求。
事件存在消耗,事件的处理方法都会返回一个boolean值,如果该值为true,则本次事件下发将会终止。
2. MotionEvent
2.1 MotionEvent对象的产生
系统有一个线程在循环收集屏幕硬件信息,当用户触摸屏幕时,该线程会把从硬件设备收集到的信息封装成一个MotionEvent对象,然后把该对象存放到一个消息队列中。
系统的另一个线程循环的读取消息队列中的MotionEvent,然后交给WMS去派发,WMS把该事件派发给当前处于活动的Activity,即处于活动栈最顶端的Activity。
这就是一个先进先出的消费者和生产者的模板,一个线程不停的创建MotionEvent对象放入队列中,另一个线程不断的从队列中取出MotionEvent对象进行分发。
当用户的手指从接触屏幕到离开屏幕,是一个完整的触摸事件,在该事件中,系统会不断收集事件信息封装成MotionEvent对象。收集的间隔时间取决于硬件设备,例如屏幕的灵敏度以及cpu的计算能力。目前的手机一般在20毫秒左右。
MotionEventCompat.getActionMasked()
2.2 MotionEvent对象详解
MotionEvent对象包含了触摸事件的时间、位置、面积、压力、以及本次事件的Dwon发生的时间。
MotionEvent常用的Action分为5种:Down 、Up、Move、Cancel、OutSide
MotionEvent中我们常用的方法就是获取点击的坐标,因为这是与我们操作息息相关的。获取坐标有两种方式:
- getX和getY用于获取以该View左上角为坐标原点的坐标
- getRowX和getRowY用于获取以屏幕左上角为坐标原点的坐标
2.3 5种Touch事件
- Down:一次触摸事件的第一个MotionEvent对象,即手指初次接触屏幕。
- Up:通常为一次触摸事件的最后一个MotionEvent对象,即手指离开屏幕。
- Move:通常多次发生在一次触摸事件之中。表示触摸点发生了移动,我们通常把手指放到屏幕上,实际也会触发该事件,因为人手总是在轻微抖动的。
- Cancel:常用于取消某个触摸事件,一般是由程序逻辑来指定该事件,用于取消某次触摸事件。
- OutSide:当触摸点发生在响应事件的View之外时,传递的事件,通常由程序逻辑来指定。
在上面5种事件中,Down为最重要的事件,因为这是一个触摸事件的起始点,程序的很多逻辑判断,都需要根据该事件做处理,例如分发拦截。一次触摸事件必须要有Down事件,这也是MotionEvent对象中都包含了本次触摸事件的Down事件发生的时间点这个属性。其次是Move和Up,通过这3个事件的逻辑处理,就构建出来滑动,点击,长按,双击等多种效果。
2.4 创建一个MotionEvent对象
|
|
或者一个更简单的方式:
|
|
也可以通过一个MotionEvent来创建一个新的
|
|
通过以上的方式,我们知道,我们也可以通过代码来构建一个虚假的MotionEvent,并分发下去。
|
|
然后通过延迟以此往下派发Move和Up时间,形成一个完整的触摸操作。
3. dispatchTouchEvent触摸事件分发
之前我们知道触摸事件是被包装成MotionEvent进行传递的,而该对象是继承了Parcelable接口,正因为如此,才可以从系统中传递到我们的应用中。系统通过跨进程通知ViewRoot,ViewRoot会调用DecorView的dispatchTouchEvent下发。
这里有一个和其他事件传递不同的地方,DecorView会优先传递给Activity,而不是它的子View。而Activity如果不处理又会回传给DecorView,DecorView才会再将事件传给子View。
dispatchTouchEvent就是触摸事件传递的对外接口,无论是DecorView传给Activity,还是ViewGroup传递给子View,都是直接调用对方的dispatchTouchEvent方法,并传递MotionEvent参数。
我们首先来看看Activity中的dispatchTouchEvent逻辑:
|
|
至此我们已经至少明白了以下几点:
1、我们可以重载Activity的onUserInteraction方法,在Down事件触发传递前,实现我们的一些需求,实际上源码中有很多这样的方法,再某个方法体的第一行提供一个空实现的回调方法,在某个方法的最后一行提供一个空实现的回调方法,以便子类去实现自己的逻辑,例如AsyncTask就有类似的方式。这些技巧都能很好的提高我们代码的扩展性。
2、Activity会间接的调用根View的dispatchTouchEvent,并通过if判断返回值,如果为true,即向上层返回true,也就是调用Activity的dispatchTouchEvent的WMS,即操作系统。
3、如果if判断为false,即根View和根View下的所有子View均为消费掉该事件,那么下面的代码就有执行机会,即Activity的onTouchEvent,并把该方法的返回值作为结果返回给上层。
3.1 View的dispatchTouchEvent
View中的处理相当简单明了,因为不涉及到子View,所以只在自身内部进行分发。首先判断是否设置了触摸监听,并且可以响应事件,就交由监听的onTouch处理。如果上述条件不成立,或者监听的onTouch事件没有消费掉该事件,则交由onTouchEvent进行处理,并把返回结果交给上层。
|
|
3.2 ViewGroup的dispatchTouchEvent
3.3 Down事件
- 通过onInterceptTouchEvent方法判断是否要拦截事件,默认fasle
- 根据scroll换算后的坐标找出所接受的子View。有动画的子View将不接受触摸事件。
- 找到能接受的子View后把event中的坐标转换成子View的坐标
- 调用子View的dispatchTouchEvent把事件传递给子View。
- 如果子View消费了该事件,则把target记录为子View,方便后面的Move和Up事件的传递。
- 如果子View没有消费,则继续寻找下一个子View。
- 如果没找到,或者找到的子View都不消费,就会调用View的dispatchTouchEvent的逻辑,也就是判断是否有触摸监听,有的话交给监听的onTouch处理,没有的话交给自己的onTouchEvent处理
接下来我们来研究ViewGroup的dispatchTouchEvent,这是稍微复杂的分发逻辑。
|
|
3.4 Move和Up事件
判断事件是否被取消或者事件是否要拦截住,是的话,给Down事件找到的target发送一个取消事件。如果不取消,也不拦截,并且Down已经找到了target,则直接交给target处理,不再遍历子View寻找合适的View了。这种处理事件是正确的,我们用手机经常可以体会到,当我手指按在一个拖动条上之后,在拖动的时候手指就算移出了拖动条,依然会把事件分发给拖动条控制它的拖动。
4. onInterceptTouchEvent
ViewGroup的方法,事件拦截,return true表示拦截触摸事件,事件就不往下传递
子View可以调用getParent().requestDisallowInterceptTouchEvent( true ) 请求父控件不拦截touch事件
5. View的onTouchEvent
从View的dispatchTouchEvent可以看出,事件最终的处理无非是交给TouchListener的onTouch方法或者是交由onTouchEvent处理,由于onTouch默认是空实现,由程序员来编写逻辑,那么我们来看看onTouchEvent事件。View只能响应click和longclick,不具备滑动等特性。
Down时,设置按压状态,发送一个延迟500毫秒的长按事件。
Move时,判断是否移出了View,移出后移除按压状态,长按事件。
Up时,取消按压,并判断它是否可以通过触摸获取焦点,是的话设置焦点,判断长按事件是否执行了,如果还没执行,就删除,并执行点击事件。
|
|
从上面的代码我们总结一下View对触摸事件的处理:
1、是否为diabale,如果是,直接根据是否设置了click和longclick来返回。
2、是否设置了触摸代理对象,如果有,把事件传递给触摸代理对象,交由其处理,如果消费了,直接返回
3、是否为click或者longclick的,如果是,返回true,不是返回false。
而View对click和longclick的处理如下:
Down:
- 判断是否可以触摸上下文菜单。
- 是否在可以滑动的容器中,如果是先设置临时按压,再发送一个延迟消息把临时按压改为按压,并发送一个延迟500毫秒的事件去执行长按代码
- 如果不在滚动容器中,直接设置按压状态,并发送一个延迟500毫秒的事件去执行长按代码。
Move:
- 取触摸点坐标判断是否在View中(额外增加了8像素的范围)
- 如果在,不用做任何事。
- 如果不在,取消临时按压到按压回调,取消长按延迟回调,设置为非按压状态
Up
- 判断是否为按压或者临时按压状态
- 如果不是,不做任何处理
- 如果是先判断其是否可以获取焦点,然后请求焦点。
- 如果是临时按压状态,设置临时按压状态为按压状态。保证界面被绘制成按压状态,让用户可以看见。
- 如果长按回调还未触发,取消长按回调,如果不是焦点状态,触发click事件。
- 如果是临时按压状态,发送一个延迟取消按压状态的,保证按压状态持续一段时间,让用户可见。
- 如果不是临时按压状态,直接发送消息取消按压状态。发送失败,直接取消按压状态。
- 取消把临时按压设置按压的回调。
从中我们知道View的onTouchEvent主要处理了click和longclick事件,当按下时,向消息机制发送一个延迟500毫秒的长按回调事件,当移动时候判断是否移出了View的范围,超出则取消事件。当离开时,判断长按事件是否触发了,如果没触发且不是焦点,就触发click事件。
在这里最绕的就是临时按压和按压状态,临时按压是为了处理滑动容器的,让处于滑动容器中,按下时,我们先设置的是临时按压,持续64毫秒,是为了判断接下来的时间内是否发生了move事件,如果发生了,将不会再出发按压状态,这样不会让用户看到listView滚动时,item还处于按压状态。在离开时,我们再次判断是否处于临时按压,如果是在64毫秒内触发了down和up,说明按压状态还没来得急绘制,则强制设置为按压状态,保证用户能看到,并在取消回调的方法上加上64毫秒的延迟
6. onTouch与onClick
|
|
点击ImageView的时候只会打印一次,因为onTouch()返回false,只传递down事件,不会传递up事件
|
|
把onTouch()方法返回值改为true,点击ImageView会打印两次(down and up)
|
|
|
|
还是打印两次,onTouch()返回true,click事件并不会得到执行
打印三次,两次touch事件(down and up)和一次click事件
点击Button会打印两次
|
|
打印两次,因为onTouch()返回true,不会执行onTouchEvent(),而click事件是在onTouchEvent()中执行,所以也不会执行click事件
打印三次
a 判断mOnTouchListener是否为null
b 判断当前的控件是否可用
c 判断view的onTouch。
d 如果以上一个返回为false。那么就会调用onTouchEvent
首先判断mOnTouchListener不为null,并且view是enable的状态,然后 mOnTouchListener.onTouch(this, event)返回true,这三个条件如果都满足,直接return true ; 也就是下面的onTouchEvent(event)不会被执行了。如果我们设置了setOnTouchListener,并且return true,那么View自己的onTouchEvent就不会被执行了
onTouch是优先于onClick执行, onClick的调用在onTouchEvent(event)方法中
view的事件分发
- 返回true,说明可以响应down事件和up事件
- 返回false,只会响应down事件。不会响应up事件。在down事件如果能消费(处理)当前事件。那么在up的时候也会把事件传递给当前的view,在down事件处理不了当前事件。那么在up的时候。也不会把事件传递给当前的view
模拟点击事件
|
|
7. ScrollView的onTouchEvent
普通的ViewGroup并没有对onTouchEvent事件做处理,只有可以滚动的才有,我们可以分析一下ScrollView
Down时,判断落点是否在子View中,不再就不处理,因为ScrollView只有一个子View。
Move时,通过对比本次手指的位置和上一次的位置的距离,计算出Y方向的差值,然后用scorllBy进行滚动视图
Up时,通过速度进行fling,这里利用了两个帮助类,一个是计算速度的帮助类VelocityTracker,一个是滚动的帮助类Scroller
|
|
通过以上分析,我们得出以下知识:
- 在down事件的时候先判断触摸是否处于边缘,如果是,则不处理
- 在down事件中判断落点是否在子View中,如果不在,不处理
- 在down事件中判断是否仍在滑动,如果是,先停止
- 记录第一个按下点的索引值
- 每次事件都记录住当前的y值
- 在move事件中通过记录的索引值找到对应的点,获取y坐标
- 与上一次y坐标进行比对,scrollBy两次的差值
- 在up事件的时候计算最后一秒钟的速度,并且有最大速度进行限制,当计算的速度大于系统默认的最小速度时,只想fling
- up和cancel事件还原变量为默认值
- 如果为多点离开,进行多点离开的处理
- 该处理方式时:如果离开的是第一个按下的点,那么由第二个按下的点代替其进行y值偏移计算的基点,并清空速度计算的帮助类,重新记录MotionEvnet
8. Layout和Scroll的区别
- Layout中设置的是自身在父View中的显示区域
- Scroll是调整自己的显示区域
- 当父View滚动或者layout变化后,自身在屏幕上的位置会发生变化。
当自身Scroll滚动后,在屏幕上的显示位置是不变的,变的只是自身的显示内容。 - Scroll滚动不会影响Layout,只是在draw的时候影响画布偏移和触摸时的坐标计算。