Android6.0 源码解读之 ViewGroup 点击事件分发机制

907 阅读8分钟

    本篇博文是Android点击事件分发机制系列博文的第三篇,主要是从解读ViewGroup类的源码入手,根据源码理清ViewGroup点击事件分发原理,明白ViewGroup和View点击事件分发的关系,并掌握ViewGroup点击事件分法机制。特别声明的是,本源码解读是基于最新的Android6.0版本。

各位童鞋可以参考下面链接进行系统学习
(一)Android6.0触摸事件分发机制解读

(二)Android6.0源码解读之View点击事件分发机制

(三)Android6.0源码解读之ViewGroup点击事件分发机制

(四)Android6.0源码解读之Activity点击事件分发机制

ViewGroup事件分发中的三个重要方法的源码解析

    关于ViewGroup事件分发,我们重点需要解读dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent三个方法。ViewGroup比View多了一个onInterceptTouchEvent拦截事件方法,该方法源码默认返回false,即ViewGroup默认不拦截任何事件。

(一)dispatchTouchEvent源码解析

    /**
     * 重写了父类View的dispatchTouchEvent方法
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
        }

        
        
        if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
            ev.setTargetAccessibilityFocus(false);
        }

        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                
                
                
                
                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); 
                } else {
                    intercepted = false;
                }
            } else {
                
                
                intercepted = true;
            }

            
            
            if (intercepted || mFirstTouchTarget != null) {
                ev.setTargetAccessibilityFocus(false);
            }

            
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            if (!canceled && !intercepted) {

                
                
                
                
                
                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(); 
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;

                    
                    
                    removePointersFromTouchTargets(idBitsToAssign);

                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        
                        
                        
                        final ArrayList preorderedList = buildOrderedChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = customOrder
                                    ? getChildDrawingOrder(childrenCount, i) : i;
                            final View child = (preorderedList == null)
                                    ? children[childIndex] : preorderedList.get(childIndex);

                            
                            
                            
                            
                            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) {
                                
                                
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    
                                    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;
                            }

                            
                            
                            ev.setTargetAccessibilityFocus(false);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }

                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        
                        
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }

            
            if (mFirstTouchTarget == null) {
                
                
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                
                
                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;
                }
            }

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

        if (!handled && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        return handled;
    }

ViewGroup拦截情况源码分析

首先我们来看一下第34行~48行的代码,ViewGroup在如下两种情况下会判断是否要拦截当前事件:

事件类型为ACTION_DOWN或者 mFirstTouchTarget != null

    即,当事件由ViewGroup的子元素成功处理时,mFirstTouchTarget 会被赋值并指向子元素,换句话说,当ViewGroup不拦截事件并将事件交由子元素处理时mFirstTouchTarget != null。反过来,一旦事件由当前的ViewGroup拦截时,mFirstTouchTarget != null条件就不成立。那么当ACTION_MOVE和UP事件到来时,由于actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null这个条件为false,导致ViewGroup的onInterceptTouchEvent不会再被调用,并且同一序列中的其他事件都会默认交给它处理。
    另外,这里有一种特殊情况,我们看36行代码,有个FLAG_DISALLOW_INTERCEPT标记为,这个标记是通过requestDisallowInterceptTouchEvent()方法来设置的,一般用在子View中。如果我们通过reqeustDisallowInterceptTouchEvent()方法设置了FLAG_DISALLOW_INTERCEPT标记位后,ViewGroup将无法拦截除了ACTION_DOWN以外的其他方法(即调用该方法并不影响ACTION_DOWN事件处理)。因为ViewGroup会在ACTION_DWON事件到来时做重置状态操作,这里从代码第22~29行可以看出。

requestDisallowInterceptTouchEvent源码解析

    /**
     * {@inheritDoc}
     */
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            
            return;
        }

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

        
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

取消、清理、重置之前的触摸状态

    /**
     * Cancels and clears all touch targets.
     */
    private void cancelAndClearTouchTargets(MotionEvent event) {
        if (mFirstTouchTarget != null) {
            boolean syntheticEvent = false;
            if (event == null) {
                final long now = SystemClock.uptimeMillis();
                event = MotionEvent.obtain(now, now,
                        MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
                event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
                syntheticEvent = true;
            }

            for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
                resetCancelNextUpFlag(target.child);
                dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
            }
            clearTouchTargets();

            if (syntheticEvent) {
                event.recycle();
            }
        }
    }
    /**
     * Resets all touch state in preparation for a new cycle.
     */
    private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }

