View事件分发机制

View的事件分发机制

View的事件分发机制简单来说就是将用户与手机屏幕的交互事件交由正确的控件进行处理,从而可以对用户事件作出相应,完成交互。这里主要涉及到两个对象,一个是View, 一个是事件,即MotionEvent。

1. 事件MotionEvent

事件包括action code和axis value两部分,前者指定该事件的类型,后者指定事件发生的位置,此外每个MotionEvent中还包含一个Pointer, 用于多点触摸,每个Pointer表示一个触摸点,每个Pointer都会有对应的index和id

事件类型 action code

事件类型有很多种,常见的包括以下几种:

    public static final int ACTION_DOWN             = 0;
 
    public static final int ACTION_UP               = 1;
    
    public static final int ACTION_MOVE             = 2;
    
    public static final int ACTION_CANCEL           = 3;
    
    public static final int ACTION_OUTSIDE          = 4;

    public static final int ACTION_POINTER_DOWN     = 5;
    
    public static final int ACTION_POINTER_UP       = 6;

其中前三个类型最为常见,CANCEL则表示取消一个事件流,即当一个View接受到ACTION_CANCEL事件以后就不再处理该事件流的后续事件。最后两个事件类型则是在多点触摸的情况下,当已经有触摸点按下屏幕,第二个触摸点按下屏幕时,此时系统会发送一个ACTION_POINTER_DOWN类型的事件,同理,在有其他触摸点并未离开触摸屏,一个触摸点离开触摸点,系统发送一个ACTION_POINTER_UP事件。

事件位置 axis value

MotionEvent提供了四个方法获取事件的位置,

event.getX(); //触摸点相对于View左上角为原点坐标系的X坐标
event.getY(); //触摸点相对于View左上角为原点坐标系的Y坐标
event.getRawX(); //触摸点相对于屏幕左上角为原点坐标系的X坐标
event.getRawY(); //触摸点相对于屏幕左上角为原点坐标系的Y坐标

事件流

在事件分发的过程中,经常会用到事件流的概念。用户的一次操作都会触发多个事件,它们组成事件流,通常为ACTION_DOWN,ACTION_UP或者ACTION_DOWN,ACTION_MOVE,......, ACTION_UP两种类型,对于多点触摸操作中间还会包含ACTION_POINTER_DOWN和ACTION_POINTER_UP。在事件分发的过程中,一个事件流通常是交由一个控件处理,因此ACTION_DOWN的处理逻辑通常会比较特殊,它代表着一个事件流的开始,此时会有清除之前的状态等操作。一个事件流通常会以ACTION_UP或者ACTION_CANCEL结束。

多点触摸操作

对于多点触摸操作,MotionEvent中使用Pointer表示,每个Pointer都有一个Id和Index, 在一个事件流中,一个Pointer的id是保持不变的,可以通过id获取Pointer的index,进而可以执行pointer的其他操作。(在有些资料中看到说一个MotionEvent中包括一系列的Pointer,表示多点触摸,在后面的分析中感觉行不通,这里我的理解是每个MotionEvent只有一个Pointer,它有一个index和一个id, 比如第一个点触摸时,系统发出一个ACTION_DOWN事件,对应有一个Pointer,第二个点触摸时系统发出ACTION_POINTER_DOWN事件,此事件也对应一个Pointer,它有一个index和一个id, 然后两个手指滑动过程中都会触发ACTION_MOVE事件,而其Pointer对应的id保持不变,而index由可能因为滑动顺序发生改变,通过id可以查找该pointer对应的index,进而执行其他的关于这个pointer对应MotionEvent的操作, 仅仅是个人理解,有待查证)
多点触摸操作的常用方法:

int getPointerCount() //操作事件所包含的点的个数
int findPointerIndex(int pointerId) //根据pointerId找到pointer在MotionEvent中的index
int getPointerId(int pointerIndex) //根据MotionEvent中的index返回pointer的唯一标识
float getX(int pointerIndex) //返回操作点的x坐标
float getY(int pointerIndex) //返回操作点的y坐标
final int getActionMasked () //获取action code
final int getActionIndex()// 获取 pointer的index

