Android源码分析--View的事件分发机制

Android源码分析–View的事件分发机制

网上关于View的事件分发机制的文章有很多,所以这次更博呢,更重要的目的是让自己重新温习一遍View的分发机制,通过文字的描述,希望自己能讲透讲明白这个过程,从头到尾从源码层面去剖析内部原理。

看了本文你能学到什么?

  • View事件的分发流程;
  • View事件分发过程中,源码级的分析

View事件的分发流程

首先我们来看一张图片,大体上了解下这整一个过程

这里写图片描述

从上图我们可以看到,最先拿到View事件的是Activity,Activity可以先下分发,也可以自己消费,向下分发这个过程,下面我们会通过源码来分析,我们主要先看下大体的一个流程。可以发现,Activity、ViewGroup、View三者都能消费事件,事件消费既在onTouchEvent中返回true。如果View和ViewGroup都不消费这个事件,最终事件会交还给Activity,由它来消费。当然,ViewGroup中还可以拦截事件,不向下继续传递。这一part先到这里,看不太不明白没关系其实,下面我们将从源码层面去一步步跟踪分析这一个过程。


View事件分发过程中,源码级的分析

首先,我们先得了解事件分发过程中,非常重要的三个方法:

public boolean dispatchTouchEvent(MotionEvent ev); //用来分发事件
public boolean onInterceptTouchEvent(MotionEvent ev); //用来拦截事件
public boolean onTouchEvent(MotionEvent ev); //用来处理事件

这三个方法,在事件分发过程中,起了非常重要的作用,需要注意的是,onInterceptTouchEvent是ViewGroup才有的方法。我们先要有一个大体的了解,上面我们说了,首先获得事件的是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),这句话是调用了Window的superDispatchTouchEvent(ev)方法,事件就交给了Window,但是,问题来了,假如对源码有一定了解的话,Window是一个抽象类,该方法是空方法,那么,我们就得着Window的实现类,我们知道,Android中,PhoneWindow是Window的唯一实现类,所以,我们在PhoneWindow中查看superDispatchTouchEvent(ev)的具体实现:

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

哈哈,坑爹了吧,这里直接return的是mDecor的superDispatchTouchEvent(ev)返回值,那么,事件就传递到了mDecor上面了。什么是mDecor呢,这是PhoneWindow中的一个成员变量,是DecorView的一个对象,我们回忆一下,我们在Activity的onCreate方法里面有这一句:setContentView(view),这个View我们可以通过getWindow().getDecorView().findViewById(R.id.content)得到View的父控件,这的返回值其实是FrameLayout,我们把返回值强转为ViewGroup之后调用getChildAt(0),得到的就是setContentView中我们设置的View。
刚刚说到现在事件由PhoneWindow传递到了DecorView上面,而DecorView是继承自FrameLayout的,所以,到这一步,实际上事件就已经传递给View了。我们接下来看下DecorView中的superDispatchTouchEvent(event)方法:

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

可以看到,还是没有具体的处理case,继续往前看,刚刚说到DecorView是继承自FrameLayout的,FrameLayout是没有实现dispatchTouchEvent方法的,而FrameLayout有是继承自ViewGroup的,所以 我们可以直接看ViewGroup中的dispatchTouchEvent方法,由于这个方法实现过长,我们分开看

// Handle an initial down.
//如果是down事件的话,设置的请求不拦截的FLAG_DISALLOW_INTERCEPT标记将被清空,
//这个标记位是在requestDisallowInterCeptTouchEvent方法中
//设置的。换句话表达,就是ViewGroup将无法拦截设置了
//FLAG_DISALLOW_INTERCEPT标记的move和up事件。
//下面的代码,如果是down事件,将重置标记位,所以如果down事件
//设置了FLAG_DISALLOW_INTERCEPT标记,其实也是没用的,
//依然会判断是否拦截。这点需要非常清楚,后续的事件冲突处理需要灵活运用。
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();
            }
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;
            }
//设置标记位
 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            return;
        }

        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

