Android查缺补漏(View篇)--事件分发机制源码分析

Posted CodingBlock

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android查缺补漏(View篇)--事件分发机制源码分析相关的知识,希望对你有一定的参考价值。

在上一篇博文中分析了事件分发的流程及规则,本篇会从源码的角度更进一步理解事件分发机制的原理,如果对事件分发规则还不太清楚的童鞋,建议先看一下上一篇博文 《Android查缺补漏(View篇)--事件分发机制》 ,先来看一下本篇的分析思路,一会儿会按照事件传递的顺序,针对以下几点进行源码分析:

  • Activity对点击事件的分发过程
  • PhoneWindow是如何处理点击事件的
  • 顶级View对点击事件的分发过程
  • View对点击事件的处理过程

Activity对点击事件的分发过程

通过上一篇博文中我们就知道了当一个点击事件发生后,最先传递给当前的Activity,并有Activity的dispatchTouchEvent方法来分发,其具体的分发工作时由Activity内部的Window处理的。

Activity的dispatchTouchEvent方法源码如下:

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction(); // 这是一个空方法,不用理会
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
}

在dispatchTouchEvent内部我们看到通过getWindow获取了Window,然后将事件分发的处理操作交给了Window,通过上述源码我们即可看出从大的流程上Window将分发给顶级View,如果在getWindow().superDispatchTouchEvent(ev)这一步返回了true,那么事件循环就此结束,返回false意味着各界的View的onTouchEvent都返回了false,最终会去调用当前Activity的onTouchEvent。

PhoneWindow是如何处理点击事件的

接着详细看一下在getWindow().superDispatchTouchEvent(ev)中都干了啥,我们知道Window是一个抽象类,我们必须找出他们实现类才可以进行下一步分析,很简单,通过在Activity中搜索mWindow变量,很快我们就在Activity的attch方法中找到了它的赋值过程:

final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback) {
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);

        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        ...// 省略其他代码
}

没错,Window的实现类就是PhoneWindow,接下来直接去看PhoneWindow的superDispatchTouchEvent方法:

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

PhoneWindow什么都没做,直接把DecorView,DecorView就是Activity的顶级View,在Activity中可以通过getWindow().getDecorView()获取。

顶级View(DecorView)对点击事件的分发过程

DecorView的superDispatchTouchEvent源码如下:

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

其实DecorView继承了FrameLayout,当然也是一个ViewGroup,所以这里我们再跳转到ViewGroup的dispatchTouchEvent方法中进一步分析,在上一篇博文《Android查缺补漏(View篇)--事件分发机制》 中已经详细介绍了ViewGroup的dispatchTouchEvent对点击事件分发的流程,这里再通过源代码了解一下它的实现过程。