MotionEvent的方法getAction可以获取action code, 对于单点的操作,即ACTION_DOWN,ACTION_UP等事件,getAction和getActionMasked二者是相同的,而对于多点操作,getAction可以获取Pointer的index(8~15位)和action code(0~7位),而getActionMasked则是获取action code

public static final int ACTION_MASK             = 0xff;
public static final int ACTION_POINTER_INDEX_MASK  = 0xff00;
public final int getAction() {
        return nativeGetAction(mNativePtr);
    }

public final int getActionMasked() {
        return nativeGetAction(mNativePtr) & ACTION_MASK;
    }

public final int getActionIndex() {
        return (nativeGetAction(mNativePtr) & ACTION_POINTER_INDEX_MASK)
                >> ACTION_POINTER_INDEX_SHIFT;
    }

从代码中可以清晰地看出三个方法作用。

2. View的事件分发

在系统产生一个事件流以后,事件通常交由某个合适的View处理,而找到合适的View则是事件分发机制的关键问题。而View有一个特殊的子类即ViewGroup,它主要负责布局它的子View, 因此展示内容的View和容器View(ViewGroup)对事件分发处理的逻辑有所不同,这里分两部分讲述。该小节首先说明内容View的事件分发机制。
内容View的事件分发处理逻辑较为简单,只有一个问题,即我是否要处理并消费该事件,处理逻辑在内部实现,而对该事件是否完全处理,即是否消费该事件,需要通过返回值反馈给父控件,让父控件针对处理结果做下一步处理。因此View的事件分发机制,只需要分析View#dispatchTouchEvent(MotionEvent event)的方法即可,该方法返回一个boolean值,true表示消费该事件,false则不消费。

事件分发的的方法View#dispatchTouchEvent(MotionEvent event)

 /**
     * Pass the touch screen motion event down to the target view, or this
     * view if it is the target.
     *
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     */
    public boolean dispatchTouchEvent(MotionEvent event) {
        
        ......

        boolean result = false;

        ......

        final int actionMasked = event.getActionMasked();
        ......

        if (onFilterTouchEventForSecurity(event)) {
            
            ......

            //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的事件分发逻辑很清晰,通过安全性检查以后,首先是调用OnTouchListener的onTouch()方法(如果有),onTouch方法如果返回true(表示已经处理该事件),则事件分发结束,事件处理完毕,如果没有OnTouchListener或者onTouch()方法返回false, 则调用View#onTouchEvent()方法,并将该方法的返回值作为dispatchTouchEvent()方法的返回值,表示该View是否消费该事件。

下面是onTouchEvent(MotionEvent event)负责处理事件,自定义View可以通过覆写该方法自定义事件处理逻辑,下面为View的onTouchEvent方法的简略化源码
View#onTouch(MotionEvent event)

/**
     * Implement this method to handle touch screen motion events.
     * <p>
     * If this method is used to detect click actions, it is recommended that
     * the actions be performed by implementing and calling
     * {@link #performClick()}. This will ensure consistent system behavior,
     * including:
     * <ul>
     * <li>obeying click sound preferences
     * <li>dispatching OnClickListener calls
     * <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
     * accessibility features are enabled
     * </ul>
     *
     * @param event The motion event.
     * @return True if the event was handled, false otherwise.
     */
    public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

        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 (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                ......
            }

            return true;
        }

        return false;
    }

这里将代码简化,只看最外层的逻辑,根据View的enable和disable两种状态分为两种情况,首先disable状态下,View为clickable时,该view消费该事件但并未做任何处理(这一点符合逻辑,即该view为clickable,则可以处理单击等事件,只不过现在处于disable状态,可以消费,但不做逻辑处理)。其次在enable状态下,如果View为clickable时,该view消费该事件(即返回true)并处理该事件,根据事件类型不同处理逻辑不同(switch内的分支代码在下一段)。最后就是view的所有clickable均为false时则该view不处理该事件,也不消费该事件,交由父控件处理。

下面为switch的分支语句,是不同事件的处理逻辑

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

        ......

        removeTapCallback();
    }
    mIgnoreNextUpEvent = false;
    break;

