彻底弄清事件分发流程之ViewGroup源码详细分析

背景:

在android开发中,经常会遇到触摸事件的分发处理,事件冲突,事件消费,如果界面比较复杂,一旦出现问题,如果对事件的分发处理机制不了解的话,这将使得我们难以处理,不知道从何处着手处理,更不知道该怎么去修改。但是如果我们对事件的分发处理机制非常熟悉,那么处理这些冲突或者由于事件消费问题引起的bug,我们就能很好的处理,并且毫不费力。
举个栗子:比如你在一个scrollView里面嵌套了一个RecyclerView然后在RecyclerView里面,你又放了一个自己定义的可以伸缩并且滑动展开折叠的一个View,这个时候就很容易出现问题,而不能达到你所期望的效果。还有等等的一些滑动冲突的问题…

由于上面所述的背景,如果我们想要在开发过程中游刃有余,并且满足ui的各种无理要求,那么事件的分发流程就是我们必须要弄清楚的一件事。好了废话不多说下面开始事件流程分析。

在Activity.java这个类中有个dispatchTouchEven() 方法,从字面意思来看是:分发触摸事件,我们来看看源码。

/**
     * 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.
     *
     * 翻译:这个方法用来处理触摸屏幕事件,你可以重写这个方法去拦截所有的触摸屏幕事件
     * 在这些事件被分发到window之前。确保这个方法实现能够正常的处理屏幕的触摸事件。
     *
     * @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);
    }

从这个方法描述我们可以得到几个信息:

  1. 这个方法时处理屏幕触摸事件的。
  2. 我们可以重写这个方法来拦截所有的屏幕触摸事件,不让这个事件再向下分发了,这个事件让我自己来处理。
  3. 重写的话,一定要确保这个方法能正常处理这个事件。

往这个方法里面看

  1. 如果这个事件是【按下】事件,那么先执行onUserInteraction()方法,进入这个方法看到,这个方法是一个空的实现,里面什么都没写,而且这个方法申明是public,这就意味着:我们可以重写这个方法,在用户刚开始【按下】的时候,就会执行到我们重写的这个方法里面,可以做一些事件预处理。
  2. 把事件传递给window,如果 windowsuperDispatchTouchEvent(ev) 方法执行结果为true 代表事件被消费掉了,那么dispatchTouchEvent(MotionEvent ev)方法返回true;如果window执行结果为false 代表传下去的事件没有被消费掉,那么activity将执行自己的onTouchEvent(ev)方法,并把结果继续往上返回给调用者。

我们来看下window是如何处理的:

public Window getWindow() {
        return mWindow;
    }

我们可以看到这个getWindow()方法,返回的是一个Window对象,进入Window类查看:

/**
 * Abstract base class for a top-level window look and behavior policy.  An
 * instance of this class should be used as the top-level view added to the
 * window manager. It provides standard UI policies such as a background, title
 * area, default key processing, etc.
 *
 * <p>The only existing implementation of this abstract class is
 * android.view.PhoneWindow, which you should instantiate when needing a
 * Window.
 *
 * 翻译:这个抽象类,仅存的实现类是android.view.PhoneWindow。
 */
public abstract class Window {

我们看到了关键的一个描述:The only existing implementation of this abstract class is
android.view.PhoneWindow。意思是这个抽象类,有且只有一个实现类,就是PhoneWindow。那么我们去找PhoneWindow查看:

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

在PhoneWindow里面,我们找到了superDispatchTouchEvent(MotionEvent event)这个方法,这个方法实际上是调用了mDecor的superDispatchTouchEvent(event)方法。那我们又去找mDecor是什么:

 // This is the top-level view of the window, containing the window decor.
    // 翻译:这个是window最顶层view,包含window的装饰。
    private DecorView mDecor;

在PhoneWindow中找到mDecor的申明,实际是DecorView,是window的顶层view,如果了解activity的启动流程,DecorView并不陌生。好了继续进入DecorView这个类里面去找superDispatchTouchEvent(event)方法:

public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {

首先我们可以看到DecorView的申明,DecorView其实他的本质是一个继承自FrameLayout的View。继续找到superDispatchTouchEvent()方法:

public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

这个方法又调用了父亲的dispatchTouchEvent(event)方法,那我们点进去看父亲的实现,点进去之后发现到了ViewGroup的dispatchTouchEvent()方法,终于找到我们今天的重点了,咋一看这个方法里面的代码非常多,但是其实逻辑很简单,让我们来分析分析,由于代码片段比较长,我把这块方法逻辑大致分成三个大块

  1. 判断是否拦截代码块。
  2. 找寻消费事件的子view。
  3. 分发事件。
 /** 这个是ViewGroup的分发事件方法,主要是向下传递事件,然后返回结果给调用者 */
    @Override
    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)) {...}

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

        /** 返回定义的局部变量 */
        return handled;

