【Android】事件分发机制

作者: 黑暗终将过去 | 来源:发表于2018-05-31 22:37 被阅读103次

    一、事件分发机制过程

    Android事件分发机制是Android开发必须掌握的东西,分发的事件是点击Touch事件,在Android中对应的是MotionEvent对象。

    该对象类型主要有三种:

    含义
    MotionEvent.ACTION_DOWN 按下View。
    MotionEvent.ACTION_UP 松开View。
    MotionEvent.ACTION_MOVE 移动View。

    整个过程会形成一个事件列:从用户点击View开始传递MotionEvent.ACTION_DOWN事件,然后随着用户移动,会传递N多个MotionEvent.ACTION_MOVE事件,最后用户松开手指,传递MotionEvent.ACTION_UP事件。

    整个事件传递的过程就是一个事件分发的过程。这个过程参与的对象主要有Activity->Window->ViewGroup->View。

    二、事件分发机制三个重要方法

    事件分发机制的三个重要方法如下:

    方法 作用
    dispatchTouchEvent() 分发事件。
    onInterceptTouchEvent() 判断是否进行事件拦截。
    onTouchEvent() 点击事件处理。

    任玉刚大神的《Android开发艺术探索》里面有一段伪代码表达的非常清楚~

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

    对于传入的事件,首先调用diapatchTouchEvent方法开始进行分发,然后调用onInterceptTouchEvent来判断是否进行拦截,如果要拦截,那么事件就在这个ViewGroup进行处理了,onTouchEvent会被调用。如果不拦截事件,就会继续传递给子View的dispatchTouchEvent进行处理。层层传递,直到事件被拦截。

    三、源码

    源码基于Android7.1。

    1、从Activity出发。里面的dispatchTouchEvent方法如下。

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

    可以看见调用了getWindow().superDispatchTouchEvent(ev)方法,如果这个方法返回true,就直接返回,即消费了,否则Activity会调用onTouchEvent方法。

    2、先看Window的处理,getWindow获取对应的Window,进入Window.java找对应的方法。可以看见是一个抽象的方法。

    public abstract boolean superDispatchTouchEvent(MotionEvent event);
    

    具体实现在哪呢?可以看见前面的说类说明有这么一段话。

    The only existing implementation of this abstract class is android.view.PhoneWindow
    

    所以具体实现类是PhoneWindow,进入PhoneWindow.java,看superDispatchTouchEvent方法。

    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
    

    可以看见直接调用了mDecor的superDispatchTouchEvent方法,即Windows其实对事件没有进行任何处理。

    mDecor是什么呢?

    // This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;
    

    下面有一张图其实比较清楚的能展示DecorView。

    DecorView.png

    可以看见DecorView其实就是我们的顶层View,它是一个FrameLayout布局,从源码也可以看见它是继承自FrameLayout。里面是一个线性布局,包含一个TitleBar一个content,content就是我们每次在setContent的内容,即我们自定义的布局。

    public class DecorView extends FrameLayout
    

    说到这里呢,再返回去看superDispatchTouchEvent函数,这个调用了super的dispatchTouchEvent方法,一直往父类看,可以看见super的dispatchTouchEvent方法在FrameLayout没有实现,再往上是ViewGroup类,这里面就有实现了,所以DecorView其实直接调用了ViewGroup的dispatchTouchEvent方法。

    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }
    

    3、看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;
        }
    

    intercepted是来标记是否在这个地方进行拦截,先不管最外面的else,先从最里面看起,最里面调用了onInterceptTouchEvent来判断是否拦截,即伪代码里面的内容。

    往外有个判断disallowIntercept,如果mGroupFlags有FLAG_DISALLOW_INTERCEPT这个标记的话,直接就不拦截,FLAG_DISALLOW_INTERCEPT这个是什么呢?这个其实是在子View里面设置的,即不允许父容器拦截,设置这个标记之后,父容器就不进行拦截。

    但是往前还有这么一段话。

        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Throw away all previous state when starting a new touch gesture.
            // The framework may have dropped the up or cancel event for the previous gesture
            // due to an app switch, ANR, or some other state change.
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }
    

    即当MotionEvent.ACTION_DOWN事件的时候,会执行resetTouchState函数,这个函数里面有个处理就是去掉这个标记。所以开发中有时候遇到requestDisallowInterceptTouchEvent设置失效,就是在这里失效的。

    mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    

    再往外有一个mFirstTouchTarget这个标记,这个标记是啥呢?mFirstTouchTarget的意思是,如果ViewGroup的有子元素成功处理,mFirstTouchTarget就会指向该元素。
    如果onInterceptTouchEvent()返回true,说明ViewGroup拦截事件,mFirstTouchTarget为null,同一序列的事件都由它处理,onInterceptTouchEvent也不会再调用了,因为actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null条件都不满足。

    最后看看onInterceptTouchEvent方法,可以看见,默认是不拦截的。

    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) {
            return true;
        }
        return false;
    }
    

    分析到这里,先得出一点小结论:

    • 当ViewGroup开始拦截事件之后,后面的事件列都会交给它,且不调用onInterceptTouchEvent方法。
    • 子元素可以设置FLAG_DISALLOW_INTERCEPT标记,这样父元素ViewGroup就不会进行拦截,但是有个前提就是父元素一开始不拦截MotionEvent.ACTION_DOWN。
    • 只有diapatchTouchEvent一定每次调用,onInterceptTouchEvent不一定每次调用。
    • ViewGroup的onInterceptTouchEvent方法默认是不拦截的。

    继续,如果不拦截的话会干嘛呢?即在if (!canceled && !intercepted)这个if条件里面,直接看里面关键部分。

    final View[] children = mChildren;
    for (int i = childrenCount - 1; i >= 0; i--) {
        final int childIndex = getAndVerifyPreorderedIndex(
                    childrenCount, i, customOrder);
        final View child = getAndVerifyPreorderedView(
                    preorderedList, children, childIndex);
        ...
    }
    

    这里是遍历所有的子View。之后怎么处理呢,往下看。接下来有几个判断。

    if (!canViewReceivePointerEvents(child)
            || !isTransformedTouchPointInView(x, y, child, null)) {
        ev.setTargetAccessibilityFocus(false);
        continue;
    }
    

    canViewReceivePointerEvents函数。从代码可以看出这个是在判断View可见并且没有播放动画才可以接收。

    /**
     * Returns true if a child view can receive pointer events.
     * @hide
     */
    private static boolean canViewReceivePointerEvents(@NonNull View child) {
        return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                || child.getAnimation() != null;
    }
    

    isTransformedTouchPointInView函数。从代码可以看出必须要点在View内才可以,这里是用View的Top和Left参数判断,即View的真身。

    /**
     * Returns true if a child view contains the specified point when transformed
     * into its coordinate space.
     * Child must not be null.
     * @hide
     */
    protected boolean isTransformedTouchPointInView(float x, float y, View child,
            PointF outLocalPoint) {
        final float[] point = getTempPoint();
        point[0] = x;
        point[1] = y;
        transformPointToViewLocal(point, child);
        final boolean isInView = child.pointInView(point[0], point[1]);
        if (isInView && outLocalPoint != null) {
            outLocalPoint.set(point[0], point[1]);
        }
        return isInView;
    }
    
    /**
     * @hide
     */
    public void transformPointToViewLocal(float[] point, View child) {
        point[0] += mScrollX - child.mLeft;
        point[1] += mScrollY - child.mTop;
    
        if (!child.hasIdentityMatrix()) {
            child.getInverseMatrix().mapPoints(point);
        }
    }
    

    综上,遍历之后判断子元素View是否可以接收事件的条件有三个:

    • View可见
    • View没有在播放动画
    • 点击的点的坐标在View里面

    然后,如果可以接收,继续执行。

    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;
     }
    

    接着一句句看上面的部分,首先是dispatchTransformedTouchEvent函数。直接看关键代码,一般关键代码都在后面。

    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        ...
        handled = child.dispatchTouchEvent(transformedEvent);
    }
    

    可以看见,如果子View不为null,则调用child.dispatchTouchEvent(transformedEvent)将事件分发下去。如果子元素处理了,那么dispatchTransformedTouchEvent会返回true。返回true之后会调用执行newTouchTarget = addTouchTarget(child, idBitsToAssign),这里就是对mFirstTouchTarget 进行了赋值,前面讲的mFirstTouchTarget 就是在这里赋值的。

    如果在遍历完子View以后ViewGroup仍然没有找到事件处理者即ViewGroup并没有子View或者子View处理了事件,但是子View的dispatchTouchEvent返回了false(一般是子View的onTouchEvent方法返回false)那么ViewGroup会去处理这个事件。即dispatchTransformedTouchEvent返回了false,那么mFirstTouchTarget 就为null。

    if (mFirstTouchTarget == null) {
        // No touch targets so treat this as an ordinary view.
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                TouchTarget.ALL_POINTER_IDS);
    }
    

    这时候还是调用dispatchTransformedTouchEvent,只是有一个不同是此时第三个参数也就是child传入了null。再贴一次代码,可以看见此时调用super.dispatchTouchEvent(transformedEvent),即调用父类View的dispatchTouchEvent方法。

    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        ...
        handled = child.dispatchTouchEvent(transformedEvent);
    }
    

    4、此时看View.java的dispatchTouchEvent方法。

    看最关键的几句话。

    if (li != null && li.mOnTouchListener != null
            && (mViewFlags & ENABLED_MASK) == ENABLED
            && li.mOnTouchListener.onTouch(this, event)) {
        result = true;
    }
    
    if (!result && onTouchEvent(event)) {
        result = true;
    }
    

    如果View设置了OnTouchListener且这个监听里面的onTouch返回true,那么onTouchEvent就不被调用。反之,调用自身的onTouchEvent方法。

    接下来看onTouchEvent方法。这个方法比较长。由一个个判断来看。

    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);
    }
    

    首先,如果View被设置为disable,那么只要CLICKABLE和LONG_CLICKABLE有一个为true,就一定会消费这个事件。View的longClickable默认为false,clickable需要区分情况,如Button的clickable默认为true,而TextView的clickable默认为false。

    if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
        ...
    }
    

    接下来,即View为enable的时候,只要CLICKABLE和LONG_CLICKABLE有一个为true,也会消费这个事件。里面是一些列的action判断,这里主要看ACTION_UP,执行了一个performClick()方法。这个方法即如果设置了mOnClickListener监听,那么就会执行对应的监听里面的onClick方法。

    public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        return result;
    }
    

    从这里可以总结出优先级OnTouchListener>OnTouchEvent>OnClickListener,当设置了OnTouchListener且OnTouch方法返回true的时候,就不再执行后面两个。

    同时可以看见,如果View全部不进行消费,那么事件又会一层层回传,直到Activity那儿执行onTouchEvent方法。回到Activity的dispatchTouchEvent方法。

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

    看下对应的onTouchEvent方法,这里判断了一个shouldCloseOnTouch方法,这个其实只是判断是否点在了空白区域,所以点击空白区域会关闭就是因为这里执行了finish()。

    public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }
        return false;
    }
    

    四、太乱了,总结一下

    所有入口都是dispatchTouchEvent,从Activity->Window->ViewGroup->View依次传入,如果onInterceptTouchEvent为true(不一定都执行)拦截,否则继续一级级往下传。事件处理onTouchEvent为true则消费掉,否则原路返回一级级往上传。

    任玉刚大神的《Android开发艺术探索》给出了11点结论帮助理解。

    1、同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以down事件开始,中间含有数量不定的move事件,最终以up事件结束。

    2、正常情况下,一个事件序列只能被一个View拦截且消耗。因为前面源码分析过了,当一个事件交给一个View执行之后,就不再执行onInterceptTouchEvent进行判断了。但是通过特殊手段可以使得事件列里面不同事件被不同View处理,比如一个View本该处理然后通过onTouchEvent强行转给其他View。

    3、某一个View一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件序列能够传递到它的话),并且它的onInterceptTouchEvent不会再被调用。这条也很好理解,就是说当一个View决定拦截一个事件后,那么系统会把同一个时间序列内的其他方法都交给它来处理,因此就不用再调用这个View的onInterceptTouchEvent去询问它是否要拦截啦。参考2。

    4、当某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。意思就是事件一旦交给一个View处理,那么它就必须消耗掉,否则同一事件序列中剩下的事件就不再交给它来处理了,这就好比上级交给程序员一件事,如果这件事没有处理好,短期内上级就不敢再把事情交给这个程序员做了,两者类似。

    5、如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失(当然了:点击事件需要消耗,DOWN 和 UP事件的),此时父元素的onTouchEvent并不会被调用,并且当前的View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理。

    6、ViewGroup默认不拦截任何事件。Android源码中的ViewGroup的onInterceptTouchEvent方法默认不拦截false。

    7、View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用。

    8、View的onTouchEvnet默认都会消耗(返回true),除非它是不可点击的(clickable和longClickable同时为false)。View的longClickable属性默认都为false,clickable属性要分请情况,比如Button的clickable属性默认为true,而TextView 的 clickable属性默认为false。

    9、View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只要它的clickable或则longClickable有一个为true,那么它的onTouchEvent就返回true。

    10、onClick会发生的前提是当前View是可点击的,并且他收到了down和up的事件。

    11、事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。

    相关文章

      网友评论

      本文标题:【Android】事件分发机制

      本文链接:https://www.haomeiwen.com/subject/ljhqsftx.html