因此我们可以得出如下结论:

1.当ViewGroup决定拦截事件后,那么点击事件将会默认交给它处理并且不再调用它的onInterceptTouchEvent方法,FLAG_DISALLOW_INTERCEPT这个标记的作用是ViewGroup不再拦截事件,前提是ViewGroup不拦截ACTION_DOWN事件处理。

2.如果事件能够传递到当前的ViewGroup,如果我们要提前处理所有点击事件,应该选择dispatchTouchEvent方法,因为只有这个方法能确保每次都会被调用;而onInterceptTouchEvent()却无法保证每次事件都会被调用。

3.FLAG_DISALLOW_INTERCEPT标记位可以用于解决滑动冲突问题。

ViewGroup不拦截情况源码分析

    ViewGroup不拦截事件的时候,事件会向下分发交由它的子View进行处理。先来看下代码64行(!canceled && !intercepted)这里的canceled和intercepted都为false时,条件成立,也就是说不拦截。接下来74行的条件判断:

actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE

    在该if条件内,看到第86行,如果newTouchTarget == null && childrenCount != 0,即子控件的个数不为0 且 newTouchTarget为空,在95行中遍历整个ViewGroup中的子控件,这里的集合做了个倒序排列,如果两个View交叉覆盖在一起,下面的子控件先放进集合,因为后被添加的子控件会浮在上面,通常我们会希望点击的时候最上层的那个组件先去响应事件。接着105行代码开始判断子控件是否能够接收到点击事件,主要依赖于两个条件:第一子控件是否在播动画;第二点击事件是否落在子控件的区域内。如果某个子控件满足这两个条件,那么事件就会传递给它来处理。

buildOrderedChildList方法解析

    /**
     * 实现倒序排序
     * Populates (and returns) mPreSortedChildren with a pre-ordered list of the View's children,
     * sorted first by Z, then by child drawing order (if applicable). This list must be cleared
     * after use to avoid leaking child Views.
     * Uses a stable, insertion sort which is commonly O(n) for ViewGroups with very few elevated
     * children.
     */
    ArrayList buildOrderedChildList() {
        final int count = mChildrenCount;
        if (count <= 1 || !hasChildWithZ()) return null;

        if (mPreSortedChildren == null) {
            mPreSortedChildren = new ArrayList(count);
        } else {
            mPreSortedChildren.ensureCapacity(count);
        }

        final boolean useCustomOrder = isChildrenDrawingOrderEnabled();
        for (int i = 0; i < mChildrenCount; i++) {
            
            int childIndex = useCustomOrder ? getChildDrawingOrder(mChildrenCount, i) : i;
            View nextChild = mChildren[childIndex];
            float currentZ = nextChild.getZ();

            
            int insertIndex = i;
            while (insertIndex > 0 && mPreSortedChildren.get(insertIndex - 1).getZ() > currentZ) {
                insertIndex--;
            }
            mPreSortedChildren.add(insertIndex, nextChild);
        }
        return mPreSortedChildren;
    }

    接着看代码,129行通过dispatchTransformedTouchEvent()这一重要方法(后面有详细分析),判断是否有子控件,如果有子控件则执行内部的操作,并找到最终触摸的对象,通过addTouchTarget方法赋值给newTouchTarget。在dispatchTransformedTouchEvent()方法中,如果子控件的dispatchTouchEvent()方法返回true,那么mFirstTouchTarget就会被赋值,同时跳出for循环,详见148行代码。同样如果dispatchTouchEvent()方法返回false,ViewGroup就会把事件分发给下一个子控件(如果还有下一个子控件)。

    mFirstTouchEvent的真正赋值其实是在addTouchTarget方法中完成的,mFirstTouchEvent其实是一个单链表结构,如果mFirstTouchEvent为null,那么ViewGroup就会默认拦截下来同一序列中所有的点击事件。

