美文网首页
Android ViewGroup事件分发机制

Android ViewGroup事件分发机制

作者: htkeepmoving | 来源:发表于2018-11-07 23:30 被阅读0次
参考资料

鸿洋版ViewGroup事件分发机制
郭霖版ViewGroup事件分发机制
Android开发艺术探索

上一篇已经分析了Android View的事件分发机制,本篇将根据源码讲解ViewGroup的事件分发机制,View的一大难题是滑动冲突,滑动冲突的理论基础就是事件分发机制,所以了解事件分发机制,也有益于大家了解冲突产生的原因,以及对冲突进行处理。
关于事件传递机制,这里先给出一些结论,根据这些结论可以更好的理解整个传递机制:
(1)同一事件序列是指从手指触摸屏幕的那一刻起,最手指离开屏幕的那一刻结束,在整个过程中
所产生的一系列事件,这个事件序列以down事件开始,中间含有不定数量的move,最终以up事件结束;
(2)事件传递都是由外向内传递的,即事件总是先传递给父view,然后再由父View传递给子view;
通过requestDisallowInterceptTouchEvent可以在子元素中干预父View的时间分发过程,但是ACTION_DOWN除外;
(3)如果某个View一旦开始拦截某个事件,那么同一事件序列都只能由他来处理,
并且它的onInterceptTouchEvent不会再次被调用。
(4)View没有onInterceptTouchEvent方法,一旦有事件传递给它,那么它的onTouchEvent就会被调用,
View的ontouchEvent默认都会消耗事件,viewGroup默认不拦截事件,Android源码中可以看到onInterceptTouchEvent
默认返回值为false;
(5)子元素是否可以接受点击事件?第一,子元素是否在做动画;第二,点击事件的坐标是否落在子元素区域中。
(6)如果所有子元素都没有处理事件,这里包含两种情况,第一ViewGroup没有子元素,第二子元素处理了点击事件,但是在dispatchTouchEvent中返回false,一般是因为子元素在onTouchEvent返回false,这两种情况ViewGroup会自己处理点击事件
(7)事件大体传递流程:VeiwGroup的dispatchTouchEvent -> VeiwGroup的onInterceptTouchEvent ->View的dispatchTouchEvent ->View的onTouchEvent

源码解析

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;

            // Handle an initial down.
            // 处理原始的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.
                // 需要在新事件开始时处理完上一个事件,并且调用resetTouchState重置状态
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            // Check for interception.
            // 检查当前View是否拦截事件
            // ViewGroup在如下两种情况下会判断是否拦截当前事件:事件类型为down或者mFirstTouchTarget != null。
            // 当ViewGroup不拦截事件并将事件交由子元素处理时,mFirstTouchTarget会被赋值也就是mFirstTouchTarget != null。
            // 这样当move事件和up事件到来时,并且事件已经被分发下去,那么onInterceptTouchEvent这个方法将不会再被调用。
            //所以当前ViewGroup拦截事件之后就不会再次调用onInterceptTouchEvent方法;
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    //调用onInterceptTouchEvent方法判断是否拦截当前事件,ViewGroup默认返回false;
                    //如果拦截事件将intercepted置为true;
                    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;
            }

            // 如果事件未被取消且未被拦截,如果拦截事件会将intercepted置为true;
            if (!canceled && !intercepted) {
                //ACTION_DOWN事件 
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    // 当ViewGroup不拦截事件的时候,事件会向下分发交由它的子View进行处理
                    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.
                        // 从上至下去寻找一个可以接收该事件的子View
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        // 遍历ViewGroup的所有子元素,然后判断子元素是否能够收到点击事件
                        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.
                            // 是否能够收到点击事件主要由两点来衡量:
                            // 子元素是否在播放动画和点击事件的坐标是否落在子元素的区域内。
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }

                            // 子元素是否能够接收PointerEvent,或事件有没有落在子元素的边界范围
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }
                            resetCancelNextUpFlag(child);
                            // 如果某个子元素满足条件,那么事件就会传递给它处理,
                            // dispatchTransformedTouchEvent这个方法实际上就是调用子元素的dispatchTouchEvent方法,
                            // 如果子元素仍然是一个ViewGroup,则递归调用重复此过程。
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                // 子View在其边界范围内接收事件
                                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();
                                // 如果子元素的dispatchTouchEvent返回true,表示子元素已经处理完事件,
                                // 那么mFirstTouchTarget就会被赋值同时跳出for循环。
                                // mFirstTouchTarget的赋值在addTouchTarget内部完成
                                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();
                    }
                }
            }

            // Dispatch to touch targets.
            // 如果遍历完所有的子元素事件没有被合适处理,有两种情况:
            // 1. ViewGroup没有子元素
            // 2. 子元素处理了点击事件,但是dispatchTouchEvent返回false
            // 这时ViewGroup会自己处理点击事件。
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                // 这里的第三个参数child为null,此时会调用handled = super.dispatchTouchEvent(event);
                //最终会调用ViewGroup自身的onTouchEvent来处理事件;
                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;
                //将处理该事件的子view复制给target,由子元素来处理该事件
                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;
                        //该方法最终会调用子元素的dispatchTouchEvent,传给给子元素来处理事件    
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                    }
                }
            }

            // Update list of touch targets for pointer up or cancel, if needed.
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                //UP事件到来,重置状态,例如将处理该事件的子view mFirstTouchTarget置为null;   
                resetTouchState();
            }
        }
        return handled;
    }

