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

Activity构成

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

Android View事件分发机制源码详解_第1张图片
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事件分发机制源码详解)