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

来源:互联网 发布:软件著作权登记申请表 编辑:程序博客网 时间:2024/06/04 22:47

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完整的分发机制,然后,看懂上面的代码,回过头再看这图片,是不是非常简单了呢?祝各位进步!
这里写图片描述