假设手机屏幕上有一个 button。我们去点击了一下,然后 button 做出了相应的反应,那么这个过程其实这样的,当手机点击到屏幕时,TP(Touch panel)传感器的数据发生了变化。数据经过驱动的处理(其实用示波器来看传感器数据,这个数据肯定不可能那么规整),然后数据依次传递到内核,framwork,然后再传递相应的 app 的当前 Activity 中。我们说 android 中 View 的事件分发,其实指的就是在此之后的过程。当手指接触屏幕所产生的反应,我们称之为事件 (
). 当然
- MotionEvent
实际上也可以细分为很多种,典型的有以下几种。
- MotionEvent
: 手指刚刚接触到屏幕。
- ACTION_DOWN
: 手指在屏幕上滑动。
- ACTION_MOVE
: 手指从屏幕上离开的一瞬间。
- ACTION_UP
所以正常情况下,手指触摸屏幕会触发一系列事件。 比如
。从手指接触屏幕那一刻起,到手指离开屏幕那一刻结束,中间所产生的一系列事件总和,我们称之为一个事件序列。它以一个
- DOWN -> MOVE -> UP
事件开始,中间夹杂着 0 个或多个
- down
事件,以一个
- move
事件结束。
- up
我们手指接触到屏幕,怎么判断是点击还是滑动呢?其实在手机 framework 中,有一个默认值,比如 8px(和具体的设备和手机厂商有关),当手指移动的距离大于该值时,就是滑动事件
,否则就是点击事件。
- TouchSlop
我们所讨论的事件分发,指的就是
从
- MotionEvent
这个过程。 (毕竟我们都是 做上层 app 开发的,像 TP 数据怎么处理之类的那些是驱动工程师们关心的问题)。
- Activity -> Window -> ViewGroup -> View
这个事件分发的过程,总结起来其实很简单,因为该场景和我们日常生活中的场景是一致的。
boss(老板) 要做某件事情,但是既然身为 boss,人家肯定不用自己动手嘛,所以这事交给下面的 manager(经理) 就好了,经理好歹也是领导嘛,所以也不用自己亲自动手,交给下面的 worker(工人) 就好了, 所以转了一圈,干活的还是最下面的工人,如果 worker 把这活处理了,那么这件事情就算完了,可是也有例外情况,如果 worker 发现这个活自己处理不了,那么这事就只能向上反馈给 manager 了,如果 manager 能把这活给处理了,那自然最好,可是如果 manager 发现这活自己也处理不了,那就只能再反馈给 boos,让 boss 去处理了
事件分发也是如此。点击屏幕上的一个点, 事件那么经过
, 然后传递到 界面最外层(或者叫最顶级)的
- activity -> window
容器中(指的是
- ViewGroup
,
- Relativelayout
等)。 然后最后依次传递到被包裹在最里面到
- LinearLayout
中。(当然,如果
- View
包裹的有几层的
- View
,那么肯定是依次先传递给它们, 并且需要注意,最里面的控件完全可以是一个
- ViewGroup
,而它不必包含
- ViewGroup
。)
- View
事件被传递到了最里面的
,所以交给了
- View
来处理,如果 View 没处理,那么就依次倒过来向上回溯。分别交给各级
- View
,如果它们也没处理,那么就继续向上回溯,乃至
- ViewGroup
,
- Window
中来处理。
- Activity
就是最底层的 worker,而 boss,manager 都是包裹在它外面的
- View
, 如果层级比较复杂的话,还要经过多层才能传到到最底层的
- ViewGroup
,(比如经过老板,总经理,部门经理,项目经理等多层,事情才分配到最底层的工人这里),
- View
处理不了的,那么就再依次向上反馈给各级领导,看看谁去处理。
- View
该过程其实就是一个递归的过程。这是 android 系统中默认的事件分发规则,如果我们理解了这个规则,那么我们就可以解决很多实际问题,比如 我们可以在某一层
中来拦截处理事件,而不用向下传递到
- ViewGroup
中。
- View
Notice:
,所以从继承关系上来讲,
- ViewGroup extends View
也是一个 View, 而在本文当中说的
- ViewGroup
指的是
- ViewGroup
,
- RelativeLayout
等容器类 View,而 View 指的是
- LinearLayout
,
- Button
等不会再有 child 的 View(因为它们不会再包裹什么了)。之所以要明确区分,是因为它们的有些方法的实现是有区别的。
- TextView
指的是 View 或 ViewGroup(也可能是 View 和 ViewGroup)。
- 控件
既然是事件分发,那么不得不提到到几个主要方法。
中:
- View
- //从名字中就可以看出来,是负责分发事件的方法。
- public boolean dispatchTouchEvent(MotionEvent event)//该方法中设置去消费事件,比如我们平时常用的OnClickView,OnLongClick等监听事件都是在这里被调用的。
- public boolean onTouchEvent(MotionEvent event)
实际上,View 当中也有具体负责消费事件的接口。
- public interfaceOnTouchListener {boolean onTouch(View v, MotionEvent event);
- }// 这个就是我们最常用的 点击事件方法。
- public interfaceOnClickListener {void onClick(View v);
- }
说完了
, 那么再来看看
- View
中的主要方法。
- ViewGroup
- //View中也有该方法,但是和ViewGroup中的实现方式不同
- public boolean dispatchTouchEvent(MotionEvent ev)//该方法是ViewGroup特有的,决定是否拦截事件。(默认返回false),如果返回true,则事件不会再向child传递了,这个道理很简单,ViewGroup是属于领导, 它才有权利决定是否拦截事件。而View是最底层的工人,是没有决定权的。
- public boolean onInterceptTouchEvent(MotionEvent ev)//几个相关方法或接口中,ViewGroup实现了这两个,其他都没重写,也就是利用View当中的实现
和事件分发有关的方法就这几个。基本的道理其实也不复杂。(联想老板给工人分配任务的场景就可以了)。 而关于这个过程,可以使用伪代码描述如下: 对于
:
- View
- //@author www.yaoxiaowen.com
- boolean dispatchTouchEvent(MotionEvent event){booleanresult =false;if(OnTouchListener.OnTouch(view, event)){
- result =true;
- }if(!result &&onTouchEvent(event)){
- result =true;
- }returnresult;
- }//实际上这个类,处理的比较复杂,比如分为 ACTION_DOWN, ACTION_UP 之类的,
- //不过这里是仅仅是作为示例的伪代码。
- //@author www.yaoxiaowen.com
- boolean onTouchEvent(MotionEvent event){if(clickable || clickable){if(event.getAction()==ACTION_UP && OnClickListener!=null){
- OnClickListener.onClick();
- }return true;
- }return false;
- }
而对于
,伪代码如下:
- ViewGroup
- //@author www.yaoxiaowen.com
- boolean dispatchTouchEvent(MotionEvent event){booleanresult =false;if(onInterceptTouchEvent(event)){//Notice:这个地方不是调用 的this.onTouchEvent。调用自己那就是死循环了,既然调用了父类View的dispatchTouchEvent(),想想View的该方法做什么,所以实际上就是把事件分发给自己了。
- //(因为类似onTouchEvent, onTouchListener方法在ViewGroup当中没实现,只在父类View当中才实现)reuslt =super.dispatchTouchEvent();
- }else{//child可能是 ViewGroup,也可能是 Viewresult = child.dispatchTouchEvent(event);
- }returnresult;
- }
结合伪代码,再想象具体的分发流程。仔细琢磨一下,就可以大概的了解这个分发流程了。
虽然总结的内容的不复杂,但是实际上依旧有很多的细节。下面我们就结合具体的源码来进行分析。(具体的源码非常复杂,参考了不少博客,然后自己也读了几遍,但是也只是理解了一小部分,毕竟自己也水平有限。不过我们要分析的也就是流程的主干内容。)
沿着事件分发的流程。
.
- Activity -> Window -> ViewGroup -> View
先看
:
- Activity
- //Activity.java
- public boolean dispatchTouchEvent(MotionEvent ev) {//省略部分代码
- //从这句代码中,将事件分分发到了 Window
- if(getWindow().superDispatchTouchEvent(ev)) {return true;
- }return onTouchEvent(ev);
- }public boolean onTouchEvent(MotionEvent event) {if(mWindow.shouldCloseOnTouch(this, event)) {finish();return true;
- }return false;
- }
在 Activity 中,事件被传递给了 Window,但是此时要注意了,如果 Window 当中未消费事件。(就是返回了 false)。那么此时就调用 Activity 自身的 onToucheEvent()了。这在后面的流程中,原理是相同的。就是根据返回值来判断是否消费了事件,整个事件分发机制从上向下,再从下到上这套递归的回溯机制,就是如此。
Activity 当中把事件分发给了 Window,不过 Window 类是
的, Window 的唯一实现 就是
- abstract
,然后这中间的传递过程就比较复杂(因为中间很多源码其实我也没看懂),反正最后就传递到了 Activity 布局中最外层的 ViewGroup 中。 ViewGroup 中的
- PhoneWindow
方法的实现就比较长了,所以我们这里就先从上到下分片段的来看这个方法的具体实现。
- dispatchTouchEvent
- //ViewGroup.java当中的代码
- @Override
- public boolean dispatchTouchEvent(MotionEvent ev) {//首先,在该方法的开头,并没有 super()的代码,这说明 dispatchTouchEvent方法就是在ViewGroup当中实现的,View和ViewGroup当中各有一套实现规则。
- //......
- booleanhandled =false;//......
- // Check for interception.
- final booleanintercepted;if(actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget !=null) {final booleandisallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) !=0;if(!disallowIntercept) {//Notice:注意 这个地方调用了 onInterceptTouchEvent 方法来判断在该ViewGroup当中是否拦截。
- // onInterceptTouchEvent 默认的返回值 是false。intercepted =onInterceptTouchEvent(ev);
- ev.setAction(action);// restore action in case it was changed}else{
- intercepted =false;
- }
- }else{// There are no touch targets and this action is not an initial down
- // so this view group continues to intercept touches.intercepted =true;
- }// If intercepted, start normal event dispatch. Also if there is already
- // a view that is handling the gesture, do normal event dispatch.
- if(intercepted || mFirstTouchTarget !=null) {
- ev.setTargetAccessibilityFocus(false);
- }
而如果在 ViewGroup 中不拦截该事件,或者该事件不该在本 ViewGroup 当中处理,那么就会遍历它的 child 进行处理。
- //ViewGroup.java 的 dispatchTouchEvent(MotionEvent event) 方法
- // ......
- final intchildrenCount = mChildrenCount;if(newTouchTarget ==null&& childrenCount !=0) {final floatx = ev.getX(actionIndex);final floaty = ev.getY(actionIndex);// Find a child that can receive the event.
- // Scan children from front to back.
- finalArrayList preorderedList = buildTouchDispatchChildList();final booleancustomOrder = preorderedList ==null&&isChildrenDrawingOrderEnabled();finalView[] children = mChildren;for(inti = childrenCount -1; i >=0; i--) {final intchildIndex =getAndVerifyPreorderedIndex(
- childrenCount, i, customOrder);finalView child =getAndVerifyPreorderedView(
- preorderedList, children, childIndex);// If there is a view that has accessibility focus we want it
- // to get the event first and if not handled we will perform a
- // normal dispatch. We may do a double iteration but this is
- // safer given the timeframe.
- if(childWithAccessibilityFocus !=null) {if(childWithAccessibilityFocus != child) {continue;
- }
- childWithAccessibilityFocus =null;
- i = childrenCount -1;
- }//......
- resetCancelNextUpFlag(child);//注意这一句调用,将事件往下进行分发。
- if(dispatchTransformedTouchEvent(ev,false, child, idBitsToAssign)) {// Child wants to receive touch within its bounds.mLastTouchDownTime = ev.getDownTime();if(preorderedList !=null) {// childIndex points into presorted list, find original index
- for(intj =0; j < childrenCount; j++) {if(children[childIndex] == mChildren[j]) {
- mLastTouchDownIndex = j;break;
- }
- }
- }else{
- mLastTouchDownIndex = childIndex;
- }
- mLastTouchDownX = ev.getX();
- mLastTouchDownY = ev.getY();
- newTouchTarget =addTouchTarget(child, idBitsToAssign);
- alreadyDispatchedToNewTouchTarget =true;break;
- }//.....
我们可以看到,这里调用了
方法来进行事件分发,那么这个方法里面是做什么的呢。
- dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)
- //ViewGroup.java dispatchTouchEvent()中调用了 该方法
- /**
- * Transforms a motion event into the coordinate space of a particular child view,
- * filters out irrelevant pointer ids, and overrides its action if necessary.
- * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
- */
- private boolean dispatchTransformedTouchEvent(MotionEvent event,booleancancel,
- View child,intdesiredPointerIdBits) {final booleanhandled;// Canceling motions is a special case. We don't need to perform any transformations
- // or filtering. The important part is the action, not the contents.
- final intoldAction = event.getAction();if(cancel || oldAction == MotionEvent.ACTION_CANCEL) {
- event.setAction(MotionEvent.ACTION_CANCEL);if(child ==null) {
- handled =super.dispatchTouchEvent(event);
- }else{//这里将事件分发给了 child,并通过返回值来得到是否消费的结果 handled = child.dispatchTouchEvent(event);
- }
- event.setAction(oldAction);returnhandled;
- }//.......
自此,事件已经分发给了 child,就看 child 会怎么进行处理了。 不过关于上面的那个
方法,除了把事件分发给 child 之外,还有其他作用,就是把事件分发给自己去处理。看代码逻辑的这两行
- dispatchTransformedTouchEvent()
- if(child ==null) {
- handled =super.dispatchTouchEvent(event);
- }else{
因为在
的方法代码中,后面有逻辑判断,如果 child 没有消费事件,(或者事件被拦截,或者压根就没有 child 等)。其实还会调用该方法,只是行参当中 child 的传入值为 null。 那么事件传递到该 ViewGroup 中进行处理,
- dispatchTouchEvent
以上的内容就是 ViewGroup 当中进行处理的相关代码,那么下面就看看 View 当中相关的处理方式。
首先先看看是怎么进行分发的。
- //View 中的 dispatchTouchEvent(MotionEvent event)
- /**
- * Pass the touch screen motion event down to the target view, or this
- * view if it is the target.
- *
- * @param event The motion event to be dispatched.
- * @return True if the event was handled by the view, false otherwise.
- */
- public boolean dispatchTouchEvent(MotionEvent event) {//......
- booleanresult =false;//......
- if(onFilterTouchEventForSecurity(event)) {if((mViewFlags & ENABLED_MASK) == ENABLED &&handleScrollBarDragging(event)) {
- result =true;
- }// 在这里,先查看是否可以调用 OnTouchListener.onTouch ()方法进行处理。
- //noinspection SimplifiableIfStatementListenerInfo li = mListenerInfo;if(li !=null&& li.mOnTouchListener!=null&& (mViewFlags & ENABLED_MASK) == ENABLED
- && li.mOnTouchListener.onTouch(this, event)) {
- result =true;
- }//如果没有调用 OnTouchListener.OnTouch()方法进行处理,那么则再将事件分发到onTouchEvent()当中进行处理.
- if(!result &&onTouchEvent(event)) {
- result =true;
- }
- }//......
- returnresult;
- }
View 中的
方法比着 ViewGroup 当中的简洁了不少,也许是因为 ViewGrop 的主要功能就是包裹子元素所以功能比较复杂吧。
- dispatchTouchEvent
而在 View 中的
中,我们要注意的一点就是
- dispatchTouchEvent
接口的调用,这个方法是在
- OnTouchListener.onTouch(View, MotionEvent)
之前调用的。这也就是说,
- onTouchEvent()
的
- OnTouchListener
方法的优先级是比较高的。 如果我们在代码当中设置了
- boolean onTouch(View, MotionEvent)
(并返回了 true),那么我们事件就被消费了,就不会再分发到
- onTouch(View, MotionEvent)
了。
- onTouchEvent(MotionEvent)
再来看看
的方法。
- onTouchEvent()
- //View 中的 onTouchEvent(MotionEvent)方法
- public boolean onTouchEvent(MotionEvent event) {//......
- if(((viewFlags & CLICKABLE) == CLICKABLE ||
- (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
- (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {switch(action) {caseMotionEvent.ACTION_UP:booleanprepressed = (mPrivateFlags & PFLAG_PREPRESSED) !=0;//......
- // Only perform take click actions if we were in the pressed state
- if(!focusTaken) {// Use a Runnable and post this rather than calling
- // performClick directly. This lets other visual state
- // of the view update before click actions start.
- if(mPerformClick ==null) {
- mPerformClick =new PerformClick();
- }if(!post(mPerformClick)) {performClick();
- }
- }
- }//.....
- break;
在处理
当中,最后调用的是
- MotionEvent.ACTION_UP
方法。 而
- performClick()
其实是这样的。
- perfomClick()
- /**
- * Call this view's OnClickListener, if it is defined. Performs all normal
- * actions associated with clicking: reporting accessibility event, playing
- * a sound, etc.
- *
- * @return True there was an assigned OnClickListener that was called, false
- * otherwise is returned.
- */
- public boolean performClick() {final booleanresult;finalListenerInfo li = mListenerInfo;if(li !=null&& li.mOnClickListener!=null) {playSoundEffect(SoundEffectConstants.CLICK);
- li.mOnClickListener.onClick(this);
- result =true;
- }else{
- result =false;
- }//......
- returnresult;
- }
在这个方法当中,我们终于看到了熟悉的
接口,我们平时最常用的
- OnClickView
接口的
- OnClickView
方法就是在这里被调用的。 结合前面的分析,我们也可以知道。
- onClick(View)
接口的优先级比
- onTouchListener
要高。
- OnClickListener
其实到这里,认真思索的朋友肯定会有疑问,我们刚才说,判断是否消费了事件,是根据返回值来判断的,
方法也 的确是有返回值的。但是认真看看在
- boolean performClick()
方法中 调用到
- onTouchEvent(Motion)
的地方,
- performClick()
方法压根就没接收这个返回值啊。这是搞什么鬼呢。 我们此时再来看看
- onTouchEvent()
方法:
- onTouchEvent(MotionEvent)
```java
//......
- if ((viewFlags & ENABLED_MASK) == DISABLED) {
- if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
- setPressed(false);
- }
- // A disabled view that is clickable still consumes the touch
- // events, it just doesn't respond to them.
- return (((viewFlags & CLICKABLE) == CLICKABLE
- || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
- || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
- }
- //......
- //.......
- if (((viewFlags & CLICKABLE) == CLICKABLE ||
- (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
- (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
- switch (action) {
- case MotionEvent.ACTION_UP:
- // ......
- break;
- case MotionEvent.ACTION_DOWN:
- // ......
- break;
- case MotionEvent.ACTION_CANCEL:
- // ......
- break;
- case MotionEvent.ACTION_MOVE:
- // ......
- break;
- }
- return true;
- }
- return false;
```
从代码里可以得到结论,只要 这个控件的
和
- CLICKABLE
有一个为 true,那么代码就会向下走,返回值就是 true。也就是说 默认在这个 View 中,已经消费了这个事件了。
- LONG_CLICKABLE
但是这中间还有一个疑问, 其实对于 View,
默认为 false,而关于
- LONG_CLICKABLE
则不同的子类有不同的默认值。 比如
- CLICKABLE
的
- Button
默认为 true,但是
- CLICKABLE
的
- TextView
默认就是 false。 可是对于
- CLICKABLE
,依旧可以正常使用
- TextView
啊,其实原因也非常简单。 看一下 View 的源码就知道了.
- setOnClickListener
- /**
- * Register a callback to be invoked when this view is clicked. If this view is not
- * clickable, it becomes clickable.
- *
- * @param l The callback that will run
- *
- * @see #setClickable(boolean)
- */
- public void setOnClickListener(@NullableOnClickListener l) {if(!isClickable()) {setClickable(true);
- }getListenerInfo().mOnClickListener= l;
- }
顺便再看看
方法。
- setOnLongClickListener
- /**
- * Register a callback to be invoked when this view is clicked and held. If this view is not
- * long clickable, it becomes long clickable.
- *
- * @param l The callback that will run
- *
- * @see #setLongClickable(boolean)
- */
- public void setOnLongClickListener(@NullableOnLongClickListener l) {if(!isLongClickable()) {setLongClickable(true);
- }getListenerInfo().mOnLongClickListener= l;
- }
还有一个疑问,当控件处于 disable 时,还会消费事件吗?
答案还是从源码里找,就在上面那个源码当中的 这几句。
- if((viewFlags & ENABLED_MASK) == DISABLED) {if(action == MotionEvent.ACTION_UP&& (mPrivateFlags & PFLAG_PRESSED) !=0) {setPressed(false);
- }// A disabled view that is clickable still consumes the touch
- // events, it just doesn't respond to them.
- return(((viewFlags & CLICKABLE) == CLICKABLE
- || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
- || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
- }
其实光看注释就够了。。
A disabled view that is clickable still consumes the touch events, it just doesn't respond to them.
disable 情况下, 发生 MotionEvent 事件时,看上去没有反应,但是是否消费了事件,依旧看
或
- CLICKABLE
的值。
- LONG_CLICKABLE
以上只是源码肤浅的分析,在实际当中,还有几点需要了解。
事件 (
- ACTION_DOWN
方法返回了
- onTouchEvent
),那么(同一个事件序列中)剩下的事件也会交给它处理,并不会再调用
- true
方法来询问是否拦截了。
- onInterceptTouchEvent
事件,那么(同一个事件序列中)剩下的事件不会再交给它处理,剩下的事件会交给它的父元素处理,父元素的
- ACTION_DOWN
方法会被调用。而如果该 View 虽然消费了
- onTouchEvent
事件,但是并没有消费除
- ACTION_DOWN
以外的其他事件,那么这些事件将会消失,并不会再调用它的父元素的
- ACTION_DOWN
进行处理。
- onTouchEvent
事件是事件序列的起源,也是非常重要的一个事件。
- ACITON_DOWN
方法来干涉父元素中的传递过程。(不过这点目前没弄清楚具体的原理和使用方法,所以就不多展开了,免得误导别人)。
- requestDisallowTouchEvent
那么自此,关于 View 的事件分发机制就分析完了。。而关于源码部分,其实分析的也不够彻底和深刻。一来自己水平的确有限,二来了解大概的脉络,就能处理大部分问题了。
具体关于事件分发的测试代码,可以参见 https://github.com/yaowen369/BlogDemo/tree/master/Android/AndroidBlogDemo/app/src/main/java/com/yaoxiaowen/android_blog_demo/dispatch_event/dispatch_test 的代码,尝试修改各个地方的返回值,然后结合理论分析,来预测实际输出的后果。
来源: http://www.cnblogs.com/yaoxiaowen/p/6925702.html