美文网首页
Android View事件分发机制源码详解

Android View事件分发机制源码详解

作者: 脏仙人 | 来源:发表于2019-04-06 02:09 被阅读0次

    Activity构成

    点击事件由MotionEvent来表示,当一个点击事件产生后,事件最先传递给Activity。所以我们大致了解一下Activity的构成

    Activity构成.png
    • PhoneWindow:Window抽象类的实现类,我们使用getWindow()方法得到的就是一个PhoneWindow
    • DecorView:Activity中的根View,继承了FrameLayout
    • TitleView:DecorView中的子View
    • ContentView:DecorView的子View,我们平常应用所写的布局展示在这里

    点击事件的传递规则

    当我们点击屏幕时,就产生了点击事件,这个事件被封装成了一个类:MotionEvent。然后系统会将产生的MotionEvent传递给View的层级,MotionEvent传递的过程就是点击事件分发。点击事件分发过程由下面三个很重要的方法来完成。

    1. public boolean dispatchTouchEvent(MotionEvent ev): 用来进行事件的分发,如果事件能够传递给当前View,此方法一定会被调用。返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。
    2. public boolean onInterceptTouchEvent(MotionEvent ev):在dispatchTouchEvent方法的内部调用,用来进行事件的拦截。如果ViewGroup拦截了某个事件,那么在同一个事件序列中此方法不会再次调用,返回结果表示是否拦截当前事件。
    3. public boolean onTouchEvent(MotionEvent ev):在dispatchTouchEvent方法中调用,用来处理点击事件。返回结果表示是否消耗当前事件,如果不消耗则在同一个事件序列中,当前View无法再次接受到事件
    上述三个方法的关系用伪代码表示如下:
    public boolean dispatchTouchEvent(MotionEvent ev){
        boolean consume = false;
        if(onInterceptTouchEvent(ev)){
            consume=onTouchEvent(ev)
        }else{
            consume=child.dispatchTouchEvent(ev);
        }
        return consume;
    }
    

    通过上面伪代码我们可以了解到,如果一个根ViewGroup拿到点击事件后首先会调用它的 dispatchTouchEvent方法,然后判断onInterceptTouchEvent方法是否返回true。返回ture表示拦截当前点击事件,然后就会调用它自己的onTouchEvent方法来处理点击事件;返回false表示不拦截当前点击事件,事件就会传递到子元素的,调用子元素的dispatchTouchEvent方法进行分发重复上面的步骤,直到事件被最终处理。

    如果View设置了OnTouchListener,那么OnTouchListener中的onTouch方法会被调用,这时如果onTouch方法返回false,onTouchEvent方法才会被调用,可以看到onTouch方法的优先级比onTouchEvent方法高。

    如果我们的View设置了onClickListener,那么onClick方法会被调用。onClick方法在onTouchEvent方法中调用优先级最低,而且只能监听到点击事件。

    当点击事件产生后,事件首先会传递给当前的Activity,这里会调用Activity的dispathTouchEvent方法,当然具体的事件处理工作都是交由Activity中PhoneWindow来完成,然后PhoneWindow再把事件处理工作交给DecorView,之后事件处理工作交给根ViewGroup。

    考虑一种情况如果一个View的onTouchEvent方法返回true,那么它的父容器的onTouchEvent方法就会被调用,如果所有的元素都不处理这个事件,事件最终就会传递给Activity处理,即Activity的onTouchevent方法就会被调用。

    1. 同一个事件序列是指手指从接触屏幕的那一刻起,到手指离开屏幕的那一刻,由一个down事件开始,中间有一个或多个move事件,最终以up事件结束
    2. 正常情况下一个事件只能被一个View拦截消耗,因为一旦一个元素拦截了点击事件,那么同一个事件序列内的所有事件都会直接交给它处理。因此同一个事件序列中的事件不能分别交给两个View同时处理,但是通过特殊手段可以做到,比如一个View将本该自己处理的事件通过onTouchEvent强行传递给其他View处理
    3. 某个View一旦决定拦截,一整个事件序列都只能由它来处理(如果事件序列能传递给他的话),并且他的onInterceptTouchEvent不会再被调用。
    4. 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理。
    5. 如果View不消耗掉除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续事件,最终这些消失的点击事件会传递给Activity处理
    6. ViewGroup默认不拦截任何事件,源码中ViewGroup的onInterceptTouchEvent方法默认返回false
    7. View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,它的onTouchEvent方法就会被调用。
    8. View的onTouchEvent默认会消耗掉点击事件(返回true),除非他是不可点击的(clickable和longClickable同时为false),View的LongClickable默认都为false,clickable分情况比如Button的clickable为true,TextView默认为false
    9. View的enable属性不影响onTouchEvent的默认返回值,哪怕一个View是disable状态的,只要它的clickable或者LoneClickable一个为true那么它的onTouchEvent就返回true
    10. onClick会发生的前提是当前View是可点击的并且它收到了down和up事件
    11. 事件传递是由外向内的,事件总是先传递给父元素在由父元素分发给子View,子View通过requestDisallowInterceptTouchEvent 方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。

    ViewGroup事件分发的源码解析

    点击事件由MotionEvent来表示,点击事件最先会传递给Activity进行处理我们先从Activity的dispatchTouchEvent开始分析。

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

    我们可以看到事件首先交给Activity附属的Window进行分发,如果返回true整个事件循环就结束了,返回false意味着点击事件没人处理,所有的View的onTouchEvent都返回了false,那么Activity的onTouchEvent就会被调用。

    接下来我们看Window是怎么处理点击事件的。

     public abstract boolean superDispatchTouchEvent(MotionEvent event);
    

    我们看到Window是一个抽象类,而Window的superDispatchTouchEvent也是一个抽象方法,我们找到它的实现类。

    The only existing implementation of this abstract class is android.view.PhoneWindow, which you should instantiate when needing a Window
    

    从注释我们可以看到Window的实现类是android.view.PhoneWindow。然后我们看PhoneWindow中的superDispatchTouchEvent方法

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

    PhoneWindow直接将点击事件传递给了DecorView,DecorView是什么呢

    public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks 
    

    通过上面代码可以看到DecorView就是就是一个FrameLayout。
    它也是Activity中的根View。它包含了一个TitleView和一个ContentView,而ContentView就是我们在Activity中通过setContentView设置进去的布局。
    在使用中我们通过 ((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)可以获取到我们在Activity设置的View,而getWindow().getDecorView()很显然返回的就是DecorView。

    由于DecorView继承至FrameLayout而且是父View,我们知道FrameLayout属于ViewGroup所以我们接下来看一下ViewGroup的事件分发过程,从ViewGroup的dispatchTouchEvent方法开始。方法的代码比较长分段来说明

        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            //如果事件为Down对事件进行初始化
            if (actionMasked == MotionEvent.ACTION_DOWN) {//1
                cancelAndClearTouchTargets(ev);
                //resetTouchState方法中会把mFirstTouchTarget重置为null
                resetTouchState();
            }
    
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {//2
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//3
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                intercepted = true;
            }
    
    • 注释1处:如果事件为ACTION_DOWN首先会对点击事件进行初始化,并且在resetTouchState方法中会把mFirstTouchTarget重置为null也会重置FLAG_DISALLOW_INTERCEPT标志位。(这里进行初始化是因为一个完整的事件序列是从DOWN开始到UP事件结束,所以如果是Down事件说明是一个新的事件序列,所以进行初始化重置为默认状态)
    • 注释2处:ViewGroup会在事件类型为ACTION_DOWN或者mFirstTouchTarget != null这两种情况下判断是否拦截当前事件,mFirstTouchTarget != null是什么意思呢?从后面的代码逻辑可以看出来,当事件由ViewGroup的子元素成功处理时mFirstTouchTarget会被赋值并指向子元素。也就是说ViewGroup不拦截事件并将事假交给子元素处理时mFirstTouchTarget != null。一旦事件被当前的ViewGroup拦截时mFirstTouchTarget != null就不成立,那么当ACTION_MOVE,和ACTION_UP事件到来时,由于(actionMasked == MotionEvent.ACTION_DOWN
      || mFirstTouchTarget != null)为false将导致onInterceptTouchEvent不被调用,并且序列的其他事件都将默认交给此ViewGroup处理。
    • 注释3处:这里出现了一个FLAG_DISALLOW_INTERCEPT标志位,这个标记通过requestDisallowInterceptTouchEvent方法来设置一般用于子View中,FLAG_DISALLOW_INTERCEPT设置后ViewGroup无法拦截除了ACTION_DOWN以外的其他点击事件,因为FLAG_DISALLOW_INTERCEPT标志位会在ViewGroup的ACTION_DOWN事件里进行重置所以requestDisallowInterceptTouchEvent方法并不能影响ViewGroup对ACTION_DOWN事件的处理
    • 从上面的分析我们知道onInterceptTouchEvent方法不是每次事件都会被调用,如果我们想提前处理所有的点击事件要选择dispatchTouchEvent方法。且onInterceptTouchEvent方法在源码中默认返回false不进行事件拦截如果要拦截事件需要重写这个方法返回true

    当ViewGroup不拦截事件的时候,事件会向下分发给她的子View进行处理,源码如下:

    final View[] children = mChildren;
    for (int i = childrenCount - 1; i >= 0; i--) {//1
        final int childIndex = getAndVerifyPreorderedIndex(
                childrenCount, i, customOrder);
        final View child = getAndVerifyPreorderedView(
                preorderedList, children, childIndex);
    
        if (childWithAccessibilityFocus != null) {
            if (childWithAccessibilityFocus != child) {
                     continue;
            }
            childWithAccessibilityFocus = null;
            i = childrenCount - 1;
        }
    
        if (!canViewReceivePointerEvents(child)
                    || !isTransformedTouchPointInView(x, y, child, null)) {//2
            ev.setTargetAccessibilityFocus(false);
            continue;
        }
    
        newTouchTarget = getTouchTarget(child);
        if (newTouchTarget != null) {
            // Child is already receiving touch within its bounds.
            // Give it the new pointer in addition to the ones it is handling.
            newTouchTarget.pointerIdBits |= idBitsToAssign;
            break;
        }
    
        resetCancelNextUpFlag(child);
        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {//3
            // 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);//4
            alreadyDispatchedToNewTouchTarget = true;
            break;
        }
    
        // The accessibility focus didn't handle the event, so clear
        // the flag and do a normal dispatch to all children.
        ev.setTargetAccessibilityFocus(false);
    }
    
    • 注释1处:首先遍历ViewGroup的子元素,判断子元素是否能接受到点击事件,如果子元素能够接收到点击事件则交给子元素来处理。需要处理这个遍历是倒叙遍历的,即从最上层的子View开始往内遍历
    • 注释2处:判断触摸点是否在子View的范围内,或者子View是否在播放动画,如果满足这两个条件则事件传递给它来处理
    • 注释3处:dispatchTransformedTouchEvent方法实际上就是调用了子元素的dispatchTouchEvent方法源码如下:
    if (child == null) {
        handled = super.dispatchTouchEvent(event);
    } else {
        handled = child.dispatchTouchEvent(event);
    }
    

    上面的代码可以看到如果传递的child不是null就会调用child.dispatchTouchEvent(event)进行事件分发。如果子元素的dispatchTouchEvent方法返回false,ViewGroup就会把事件分发给下一个子元素。(如果还有下一个子元素的话)

    • 注释4处:这里完成了mFirstTouchTarget的赋值并终止对子元素的变量。mFirstTouchTarget真正的赋值操作是在addTouchTarget内部完成的,mFirstTouchTarget其实是一种单链表结构。mFirstTouchTarget是否被赋值直接影响到ViewGroup对事件的拦截策略,前面已经说过,如果mFirstTouchTarget为null,那么ViewGroup就默认拦截接下来同一序列中所有的点击事件。
     private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
            final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
            target.next = mFirstTouchTarget;
            mFirstTouchTarget = target;
            return target;
        }
    

    如果变量所有子元素事件都没有被合适的处理比如ViewGroup没有子元素或者子元素处理了点击事件但是在dispatchTouchEvent中返回了false,一般是在onTouchEvent中返回了false,这两中情况下ViewGroup就会自己处理点击事件代码如下:

    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(event)方法,即点击事件开始交给View来处理。


    View事件分发的源码解析

    View对点击事件的处理比较简单,这里主要View不包含ViewGroup先看它的dispatchTouchEvent方法,代码如下:

        public boolean dispatchTouchEvent(MotionEvent event) {
            ...
            
            boolean result = false;
            ...
    
            if (onFilterTouchEventForSecurity(event)) {
                if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                    result = true;
                }
                //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;
                }
            }
            ...
    
            return result;
        }
    

    View对事件处理就比较简单了,它没有子元素只需要自己处理点击事件,从上面的源码可以看出View处理事件首先会判断有没有设置OnTouchListener,如果OnTouchListener中的onTouch方法返回true,那么onTouchEvent就不会调用,这样做的好处是方便在外界处理点击事件。

    接着再分析OnTouchListener的实现,先看View处于不可用状态下点击事件的处理过程

    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 (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
    }        
    

    很显然不可用状态下的View照样会返回true 消耗掉点击事件。如果View有mTouchDelegate代理还会执行mTouchDelegate.onTouchEvent(event)接着就是onTouchEvent对点击事件的具体处理

            if (((viewFlags & CLICKABLE) == CLICKABLE ||
                    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                    (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
                switch (action) {
                    case MotionEvent.ACTION_UP:
                        boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                            // take focus if we don't have it already and we should in
                            // touch mode.
                            boolean focusTaken = false;
                            if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                                focusTaken = requestFocus();
                            }
    
                            if (prepressed) {
                                // The button is being released before we actually
                                // showed it as pressed.  Make it show the pressed
                                // state now (before scheduling the click) to ensure
                                // the user sees it.
                                setPressed(true, x, y);
                           }
    
                            if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                                // This is a tap, so remove the longpress check
                                removeLongPressCallback();
    
                                // 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();
                                    }
                                }
                            }
    
                            if (mUnsetPressedState == null) {
                                mUnsetPressedState = new UnsetPressedState();
                            }
    
                            if (prepressed) {
                                postDelayed(mUnsetPressedState,
                                        ViewConfiguration.getPressedStateDuration());
                            } else if (!post(mUnsetPressedState)) {
                                // If the post failed, unpress right now
                                mUnsetPressedState.run();
                            }
    
                            removeTapCallback();
                        }
                        mIgnoreNextUpEvent = false;
                        break;
    
                    case MotionEvent.ACTION_DOWN:
                        mHasPerformedLongPress = false;
    
                        if (performButtonActionOnTouchDown(event)) {
                            break;
                        }
    
                        // Walk up the hierarchy to determine if we're inside a scrolling container.
                        boolean isInScrollingContainer = isInScrollingContainer();
    
                        // For views inside a scrolling container, delay the pressed feedback for
                        // a short period in case this is a scroll.
                        if (isInScrollingContainer) {
                            mPrivateFlags |= PFLAG_PREPRESSED;
                            if (mPendingCheckForTap == null) {
                                mPendingCheckForTap = new CheckForTap();
                            }
                            mPendingCheckForTap.x = event.getX();
                            mPendingCheckForTap.y = event.getY();
                            postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                        } else {
                            // Not inside a scrolling container, so show the feedback right away
                            setPressed(true, x, y);
                            checkForLongClick(0, x, y);
                        }
                        break;
    
                    case MotionEvent.ACTION_CANCEL:
                        setPressed(false);
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
    
                    case MotionEvent.ACTION_MOVE:
                        drawableHotspotChanged(x, y);
    
                        // Be lenient about moving outside of buttons
                        if (!pointInView(x, y, mTouchSlop)) {
                            // Outside button
                            removeTapCallback();
                            if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                                // Remove any future long press/tap checks
                                removeLongPressCallback();
    
                                setPressed(false);
                            }
                        }
                        break;
                }
    
                return true;
            }
    
    

    从上面的代码可以看到只要View的CLICKABLE和LONG_CLICKABLE有一个为true,那么onTouchEvent()就会返回True消耗这个事件,CLICKABLE和LONG_CLICKABLE代表View可以被点击和长按,可以通过View的setClickabke和setLongClickable方法来设置,也可以通过View的setOnClickListenr和setOnLongClickListener来设置,它们会自动将View设置为CLICKABLE和LONG_CLICKABLE。从源码中可也以看出这一点

     public void setOnClickListener(@Nullable OnClickListener l) {
            if (!isClickable()) {
                setClickable(true);
            }
            getListenerInfo().mOnClickListener = l;
        }
    

    接着再ACTION_UP事件中会调用performClick方法,如果View设置了OnClickListener那么在performClik方法内部会调用它的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;
        }
    

    相关文章

      网友评论

          本文标题:Android View事件分发机制源码详解

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