case MotionEvent.ACTION_DOWN:
    mHasPerformedLongPress = false;

    ......

    // 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:
    ......

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

首先看ACTION_DOWN, 根据View是否处于可以滚动的容器中分为两种情况,如果在可滑动容器中,则设置PFLAG_PREPRESSED,并启动单击检测(从代码中可以看出CheckForTap是一个Runnable类型,后面再做分析), 如果不在可滑动容器中则直接将设置FLAG_PRESSED标志位,并启动长按检测,这是因为在可滑动容器中需要区分是单击还是滑动事件,所以启动了单击检测。其实在CheckForTap中所做的工作也是设置FLAG_PRESSED标志位,并启动长按检测,源码如下:

private final class CheckForTap implements Runnable {
        public float x;
        public float y;

        @Override
        public void run() {
            mPrivateFlags &= ~PFLAG_PREPRESSED;   //情况PFLAG_PREPRESSED
            setPressed(true, x, y);
            checkForLongClick(ViewConfiguration.getTapTimeout(), x, y);
        }
    }

这里可以看出其操作基本相同,继续看checkForLongClick()方法的源码

private void checkForLongClick(int delayOffset, float x, float y) {
        if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
            mHasPerformedLongPress = false;

            if (mPendingCheckForLongPress == null) {
                mPendingCheckForLongPress = new CheckForLongPress();
            }
            mPendingCheckForLongPress.setAnchor(x, y);
            mPendingCheckForLongPress.rememberWindowAttachCount();
            postDelayed(mPendingCheckForLongPress,
                    ViewConfiguration.getLongPressTimeout() - delayOffset);
        }
    }

这里是做长按检测,其逻辑执行与在可滑动容器中做单击检测比较相似,同样CheckForLongPress也是一个Runnable, 其源码:

private final class CheckForLongPress implements Runnable {
        private int mOriginalWindowAttachCount;
        private float mX;
        private float mY;

        @Override
        public void run() {
            if (isPressed() && ......) {
                if (performLongClick(mX, mY)) {
                    mHasPerformedLongPress = true;
                }
            }
        }

        .......
    }

(这里在条件判断中加入省略号是我还没有理解的条件,暂时不影响分析),可以看出在长按检测中是在一定时间之后如果是Pressed状态(因为此时单击检测时间已过,如果此时没有ACTION_UP或者ACTION_CANCEL事件,则setPressed方法一定被调用)则执行长按逻辑,并置位mHasPerformedLongPress。所以单击检测和长按检测都交由View的attachInfo中有一个mHandler来处理,在指定时间之后设置标志位并执行相应逻辑。
接着是ACTION_MOVE和ACTION_CANCEL,这两个都较为简单,CANCEL则是表示该view不再处理该事件流的后续事件,清空所有标志位,并移除单击和长按检测。MOVE则考虑手指划出View的范围时,在合适条件下移除单击和长按检测以及清除标志位。
最后再看ACTION_UP就比较容易理解,这个是一个事件流的结束,prepressed为true说明单击检测还没结束,pressed为true则说明单击检测已经结束,此时直接看if (!mHasPerformedLongPress && !mIgnoreNextUpEvent)这里的条件判断,如果没有执行长按逻辑或者长按事件处理逻辑返回了false, mHasPerformedLongPress则为false, 如果在ACTION_UP事件之前没有ACTION_CANCEL事件,则mIgnoreNextUpEvent则为false, 那么此时发生ACTION_UP事件则是一次单击事件,此时触发单击事件处理逻辑,单击处理逻辑较为简单,与PerformeLongClick一样,也是通过post交由mHandler调用performClick方法,在该方法中会调用设置的ClickListener的onClick()方法,也是我们最为熟悉的方法。(这里也就解释了,如果没有设置长按监听器,手指长按在离开时也可以触发单击事件,而且即使设置了长按监听器,如果返回false,在手指离开时也会触发单击事件,而长按事件是ACTION_DOWN加一定时间触发的,而单击则一定要等到ACTION_UP时才能触发)。最后在ACTION_UP中关于focusTaken还不明白什么意思,后续会继续学习。