这是整个方法的概览,我把中间if (onFilterTouchEventForSecurity(ev)) {…}这里的代码逻辑折叠起来了,这样我们先窥整个方法的全貌。这里定义了一个是否处理事件的局部变量,根据逻辑给变量赋值,最后返回给上层调用者。最关键的逻辑就在中间被折叠起来的那里。按照我们上面分的3大块,我们把中间那部分代码依次拆开:
#第一块,是否拦截逻辑:

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

                /** 如果事件是【按下】事件,或者触摸对象 mFirstTouchTarget 不为空,进入这里 */

                /**
                 * disallowIntercept, 是否被禁止拦截的标志,通过 FLAG_DISALLOW_INTERCEPT 这个标志位判断。
                 *  */
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;


                if (!disallowIntercept) {

                    /**
                     * 如果没有禁止拦截即 disallowIntercept = false,
                     * 调用ViewGroup的onInterceptTouchEvent(ev),并把返回值给 intercepted 这个局部变量。
                     * */

                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {

                    /** 如果禁止拦截,intercepted 这个局部变量置为false */
                    intercepted = false;
                }
            } else {
                /**
                 *  要进入这个判断,必须同时满足两个条件
                 *  1、事件不是Down【按下】。
                 *  2、mFirstTouchTarget 当前触摸目标为null,即没有触摸目标。
                 *  这个触摸目标的概念:消费事件的view就是触摸目标。
                 *  */
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }

有些概念我做一个解释:

  1. 一个完整的事件,是从【Down】到【Up】的过程。【Dow】表示事件的开始,【Up】表示事件结束。
  2. 触摸目标:表示如果有子view消费了【Down】事件,那么我们就把这个view保存在一个叫TouchTarget的链表数据结构中,我们姑且把它称为触摸目标。也就是说,如果没有子view消费【Down】事件,那么触摸目标对象就是null。

源码中我已附上了解释,我这里在连贯的描述一下,我们只分析重点的地方:

  1. 事件传递进来 ==》如果是【Down】事件 或者 触摸目标对象不为NULL ==》先检查自己禁止拦截事件的标志FLAG_DISALLOW_INTERCEPT,如果disallowIntercept=false,那么当前ViewGroup会去调用自身的onInterceptTouchEvent方法,询问是否要拦截这个事件,返回值保存在局部变量中;如果禁止拦截,那么局部变量恒等于false。这个局部变量intercepted 将影响后面第三块逻辑,是否分发结束事件给子view,所以等我等会儿回过来再连起来看。
  2. 事件传递进来 ==》如果既不是【Down】事件,而且 触摸目标对象 为NULL, 那么这就表示,在一个完整的事件中,没有消费这个完整事件的子view,那么intercepted 这个拦截标志将一直为true。
    第一块代码分析完毕,上面主要讲是intercepted 的赋值逻辑。

#第二块:寻找消费事件的子View

            /** 定义一个触摸目标 局部变量 */
            TouchTarget newTouchTarget = null;

            /** 是否已经分发给新的触摸目标 局部变量 */
            boolean alreadyDispatchedToNewTouchTarget = false;

            /**
             *  如果canceled = false 并且 intercepted = false
             *  即:
             *      1、不是【取消】事件
             *      2、不拦截(不拦截可以理解为:不拦截就意味着可以向下传递这个事件,拦截意味着就不向下传递事件了)
             *
             *  执行以下逻辑
             *  */
            if (!canceled && !intercepted) {

                // If the event is targeting accessibility 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.

                        /**
                         *  buildTouchDispatchChildList这个方法得到一个由Z从小到大排序的子view列表
                         *  Z:可以参照Z轴,距离用户越近Z值越大。
                         *  */
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();

                        final View[] children = mChildren;

                        /** 列表从后往前遍历,即Z从大到小遍历,先从离用户最近的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.
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }

                            /**
                             * 这两有两个判断
                             * 1、子view是否可以接收触摸事件
                             * 2、触摸点是否在子view区域上
                             * 如果任一一个条件不满足,直接continue跳过,寻找一个子view
                             * */
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

                            /**
                             *  执行到这一步,表示子view能接收触摸事件,并且触摸点在子view的区域上
                             *  把子view所在的触摸目标,赋值给newTouchTarget这个局部变量,
                             *  如果mFirstTouchTarget=null,那么newTouchTarget=null
                             *
                             *  这个方法表示意思是:如果这个子view之前已经被保存过了,那么直接把他给newTouchTarget赋值
                             *  */
                            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 这个方法里面根据条件,是否向下分发事件 */
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {

                                /** 进到这个执行逻辑里面表示: 有子view消费掉了这个事件 */

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

                                /**
                                 * addTouchTarget() 这个方法里面,把子view封装成一个触摸目标对象,
                                 * 并且mFirstTouchTarget这个全局变量被赋值为当前这个触摸目标对象
                                 * */
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);

                                /** 这个标志表示:已经分发给新的触摸对象了 */
                                alreadyDispatchedToNewTouchTarget = true;

                                /** 这里如果找到了处理事件的子View,那么退出循环 */
                                break;
                            }

                            // The accessibility focus didn't handle the event, so clear
                            // the flag and do a normal dispatch to all children.
                            ev.setTargetAccessibilityFocus(false);
                        }

                        /** 遍历循环走完后,将子view列表清空 */
                        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;
                    }
                }
            }