上面的代码是关于拦截事件的判断,我们可以看到,条件是down事件或者mFirstTouchTarget不为空时,才可能拦截,也就是说,当事件是move或者up时,mFirstTouchTarget不为空才能进入这个if分支。那么mFirstTouchTarget是什么呢?看到这里我也不明白,咱暂且先放下。继续往下看

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

            // 是否需要拆分MotionEvent动作,默认为拆分
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            //MotionEvent没有被取消并且没有被拦截时进入分支
            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;
                //当前事件的down,或者接下来手指的down,这里需要明白
//一个事情,MotionEvent.ACTION_DOWN是指出发当前MotionEvent事件的触点的down事件,
//MotionEvent.ACTION_POINTER_DOWN是指有另外的触点触发的down事件,
//这里其实是一个多点触控的逻辑,以下,大致理解为找出触发当前一系列MotionEvent的触点,
//然后获取事件相当于父控件的X,Y坐标。
                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.
                    //清除触点信息,只留下动作信息,一个MotionEvent是由Action和Pointer组成的
                    //一个16位值,前8位表示触点信息,后八位表示动作信息。
                    removePointersFromTouchTargets(idBitsToAssign);

                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        //子View根据动作索引去获取当前触点的相当于父控件(其实就是当前这个ViewGroup)X,Y坐标
                        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();
                        final View[] children = mChildren;
                        //遍历所有子View
                        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.
                            //子View是否能获取焦点
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }
                            //子View是否能接受事件,坐标是否在当前控件范围内
                            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实际调用的是子View(child)的dispatchTouchEvent方法。
                            //实际上就是事件传递给子View的一个过程。如果子View的dispatchTouchEvent返回true,
                            //mFristTouchEvent会被赋值,并且终止循环,如果返回false,那么循环继续,直至没有子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();
                                //mFristTouchEvent就是在下面这个方法被赋值的。
                                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();
                    }

                    ...
                }
            }

上面可以看到,当TouchEvent被传递给child时,mFristTouchEvent会被赋值。

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

可以看到 其实TouchTarget是一种单链表的结构,mFirstTouchTarget是否被赋值,将影响到ViewGroup的拦截case,那么,我们需要回过头再去想想我们放下没有解决的问题,mFirstTouchTarget在事件分发给子View之后会被赋值,就意味着,onInterCeptTouchEvent这个方法的调用时机实际上是如果ViewGroup拦截了当前的down事件的话,那么mFristTouchEvent必然为null,onInterCeptTouchEvent这个方法不会被调用,而是由它直接处理。

所以我们得出以下结论:

如果ViewGroup拦截了TouchEvent事件,那么,move和up事件也必然交由它处理,不再分发。

接下来我们继续往下看,如果mFristTouchEvent为空时如何处理的:

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

上面调用了dispatchTransformedTouchEvent方法,注意,这里是第二次调用这个方法了,第一次是在ViewGroup分发事件给子View时,child传递是的非空的,第二次是上面,我们可以看到 ,这里的第三个参数传递的是null,我们跟踪下,只看它关键部分的代码:

if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            //child为空,事件交到View的dispatchTouchEvent处理
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {//子View处理
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }

接下来看下View的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,这里的实现也比较简单,我把主要的代码贴出来了,从上面的代码可以看出,只要一个View设置了OnTouchListener,那么,将执行的是onTouch方法,通过onTouch返回true,事件到此为止消耗完毕,如何OnTouchListener为null,那么将执行View的onTouchEvent方法,执行完毕如果返回true,事件消费完毕。可以这么理解,OnTouchListener的优先级是高于onTouchEvent的。

接下来我们看下View中的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设置有代理,将执行TouchDelegate中的onTouchEvent方法
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

上面的代码,我们可以得出一个结论

一个View的状态即使是disable的状态下,照样是能消费点击事件的。

//只要View的CLICKABLE、LONG_CLICKABLE或者CONTEXT_CLICKABLE有一个为true,那么就会消费这个事件
 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;
        }

        return false;
    }

up事件发生时,会触发调用performClick()方法,如果View设置了clickListener,performClick中会调用它的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;
    }

以上,就是一个TouchEvent完整的分发机制,然后,看懂上面的代码,回过头再看这图片,是不是非常简单了呢?祝各位进步!
这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值