View事件分发的总结

至此,View的事件分发就结束了,主要流程就是调用dispatchTouchEvent()方法,然后是OnTouchListener的onTouch()方法,最后是调用View的OnTouchEvent()方法,是否调用则根据事件处理结果的返回值决定。在onTouchEvent()方法中,根据enable和各种clickable的状态分为四种情况,其中disable且clickable(这里的clickable包括多种,有一个为true就可以)时没有处理逻辑但是该view会消费该事件,clickable为false的时候,不管是否enable,都不处理且不消费该事件,只有在enable且clickable状态下才执行处理逻辑,此时根据不同的事件类型有不同逻辑,重点是ACTION_DOWN和ACTION_UP,前者开始一个事件流,主要是进行单击检测和长按检测并处罚长按事件处理逻辑(如果有),后者则是根据一个事件流中之前所设置的标志位判断是否发生了单击事件并执行单击逻辑。而其他两种事件类型处理较为简单,只是设置一些标志位。

View的事件分发是从调用View#dispatchTouchEvent(MotionEvent event)开始,那么谁来调用该方法,很自然应该是该view所属的ViewGroup调用,所以下一部分分析ViewGroup的事件分发,看ViewGroup如何调用View的dispatchTouchEvent

3. ViewGroup事件分发

在MotionEvent部分提及过事件流的概念,通常事件处理都是针对事件流,而一个事件流一般都是交由一个View处理,而当一个事件交由一个ViewGroup时,ViewGroup面临三个问题:

    1. ViewGroup是否拦截该事件
    1. 如果不拦截该事件,应该将该事件交由哪个子View处理
    1. 如果拦截如何拦截一个事件流,如果不拦截,如何将一个事件流的后续事件交由同一个View处理

由于ViewGroup是继承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;
/*****************************************************判断********************************************************/
            //1. 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();
            }

            //2. Check for interception.
            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;
            }

            ......

            //3. 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 (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();
                        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);

                            .......

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

                            ......
                        }
                        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) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }
/**************************************************清理***********************************************/
            // Update list of touch targets for pointer up or cancel, if needed.
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                resetTouchState();
            } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                final int actionIndex = ev.getActionIndex();
                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                removePointersFromTouchTargets(idBitsToRemove);
            }
        }

        ......

        return handled;
    }   

这里的代码将关于accessibilityfocus部分现在不理解,暂时先略去,只分析剩余部分(可能会有错误之处,后续继续学习时再作改正),另外由于该方法的代码较长,添加了几行分割线。下面分步骤分析:
在判断部分主要有个三个if语句块
第一步是当事件为ACTION_DOWN时,清空所有的状态。
第二步是检查是否拦截该事件,ACTION_DOWN或者一个事件流的后续事件(且该事件流的ACTION_DOWN已经交由某个子View处理,此时mFirstTouchTargent不为空),此时进入if语句块判断是否拦截。而如果某个ACTION_DOWN没能够交由ViewGroup的某个View处理,其后续事件都交由该ViewGroup处理,此时mFirstTouchTarget为空,不再判断直接设置为拦截,其他情况下均需要做判断。对于进入if语句块还需要判断disallowIntercept, 这个可以通过requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法,从而禁止执行是否需要拦截的判断,而当disallowIntercept为false时,是否拦截事件则是由onInterceptTouchEvent(ev)方法决定,自定义ViewGroup可以通过覆写该方法定义拦截某些事件。
第三步检查是不是要取消这一个事件流
在查找部分主要是查找可以处理该事件的子View,这里涉及到一个概念就是TouchTarget,用来描述一个触摸目标View以及触摸到该View上的pointers。这里不再贴出源码,重点关注它的三个public Field,

// The touched child view.
public View child;

// The combined bit mask of pointer ids for all pointers captured by the target.
public int pointerIdBits;

// The next target in the target list.
public TouchTarget next;

