Android事件分发——ViewGroup篇

Posted Eateeer

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android事件分发——ViewGroup篇相关的知识,希望对你有一定的参考价值。

上一篇说到,Activity将事件传递到ViewGroup的dispatchTouchEvent()方法中,那么,我们来看一下该方法的源码

public boolean dispatchTouchEvent(MotionEvent ev) 
        if (mInputEventConsistencyVerifier != null) 
            mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
        

        // If the event targets the accessibility focused view and this is it, start
        // normal event dispatch. Maybe a descendant is what will handle the click.
        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;

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

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

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

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

是不是瞬间感觉头大了?和Activity中dispatchTouchEvent()短小精悍的代码完全是两个风格,面对这堆庞然大物,我们要有目的性的,策略性的解决它!

回顾一下我们目标:ViewGroup中的dispatchTouchEvent()方法什么时候返回true,什么时候返回false

然后看一下ViewGroup方法中dispatchTouchEvent()方法返回的是handled的值,也就是我们要特别关注该方法中会改变handled值的语句。

第12行代码初始化了handled的值,默认为false

第13行的if (onFilterTouchEventForSecurity(ev))判断囊括了dispathcTouchEvent()的几乎所有的方法。也就是说,如果onFilterTouchEventForSecurity(ev)返回为true的话,就表示可以分发该触摸事件,如果返回为false,则不分发事件。查看一下onFilterTouchEventForSecurity(ev)的方法:

  public boolean onFilterTouchEventForSecurity(MotionEvent event) 
        //noinspection RedundantIfStatement
        if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
                && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) 
            // Window is obscured, drop this touch.
            return false;
        
        return true;
    
  1. FILTER_TOUCHES_WHEN_OBSCUREDandroid:filterTouchesWhenObscured属性所对应的位。android:filterTouchesWhenObscured是true的话,则表示其他视图在该视图之上,导致该视图被隐藏时,该视图就不再响应触摸事件。
  2. MotionEvent.FLAG_WINDOW_IS_OBSCUREDtrue的话,则表示该视图的窗口是被隐藏的。

然后在第18行开始判断,如果是ACTION_DOWN事件的话,就会触发cancelAndClearTouchTargets(ev);resetTouchState();方法,在resetTouchState()方法中,有一个clearTouchTargets();方法,这个方法将mFirstTouchTarget设置为null。为什么要关心这个呢?很快你就知道答案了。

第27行创建了一个intercepted的布尔变量,看名字就知道是记录是否拦截的标志了。第28行有一个判断:actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null,刚才我们知道了mFirstTouchTarget是为null的,也就是说,如果是ACTION_DOWN的话,就会直接进入这个方法块中。

然后第30行判断是否设置了FLAG_DISALLOW_INTERCEPT标志,如果设置了,则disallowIntercepttrue(禁止拦截判断), intercepted直接设置为false。

对于这个FLAG_DISALLOW_INTERCEPT标志,其实有两种方法可以设置,第一种自然是通过ViewGroup设置Flag来,第二种就是通过代码,调用ViewGroup中的requestDisallowInterceptTouchEvent()方法设置:

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

可以看到,其实通过代码的方式最后还是将ViewGroup设置FLAG_DISALLOW_INTERCEPTflag。在我们的代码中,使用getParent().requestDisallowInterceptTouchEvent(true);可以剥夺父view 对除了ACTION_DOWN以外的事件的处理权。

为什么是ACTION_DOWN以外的呢?
其实在第23行代码resetTouchState()的方法中,有一行代码:mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;也就是每次来到ViewGroupdispatchTouchEvent()方法里面时,都会重置这个flag。因此,设置FLAG_DISALLOW_INTERCEPTflag并不能影响ViewGroupACTION_DOWN的处理。

好了,扯远了,调用onInterceptTouchEvent()方法,然后将结果赋值给intercepted。那就来看下ViewGroup与众不同与View特有的onInterceptTouchEvent方法:

public boolean onInterceptTouchEvent(MotionEvent ev) 
    return false;

很简单,直接返回false。但是我们可以重写这个方法,也就是说,如果我们在ViewGroup中重写了onInterceptTouchEvent()方法,并且让它返回true。那么intercepted也为true,表示拦截事件。

第57行,如果既不是ACTION_CANCEL,也不拦截事件,那么就会进行下面的判断:

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

在第79行判断了childrenCount != null,然后从子View的最后一个元素开始往前遍历。

第106行判断判断子view是否能接受点击事件。

判断完了之后在137行有一个newTouchTarget = addTouchTarget(child, idBitsToAssign);语句,查看addTouchTarget()方法,会发现里面有一行代码是这样的:mFirstTouchTarget = target;这里就是将mFirstTouchTarget赋值的地方。

也就是说,当子View存在,并将事件传递给了子View后,mFirstTouchTarget就不为null了,
这个时候,如果在传入ACTION_MOVEACTION_UP事件进入ViewGoup的dispatchTouchEvent()方法后,由于不会调用cancelAndClearTouchTargets(ev)方法,mFirstTouchTarget也就不为null!!!这种情况需要特别注意,将直接影响代码的分析。

如何派发事件呢?注意到第121行有一个判断:if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign),这个dispatchTransformedTouchEvent()在后面还会出现,可以看一下里面的代码。由于代码比较长,而且我们关心的是和child有关的代码,抽象出来,就如同下面的判断:

if (child == null) 
    handled = super.dispatchTouchEvent(event);
 else 
    handled = child.dispatchTouchEvent(event);

也就是说,如果子view为空,则调用ViewGroup自身的dispatchTouchEvent()方法,如果不为空,则调用子View的dispatchTouchEvent()方法。这里就是派发事件的地方。

回到我们分析的地方。在第121行,由于判断了子View非空,则会调用子View的dispatchTouchEvent()方法,该子View既可能是View,也可能是ViewGroup。而如果没有子View,就会在164行调用super.dispatchTouchEvent()方法(其实也是View的dispatchTouchEvent()方法,因为ViewGroup的父类就是View,然而View的dispatchTouchEvent()方法和ViewGroup的那个方法是有区别的)。

现在,让我们总结一下:

  • ViewGroup执行dispatchTouchEvent()方法后,会执行onInterceptTouchEvent()方法。
  • 如果我们重写onInterceptTouchEvent()方法,并且返回true,那么就会调用父类的dispatchTouchEvent()方法。
  • 如果我们没有重写onInterceptTouchEvent()方法,或者重写了该方法但是返回了false。如果有子View,则会调用子View的dispatchTouchEvent()方法,如果没有子View,则调用父类(View)的dispatchTouchEvent()方法。

看来无论怎么样都会触发到View中的dispatchTouchEvent()方法,而且对于我们最开始的问题,也是和View中的dispatchTouchEvent()方法的返回值有直接的关系。那么,该方法里面又是怎样一番美景呢?

以上是关于Android事件分发——ViewGroup篇的主要内容,如果未能解决你的问题,请参考以下文章

Android 事件分发事件分发源码分析 ( ViewGroup 事件传递机制 二 )

Android 事件分发事件分发源码分析 ( ViewGroup 事件传递机制 一 )

Android 事件分发事件分发源码分析 ( ViewGroup 事件传递机制 七 )

Android 事件分发事件分发源码分析 ( ViewGroup 事件传递机制 六 )

Android ViewGroup事件分发机制

Android 事件分发事件分发源码分析 ( ViewGroup 事件传递机制 四 | View 事件传递机制 )