addTouchTarget方法解析

    /**
     * Adds a touch target for specified child to the beginning of the list.
     * Assumes the target child is not already present.
     */
    private TouchTarget addTouchTarget(View child, int pointerIdBits) {
        TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

    接着我们看到171行代码中,如果mFirstTouchEvent为null,也就是说要么ViewGroup中没有子控件,要么是子控件处理了点击事件,但是在dispatchTouchEvent中返回了false(子控件在onTouchEvent中返回了false),那么ViewGroup就会自己处理点击事件,需要说明的是175行代码中,第三个参数本应为child,这里是null意味着需要调用父类View的dispatchTouchEvent方法,然后调用onTouch方法。

TouchTarget 内部类源码解析

    
    private static final class TouchTarget {
        private static final int MAX_RECYCLED = 32;
        private static final Object sRecycleLock = new Object[0];
        private static TouchTarget sRecycleBin;
        private static int sRecycledCount;

        public static final int ALL_POINTER_IDS = -1; 

        
        public View child;

        
        public int pointerIdBits;

        
        public TouchTarget next;

        private TouchTarget() {
        }

        public static TouchTarget obtain(View child, int pointerIdBits) {
            final TouchTarget target;
            synchronized (sRecycleLock) {
                if (sRecycleBin == null) {
                    target = new TouchTarget();
                } else {
                    target = sRecycleBin;
                    sRecycleBin = target.next;
                     sRecycledCount--;
                    target.next = null;
                }
            }
            target.child = child;
            target.pointerIdBits = pointerIdBits;
            return target;
        }

        public void recycle() {
            synchronized (sRecycleLock) {
                if (sRecycledCount < MAX_RECYCLED) {
                    next = sRecycleBin;
                    sRecycleBin = this;
                    sRecycledCount += 1;
                } else {
                    next = null;
                }
                child = null;
            }
        }
    }
    /**
     * Transforms a motion event into the coordinate space of a particular child view,
     * filters out irrelevant pointer ids, and overrides its action if necessary.
     * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
     */
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        
        
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }

        
        final int oldPointerIdBits = event.getPointerIdBits();
        final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

        
        
        if (newPointerIdBits == 0) {
            return false;
        }

        
        
        
        
        final MotionEvent transformedEvent;
        if (newPointerIdBits == oldPointerIdBits) {
            if (child == null || child.hasIdentityMatrix()) {
                if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
                    final float offsetX = mScrollX - child.mLeft;
                    final float offsetY = mScrollY - child.mTop;
                    event.offsetLocation(offsetX, offsetY);

                    handled = child.dispatchTouchEvent(event);

                    event.offsetLocation(-offsetX, -offsetY);
                }
                return handled;
            }
            transformedEvent = MotionEvent.obtain(event);
        } else {
            transformedEvent = event.split(newPointerIdBits);
        }

        
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            transformedEvent.offsetLocation(offsetX, offsetY);
            if (! child.hasIdentityMatrix()) {
                transformedEvent.transform(child.getInverseMatrix());
            }

            handled = child.dispatchTouchEvent(transformedEvent);
        }

        
        transformedEvent.recycle();
        return handled;
    }

    该方法在dispatchTouchEvent()中被调用,用于将事件分发给子View处理。我们重点看一下60~71行代码。在dispatchTransformedTouchEvent()方法一共有三个参数,其中第三个参数View child有时为null,有时不为null。61行代码中,child==null意味着事件没有被消费,ViewGroup中没有子控件需要调用父类View的dispatchTouchEvent方法,即super.dispatchTouchEvent(event)。
    接着我们关注下handled这个变量,可以发现dispatchTransformedTouchEvent()方法return handled,而handled的值其实是取决于dispatchTransformedTouchEvent()方法递归调用dispatchTouchEvent()方法的结果,也就是说在子控件中dispatchTouchEvent()方法的onTouchEvent()是否消费了Touch事件的返回值决定了dispatchTransformedTouchEvent()的返回值,从而决定mFirstTouchTarget是否为null,更进一步决定了ViewGroup是否处理Touch事件。

(二)onInterceptTouchEvent源码解析

public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }

    你没有看错,这方法就是简简单单的一个布尔值返回,当返回true时,对事件进行拦截,返回false则不拦截。

(三)ViewGroup点击事件分发小结

    Android点击事件分发是到达顶级View后(一般是ViewGroup),会调用ViewGroup的dispatchTouchEvent方法,其中它的onInterceptTouchEvent方法如果返回true,则会对事件传递进行拦截,事件由ViewGroup处理;如果onInterceptTouchEvent方法返回false,则代表不对事件进行拦截,默认返回false。则此时子View中的dispatchTouchEvent方法将被调用,到此,事件已经由顶级View传递给了下一层的View,接下来的过程是一个递归循环的过程,和顶级View事件分发过程是一致的,直到完成整个事件分发。

这里写图片描述