child表示它代表的View,pointerIdBits表示触摸点,整型中置一的位代表着该View捕获了该pointer的事件,next则用于构建链表。在ViewGroup中,mFirstTouchTarg就是一个这样类型的域,它是一个链表,在当ACTION_DOWN事件发生时用于记录处理这个事件流的View以及Pointer,也就是通过TouchTarget表示。

另外有一个方法在多个地方被调用,这里首先简要介绍,即dispatchTransformedTouchEvent,这个方法也挺长,不过简化一些细节,我们暂时只需要明白以下逻辑:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
}

即根据child不同做不同的事件分发逻辑,具体细节可以查看具体源码。

下面开始分析子View查找部分,根据之前多次提到事件流的概念,所以ViewGroup只有在ACTION_DOWN事件或者ACTION_POINTER_DOWN事件发生时才查找相应的子View, idBitsToAssign表示该事件的Pointer对应的id(在该整型的某一位置一),首先清除pointer对应的TouchTarget,然后遍历所有的子View,调用dispatchTransformedTouchEvent,该方法的返回值表示该子view是否处理了该事件,如果处理了则进入if语句块,记录一些信息,并将该View以及对应Pointer组合成TouchTarget添加到mFirstTouchTarget的链表中,此时newTouchTarget==mFirstTouchTarget, 并且alreadyDispatchedToNewTouchTarget置位true,跳出循环。最后如果mFirstTouchTarget不为空,且没有找到合适子View,那么叫交由mFirstTouchTarget链表的最后一个TouchTarget来处理该事件。

接下来是事件分发部分,查找部分已经将TouchTarget添加到mFirstTouchTarget链表中,接下来就是典型的链表操作,遍历链表并调用dispatchTransformedTouchEvent,根据View是否为空决定是ViewGroup处理还是某个查找到的View处理。

最后是清除工作,即ACTION_UP或者ACTION_POINTER_UP事件时以及取消事件时,需要清理一些状态。

总结

ViewGroup的事件分发只在ACTION_DOWN或者ACTION_POINTER_DOWN时才做拦截或查找判断,保证拦截一个事件流或者将一个事件流交由同一个子View处理,ViewGroup通过onIntercepte()方法决定是否拦截一个事件流,通过调用dispatchTransformedTouchEvent,根据这个方法的返回值查找目标子View,通过mFirstTouchTarget这样一个TouchTarget的链表记录一个事件流的目标View以及Pointer,并且通过变量这个链表以及调用dispatchTransformedTouchEvent完成事件分发。
这一部分省略了所有关于TargetAccessibilityFocus的源码,对这部分目前还不了解,所以分析过程可能存在错误,另外由于多点触摸,涉及到TouchTarget以及Pointer的概念,这部分理解还不够,所以分析有所欠缺,这方面在网上没有找到多少资料,有比较了解的欢迎赐教!

至此分析完了ViewGroup的事件分发,同样也是dispatchTouchEvent方法,同样的问题,ViewGroup的该方法由谁调用,下一步则需要分析Activity的事件分发。

4. Activity的事件分发。

Activity的事件分发同样从Activity#dispatchTouchEvent()方法开始。

/**
     * Called to process touch screen events.  You can override this to
     * intercept all touch screen events before they are dispatched to the
     * window.  Be sure to call this implementation for touch screen events
     * that should be handled normally.
     *
     * @param ev The touch screen event.
     *
     * @return boolean Return true if this event was consumed.
     */
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

首先是在ACTION_DOWN事件时,调用onUserInteraction(),在该回调方法中可以处理事件。接着调用Window#superDispatchTouchEvent().
我们都知道每个Activity都有一个Window对象,Window是一个抽象类,它有唯一的一个实现类即PhoneWindow。它的superDispatchTouchEvent方法如下:

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

mDecor就是ContentView的父View,所以这样就将事件传递到了顶层的ViewGroup,所以事件就可以逐层传递下去。

如果事件传递结束,顶层View的dispatchTouchEvent()方法返回了false, 则事件交由Activity的onTouchEvent处理。

所以Activity的事件分发较为简单,至此就分析完了事件分发的过程,至于Activity的dispatchTouchEvent()方法由谁调用,还在进一步学习中。。。。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容