图解+源代码 全面理解Android View事件分发

来源:互联网 发布:画四格漫画的软件 编辑:程序博客网 时间:2024/06/07 06:22

View事件分发是Android里面非常重要的知识点,我查阅了很多资料,新建demo分析Log日志加以验证,总结成这篇博文。

宏观的角度

View 事件分发涉及到三种角色,三个方法,三种重点事件,分别是Activity,ViewGroup,View;dispatchTouchEvent, onInterceptTouchEvent,onTouchEvent和ACTION_DOWN,ACTION_MOVE,ACTION_UP。

三个方法的关系可概括为如下伪代码:

    public boolean dispatchTouchEvent(MotionEvent ev){      boolean consume = false;      if (onInterceptTouchEvent(ev)){        consume = onTouchEvent(ev);      } else {        consume = child.dispatchTouchEvent(ev);      }      return consume;    }

值得记住的几个结论:

  1. activity的dispatchTouchEvent无论返回true or false都没有意义,但当View or ViewGroup的dispatchTouchEvent返回true时,表示它要消耗了该事件,并且该事件不会再向下传递。
  2. onTouchEvent返回ture表示它要消耗该事件,并且该事件不会再往下或往上传递。同一个View or ViewGroup的dispatchTouchEvent的返回值会与onTouchEvent的返回值同步,也就是说当onTouchEvent返回true,它的dispatchTouchEvent也会返回true。
  3. 因此,第一个View or ViewGroup要消耗一个事件,除了可以在onTouchEvent中返回true,还可以在dispatchTouchEvent中返回true。而前者是后者的充分非必要条件,
  4. 一个事件序列包含若干个事件,一般地,ACTION_DOWN是一个事件序列的开头,ACTION_UP是一个事件序列的结尾。
  5. 一个事件序列只能被一个VIew拦截且消耗。同一个事件序列中的事件不能分别由两个View同时处理。某个View一旦消耗了ACTION_DOWN事件,那么这一个事件序列的事件,都会从上往下一直传递到这个View为止(但往下传递的过程中上层的onInterceptTouchEvent仍会被调用,上层仍可以拦截事件),而且它的onInterceptTouchEvent不会再被调用了(如果有的话)。
  6. 如果一个事件序列本来要交给下层某一View or ViewGroup(代号A)来处理,但某个事件从上往下传递的过程中被上层某一个ViewGroup(代号B)拦截(onInterceptTouchEvent return true),那么A会收到一个ACTION_CANCEL事件,而原事件也消失了。此事件序列以后的事件就不会再传到A而是传给B,B获得了事件序列处理权。

ACTION_DOWN的事件分发图解

事件分发图
图源:http://www.jianshu.com/p/e99b5e8bd67b#rd

  • 带箭头的虚线上的super表示在起点方法内调用父类的重写方法后事件的流向,也就是说带super的虚线标识着事件默认的流动。
  • 带箭头的虚线上的true or false标识起点方法返回值,方法返回后事件的流动,
  • 令dispatchTouchEvent 直接返回false时,系统会回调父容器的onTouchEvent,事件会回流给父容器。
    @Override    public boolean dispatchTouchEvent(MotionEvent ev) {        return false;    }
  • ACTION_MOVE,ACTION_UP与ACTION_DOWN的事件分发的异同可以概括为:ACTION_MOVE,ACTION_UP跟ACTION_DOWN从上往下传递,只不过当ACTION_MOVE,ACTION_UP传到拥有事件序列接管权的那一层View or ViewGroup时,不再调用该层的onInterceptTouchEvent 了,而是直接调用该层的onTouchEvent,不管它返回true or false,事件不再往别处传递。举个例子:Activity中依次套着ViewGroup1(ViewGroup2(View)) ,ACTION_UP在ViewGroup的onTouchEvent中被消费(红线表示),那么之后同一个事件序列的ACTION_MOVE,ACTION_UP的流向都会像蓝线那样。

ACTION_MOVE,ACTION_UP
图源:http://www.jianshu.com/p/e99b5e8bd67b#rd

源代码的角度

Activity对事件的分发过程

当一个点击操作发生时,事件最先传递到当前的Activity,由Activity的dispatchTouchEvent进行事件分发。

    public boolean dispatchTouchEvent(MotionEvent ev) {        if (ev.getAction() == MotionEvent.ACTION_DOWN) {            onUserInteraction();        }        if (getWindow().superDispatchTouchEvent(ev)) {            return true;        }        return onTouchEvent(ev);    }

事件会先传递给PhoneWindow中的DecorView,如果DecorView不能处理,则调用自己的onTouchEvent。

ViewGroup对事件的分发过程

在ViewGroup的dispatchTouchEvent中,有如下代码判断是否拦截点击事件。

            // Check for interception.            final boolean intercepted;            if (actionMasked == MotionEvent.ACTION_DOWN                    || mFirstTouchTarget != null) {                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;                if (!disallowIntercept) {                    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;            }

当事件由VIewGroup的子元素消耗时,mFirstTouchTarget会指向该元素。mFirstTouchTarget其实是一种单链表结构,如果mFirstTouchTarget==null,那么ViewGroup就默认拦截接下来同一事件序列的事件。

  • (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null)==false时,ViewGroup的onInterceptTouchEvent不会再被调用,并且同一个事件序列的其他事件都会默认交给它处理,因为此时的传过来的事件为ACTION_MOVE or ACTION_UP,而且没有一个子元素能处理,那就这能交给自己来处理了。
  • (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null)==true,这时,是否拦截事件取决于标记位FLAG_DISALLOW_INTERCEPT,它是通过requestDisallowInterceptTouchEvent方法来设置,一般用于子View请求让父容器不要拦截除了ACTION_DOWN以外的事件。可以用于解决滑动冲突。

接着根据点击坐标定位候选子元素,并将事件逐个分发给子元素来处理。

                for (...){                    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 (int j = 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;                            }                        }                }

如果某一子元素成功处理,mFirstTouchTarget会在addTouchTarget内被赋值。

View对事件的分发过程

在View的dispatchTouchEvent方法中进行事件分发

    public boolean dispatchTouchEvent(MotionEvent event) {        boolean result = false;        if (onFilterTouchEventForSecurity(event)) {            //noinspection SimplifiableIfStatement            ListenerInfo li = mListenerInfo;            if (li != null && li.mOnTouchListener != null                    && (mViewFlags & ENABLED_MASK) == ENABLED                    && li.mOnTouchListener.onTouch(this, event)) {                result = true;            }            if (!result && onTouchEvent(event)) {                result = true;            }        }    }

首先会判断是否设置了OnTouchListener,如果OnTouchListener#onTouch返回true,那么onTouchEvent就不会被调用了。由此可见,OnTouchListener#onTouch的优先级高于onTouchEvent。

在View中的onTouchEvent中会对点击事件做处理

        if (((viewFlags & CLICKABLE) == CLICKABLE ||(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {          ...                            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();                                }                            }          return true;        } else {          return false;        }

只要VIew的CLICKABLE和LONG_CLICKABLE有一个为true,那么它就会消耗这个事件。在ACTION_UP事件传来时,如果VIew设置了OnClickListener,那么performClick方法内部会调用它的onClick方法。

希望大家能对Android View事件分发有更深刻的理解,共勉。

参考资料:

  • 图解 Android 事件分发机制

  • Android 开发艺术探索

1 0
原创粉丝点击