这个代码块里面主要是在找寻消费事件的子view:如果当前不是【Cancel】事件并且intercepted = false,会进入这个逻辑里面寻找消费事件的子view。但是,如果我们的ViewGroup拦截这个事件,那么就不会进到这个逻辑里面,即:不会去找消费事件的子view,那么这就会导致mFirstTouchTarget永远为NULL,触摸目标为NULL。

在这个条件里面做了几件事:

  1. 把所有的子view按照Z值大小,从小到大放入一个list中。
  2. 从后往前遍历这个list,即:先从离用户最近的view开始找,以此类推。
  3. 每个子view必须满足两个条件,才能将这个事件分发给子view。
    3.1. 子view是否可以接收触摸事件。
    3.2. 触摸点在子view的区域内。
  4. 将【Down】事件分发给满足条件的这个子view。
  5. 如果满足条件的这个子view,消费了这个事件,那么将这个子view保存到触摸目标这个链表结构内并且退出遍历,即找到一个消费事件的子view后,就不会继续找了。但是如果满足条件的子view,不消费事件,那么就继续寻找。直到完全遍历完。

第二块内容大致就是这样,主要是在【Down】事件,找寻消费事件的子view,如果没有找到子view,那么触摸目标将无法赋值,也即:mFirstTouchTarget == NULL

#第三块:分发事件

            // Dispatch to touch targets.
            /**
             * mFirstTouchTarget=null
             * 其实实际的意思是:这个事件没有一个子view来消费,所以这个对象没有地方被赋值。
             * 只有当子view消费了【按下】事件后,mFirstTouchTarget这个对象才会被赋值,否则一直都是null。
             * 那么就有一个结论:如果没有子view消费【按下】事件,那么当前ViewGroup对象会把事件交给自己处理。
             * 再将处理结果返回给上层调用者。
             * */
            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.

                /** 进入到这里表示:有子view消费了【按下】事件 */

                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;

                    /**
                     * 如果
                     * 1、局部变量表示已经分发给新的触摸目标
                     * 并且
                     * 2、局部变量newTouchTarget = mFirstTouchTarget
                     *
                     * 这两个条件要满足,那必须是刚刚上面循环子view的时候找到了消费【按下】事件的子view
                     * 在上面逻辑中,已经分发事件给子view处理了,所以不能再分发一次,否则就会分发两次
                     * 【按下】事件给子view。只有除了【按下】事件的其他事件,才可以在这里分发给子view
                     *
                     * 那么这次事件就不在分发给子view处理
                     * */
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {

                        /**
                         *  局部变量 cancelChild :表示是否给子view分发一个【cancel】事件,结束掉子view的事件处理
                         *  有这种情况:如果子view消费了【按下】事件,但是如果在【MOVE】事件的时候,当前ViewGroup
                         *  拦截了事件即:intercepted = true,那么dispatchTransformedTouchEvent()这个方法
                         *  会分发一个【Cancel】事件给子view,并且将这个子view从触摸目标链表中移除。表示这个子view
                         *  不会在接收到任何事件消息了, mFirstTouchTarget 对象重置为触摸目标链表中的第一个。
                         * */
                        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;
                }
            }

第三部分分发事件:

  1. mFirstTouchTarget = null 表示没有子view消费事件,那么事件将交给ViewGroup自己来处理。
  2. 如果【Down】事件的时候,找到了消费事件的子view,即mFirstTouchTarget != null
    分发给子view。
    这里有有两个关键的判断:

第一个关键:如果是【Down】那么不会重复向子view分发事件

/**
                     * 如果
                     * 1、局部变量表示已经分发给新的触摸目标
                     * 并且
                     * 2、局部变量newTouchTarget = mFirstTouchTarget
                     *
                     * 这两个条件要满足,那必须是刚刚上面循环子view的时候找到了消费【按下】事件的子view
                     * 在上面逻辑中,已经分发事件给子view处理了,所以不能再分发一次,否则就会分发两次
                     * 【按下】事件给子view。只有除了【按下】事件的其他事件,才可以在这里分发给子view
                     *
                     * 那么这次事件就不在分发给子view处理
                     * */
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    }

我们根据第二块,知道alreadyDispatchedToNewTouchTarget这个局部变量只有在【Down】事件,并且找到子view消费的情况下,才会被赋值为true,否则都是false。如果是【Down】事件传递到这里,这两个条件都为true,所以就不会再分发一次,要不然的话,就会出现分发两个【Down】事件给子view的情况,因为在第二块寻找子view的时候已经分发过一次【Down】事件了。

第二个关键:是否主动取消子view消费

 /**
                         *  局部变量 cancelChild :表示是否给子view分发一个【cancel】事件,结束掉子view的事件处理
                         *  有这种情况:如果子view消费了【按下】事件,但是如果在【MOVE】事件的时候,当前ViewGroup
                         *  拦截了事件即:intercepted = true,那么dispatchTransformedTouchEvent()这个方法
                         *  会分发一个【Cancel】事件给子view,并且将这个子view从触摸目标链表中移除。表示这个子view
                         *  不会在接收到任何事件消息了, mFirstTouchTarget 对象重置为触摸目标链表中的第一个。
                         * */
                        final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;

如果intercepted = true,即表示:拦截事件,cancelChild = true,在dispatchTransformedTouchEvent() 这个方法中,就会给子view,分发一个【Cancel】事件,子view接收到【Cancel】事件,然后ViewGroup将子view触摸目标从链表中移除,后续将不在给这个子view分发事件了。

结论:可以看出是否取消子view,关键在于 intercepted 这个局部变量。这就跟我们第一块是否拦截的逻辑那里联系起来了。如果当前ViewGroup拦截事件,那么直接给子view发一个【Cancel】事件,结束子view接收事件。

到此我们把ViewGroup里面的骨干逻辑就全部了解清楚了,至于没有细小的逻辑我们就不一个个说了,比如:怎么得到的按Z排好序的列表的,怎么判断子view是否能接收事件的,触摸点是否在子view上的,这些都是具体的逻辑了,如果要一个个放到文章里面,那就实在太多了。

综合上述:我们可以以一个故事场景来描述这个分发事件流程。我们就以一个警察局破案这么一个场景来描述一下吧。我们知道一个完整的事件:以【Down】开始,以【Up】或者【Cancel】结束。现实中我们以【报案】开始,以【结案】结束。
场景描述:
1. 有一天【市公安】接到【报警】,【市公安】根据这个案子的严重情况,来判断是否由【市公安】自己亲自处理。如果【市公安】自己觉得这是个小案子,自己不想处理,那么就找【市公安】下属的【公安机构】去处理这个案子。
2. 【市公安】根据报案人的【事发地区】,把下属【公安机构】按照离【事发地区】距离由远到近以此排序,然后从最近的开始找,如果【公安机构】目前有条件可以空出来处理,那么【市公安】就把这个案子交给找到的这个下属【公安机构】去处理,并且在【市公安】这里做好记录信息,如果下属【公安机构】没有条件处理,那么就继续找直到找完下属【公安机构】为止,如果一个都没法处理那么就由【市公安】自己来处理。如果已经有下属【公安机构】来处理了,那么之后,所有关于这个案子的所有信息全部都交给这个【公安机构】处理。
3. 但是【市公安】一直会跟踪这个事情的严重性来判断是否需要自己来处理,如果有一天这个【案子】变严重了,【市公安】说,这个案子不用你处理了,我们自己会亲自处理,然后会给下属【公安机构】通知这个案子你们不用处理了。然后后续的这个【案子】信息全权由【市公安】自己处理。
4. 如果下属【公安机构】明确告诉【市公安】说,这个事情请你不要插手,那么【市公安】说,好我们不插手,然后整个【案子】在【结案】之前都全权由下属【公安机构】处理。

结尾

这就是ViewGroup里面的事件分发流程,下一篇我们一起看View的分发!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值