说明:
(1)如果onInterceptTouchEvent返回true,表示ViewGroup将拦截事件,会将intercepted设置true,这样就不会遍历子view寻找事件接受者;这样mFirstTouchTarget为null,会调用dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        if (child == null) {
            //因为传入的参数为null,所以调用ViewGroup自身的dispatchTouchEvent方法来处理事件;
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            //当有子元素处理事件时,会调用子元素的dispatchTouchEvent
            handled = child.dispatchTouchEvent(transformedEvent);
        }
        return handled;
    }

(2)当子元素处理事件时会调用addTouchTarget();

    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        // 由子元素处理事件时,将子元素赋值给mFirstTouchTarget
        //这样通过mFirstTouchTarget是否为null,就可以知道事件时由子view还是ViewGroup来处理事件;
        mFirstTouchTarget = target;
        return target;
    }

(3)如果ViewGroup找到了能够处理该事件的View,则直接交给子View处理,自己的onTouchEvent不会被触发;
(4)可以通过复写onInterceptTouchEvent(ev)方法,拦截子View的事件(即return true),把事件交给自己处理,则会执行自己对应的onTouchEvent方法
(5)子View可以通过调用getParent().requestDisallowInterceptTouchEvent(true); 阻止ViewGroup对其MOVE或者UP事件进行拦截;
(6)父View可以通过onInterceptTouchEvent来拦截事件,但是如果父view不拦截down事件,子view如果调用requestDisallowInterceptTouchEvent方法,那么即使父View在move和up的时候return true,也不会将事件拦截掉,也只会调用子view的onTouchEvent;当面对滑动冲突时,我们可以考虑通过requestDisallowInterceptTouchEvent设置FLAG_DISALLOW_INTERCEPT标志位来解决滑动冲突;如果父View拦截Down事件,那么子View将不会收到事件;
(7)滑动冲突的理论基础是事件分发机制,所以熟悉事件分发机制有助于我们解决滑动冲突相关问题;
以上就是事件分发机制的全部内容。最后,本文部分内容直接从其他地方文章直接Copy而来,感谢本文内容所参考文章的作者;

相关文章

网友评论

      本文标题:Android ViewGroup事件分发机制

      本文链接:https://www.haomeiwen.com/subject/geqjxqtx.html