这个方法非常长,但我们一步步分析还是能找到一些头绪的,请着重看代码中注释。

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    
    ...

    boolean handled = false;
    // 对事件进行安全过滤
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;

        // 初始化事件流的状态,当有down类型的touch事件传来时说明是一个新的事件流,此时需要重置事件状态。
        
        // Handle an initial down.
        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();
        }

        // 检查是否拦截事件,当事件交由ViewGroup的子元素处理时,就会将mFirstTouchTarget赋值并指向子元素。
        // 更详细来说就是,当ViewGroup拦截事件时,mFirstTouchTarget为null,这时候除了down以外的事件类型到来时将不再调用onInterceptTouchEvent,同一事件流中的其他时间类型都会讲给ViewGroup处理。当ViewGroup不拦截事件时,mFirstTouchTarget将会被赋值指向为子元素。
        
        // Check for interception.
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
                // 这里包含了一种特殊情况,那就是在子View中可以通过requesetDisallowInterceptTouchEvent方法来干预ViewGroup对除了down以外的事件的分发过程,
                // 这里的FLAG_DISALLOW_INTERCEPT标记为就是通过在requestDisallowInterceptTouchEvent方法来设置的,FLAG_DISALLOW_INTERCEPT被设置后,ViewGroup就不在对事件拦截。
                // 当down事件传来后,在前面的resetTouchState方法中会对FLAG_DISALLOW_INTERCEPT重置,所以说子View无法影响ViewGroup对down事件的处理。
                
            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;
        }

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

        // Check for cancelation.
        final boolean canceled = resetCancelNextUpFlag(this)
                || actionMasked == MotionEvent.ACTION_CANCEL;

        // Update list of touch targets for pointer down, if needed.
        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        if (!canceled && !intercepted) {

            // If the event is targeting accessiiblity focus we give it to the
            // view that has accessibility focus and if it does not handle it
            // we clear the flag and dispatch the event to all children as usual.
            // We are looking up the accessibility focused host to avoid keeping
            // state since these events are very rare.
            View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                    ? findChildWithAccessibilityFocus() : null;

            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                final int actionIndex = ev.getActionIndex(); // always 0 for down
                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                        : TouchTarget.ALL_POINTER_IDS;

                // Clean up earlier touch targets for this pointer id in case they
                // have become out of sync.
                removePointersFromTouchTargets(idBitsToAssign);

                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
                    // Find a child that can receive the event.
                    // Scan children from front to back.
                    final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                    final boolean customOrder = preorderedList == null
                            && isChildrenDrawingOrderEnabled();
                            
                            // 当ViewGroup不拦截事件的情况下,事件就会交给子View处理
                            // 首先遍历ViewGroup的所有子View,判断子View是否能够接收到点击事件:
                            // 判断的依据是:子元素是否在播放动画和点击事件的坐标是否落在子元素的范围内。
                            
                    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);

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

                        if (!canViewReceivePointerEvents(child)
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            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);
                        
                        /*
                         这里的dispatchTransformedTouchEvent方法实际调用的就是子元素的dispatchTouchEvent方法。
                         这个方法内部有一段逻辑是这样:
                            if (child == null) {
                                handled = super.dispatchTouchEvent(event);
                            } else {
                                handled = child.dispatchTouchEvent(event);
                            }
                        由于这里传入的child非null,所以此时事件就交给了子view处理。
                        
                        如果子View的dispatchTouchEvent方法返回了true,那么mFirstTouchTarget就会被赋值(在addTouchTarget()中被赋值),同时跳出for循环。
                        如果子View返回了false,那就继续循环,按同样的套路将调用下一个子View的dispatchTouchEvent方法。(例如:多个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;
                        }

                        // The accessibility focus didn\'t handle the event, so clear
                        // the flag and do a normal dispatch to all children.
                        ev.setTargetAccessibilityFocus(false);
                    }
                    if (preorderedList != null) preorderedList.clear();
                }

                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    // Did not find a child to receive the event.
                    // Assign the pointer to the least recently added target.
                    newTouchTarget = mFirstTouchTarget;
                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                }
            }
        }

        // Dispatch to touch targets.
        if (mFirstTouchTarget == null) {
            /*
            mFirstTouchTarget为null时,ViewGroup就会拦截接下来的同一序列中的所有事件。
            如果遍历了所有的子元素事件都没有被合适的处理(可能是因为该ViewGroup没有子View,或者在子View的dispatchTouchEvent方法返回了false),ViewGroup就会自己处理事件。
            
            这里的第三个参数传入了null,在dispatchTransformedTouchEvent中通过以下逻辑即可知道会调用super.dispatchTouchEvent(event);
            
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            */
        
            // No touch targets so treat this as an ordinary view.
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            ...
        }
        ...
}

上面代码是对顶级View(其实也是ViewGroup)事件分发的分析,接下来再看看当事件传递给View时,View对事件的处理代码。

View对点击事件的处理过程

从上面的分析我们可知,ViewGroup是通过调用View的dispatchTouchEvent方法将事件传递给View的,那么就来看一下View的dispatchTouchEvent方法源码:

public boolean dispatchTouchEvent(MotionEvent event) {
    
    ...

    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的ListenerInfo判断有没有设置OnTouchListener,如果设置了并且在OnTouchListener中的onTouch方法返回了true,那么就不再调用View自身的onTouchEvent方法,至此我们也找到了OnTouchListener监听器中的onTouch方法优先于onTouchEvent方法的原因。

接着再看onTouchEvent方法的源码:

public boolean onTouchEvent(MotionEvent event) {
    final float x = event.getX();
    final float y = event.getY();
    final int viewFlags = mViewFlags;
    final int action = event.getAction();

    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
            || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
            || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
    
    // 从这段代码可知,即使不可用状态的控件也会消费事件。
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
        // A disabled view that is clickable still consumes the touch
        // events, it just doesn\'t respond to them.
        return clickable;
    }
    
    // mTouchDelegate是View的代理,有代理的情况下回调用代理的onTouchEvent方法。
    
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }

    // 从上面代码为clickable赋值时我们可以知道,不管View是不是disable状态,只要它的CLICKABLE和LONG_CLICKABLE有一个为true时,它就会消耗这个事件。

    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                if ((viewFlags & TOOLTIP) == TOOLTIP) {
                    handleTooltipUp();
                }
                if (!clickable) {
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    break;
                }
                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)) {
                            
                                // action_up事件会触发performClick方法,在performClick方法中调用了View的onClickListener监听器的onClick方法。
                                
                                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:
                if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                    mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                }
                mHasPerformedLongPress = false;

                if (!clickable) {
                    checkForLongClick(0, x, y);
                    break;
                }

                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:
                if (clickable) {
                    setPressed(false);
                }
                removeTapCallback();
                removeLongPressCallback();
                mInContextButtonPress = false;
                mHasPerformedLongPress = false;
                mIgnoreNextUpEvent = false;
                mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                break;

            case MotionEvent.ACTION_MOVE:
                if (clickable) {
                    drawableHotspotChanged(x, y);
                }

                // Be lenient about moving outside of buttons
                if (!pointInView(x, y, mTouchSlop)) {
                    // Outside button
                    // Remove any future long press/tap checks
                    removeTapCallback();
                    removeLongPressCallback();
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                        setPressed(false);
                    }
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                }
                break;
        }

        return true;
    }

    return false;
}

当action_up事件到来时会触发performClick方法,在performClick方法中调用了View的onClickListener监听器的onClick方法,所以至此我们应该知道onClick方法是在up事件发生后调用的,优先级最低。

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

    notifyEnterOrExitForAutoFillIfNeeded(true);

    return result;
}

小结

到这里整个事件分发的源码基本分析完毕,大体总结一下:

  1. 当点击事件产生后,会首先触发Activity的dispatchTouchEvent方法,在此方法中会将事件传递个PhoneWindow处理;
  2. 在PhoneWindow的dispatch方法中又将事件传递个了DecorView的dispatchTouchEvent;
  3. 接着DecorView又调用了父类ViewGroup的dispatchTouchEvent方法对事件进行了分发;
  4. 在ViewGroup的dispatchTouchEvent方法中,首先先判断了自身是否拦截事件,如果拦截那么事件不再向其子View传递,如果不拦截就会遍历其所有的子View找到可以适合接收事件的子View并调用子View的dispatchTouchEvent方法。
  5. 在子View的dispatchTouchEvent方法中对事件做相应处理。

最后想说的是,本系列文章为博主对android知识进行再次梳理,查缺补漏的学习过程,一方面是对自己遗忘的东西加以复习重新掌握,另一方面相信在重新学习的过程中定会有巨大的新收获,如果你也有跟我同样的想法,不妨关注我一起学习,互相探讨,共同进步!

参考文献:

  • 《Android开发艺术探索》

以上是关于Android查缺补漏(View篇)--事件分发机制源码分析的主要内容,如果未能解决你的问题,请参考以下文章

Android查缺补漏(View篇)--自定义 View 的基本流程

Android查缺补漏(View篇)--布局文件中的“@+id”和“@id”有什么区别?

Android查缺补漏(View篇)--自定义View利器Canvas和Paint详解

Android查缺补漏(View篇)--在 Activity 的 onCreate() 方法中为什么获取 View 的宽和高为0?

Android查缺补漏(View篇)--自定义 View 中 wrap_content 无效的解决方案

Android查缺补漏(线程篇)-- IntentService的源码浅析