• Android View的事件分发


    假设手机屏幕上有一个button。我们去点击了一下,然后button做出了相应的反应,那么这个过程其实这样的,当手机点击到屏幕时,TP(Touch panel)传感器的数据发生了变化。数据经过驱动的处理(其实用示波器来看传感器数据,这个数据肯定不可能那么规整),然后数据依次传递到内核,framwork,然后再传递相应的app的当前Activity中。我们说android中View的事件分发,其实指的就是在此之后的过程。当手指接触屏幕所产生的反应,我们称之为事件(MotionEvent).
    当然MotionEvent 实际上也可以细分为很多种,典型的有以下几种。

    • ACTION_DOWN: 手指刚刚接触到屏幕。
    • ACTION_MOVE:手指在屏幕上滑动。
    • ACTION_UP:手指从屏幕上离开的一瞬间。

    所以正常情况下,手指触摸屏幕会触发一系列事件。
    比如 DOWN -> MOVE -> UP。从手指接触屏幕那一刻起,到手指离开屏幕那一刻结束,中间所产生的一系列事件总和,我们称之为一个事件序列。它以一个down事件开始,中间夹杂着0个或多个move事件,以一个up事件结束。

    我们手指接触到屏幕,怎么判断是点击还是滑动呢?其实在手机framework中,有一个默认值,比如8px(和具体的设备和手机厂商有关),当手指移动的距离大于该值时,就是滑动事件TouchSlop,否则就是点击事件。

    我们所讨论的事件分发,指的就是 MotionEventActivity -> Window -> ViewGroup -> View这个过程。 (毕竟我们都是 做上层app开发的,像TP数据怎么处理之类的那些是驱动工程师们关心的问题)。

    这个事件分发的过程,总结起来其实很简单,因为该场景和我们日常生活中的场景是一致的。

    boss(老板)要做某件事情,但是既然身为boss,人家肯定不用自己动手嘛,所以这事交给下面的manager(经理)就好了,经理好歹也是领导嘛,所以也不用自己亲自动手,交给下面的worker(工人)就好了,所以转了一圈,干活的还是最下面的工人,如果worker把这活处理了,那么这件事情就算完了,可是也有例外情况,如果worker发现这个活自己处理不了,那么这事就只能向上反馈给manager 了,如果manager能把这活给处理了,那自然最好,可是如果manager发现这活自己也处理不了,那就只能再反馈给boos,让boss去处理了

    事件分发也是如此。点击屏幕上的一个点,事件那么经过activity -> window ,然后传递到 界面最外层(或者叫最顶级)的ViewGroup容器中(指的是Relativelayout,LinearLayout等)。 然后最后依次传递到被包裹在最里面到 View中。(当然,如果View包裹的有几层的ViewGroup,那么肯定是依次先传递给它们, 并且需要注意,最里面的控件完全可以是一个ViewGroup,而它不必包含View。)

    事件被传递到了最里面的View,所以交给了View来处理,如果View没处理,那么就依次倒过来向上回溯。分别交给各级ViewGroup,如果它们也没处理,那么就继续向上回溯,乃至Window,Activity中来处理。

    View就是最底层的worker,而boss,manager都是包裹在它外面的ViewGroup,如果层级比较复杂的话,还要经过多层才能传到到最底层的View,(比如经过老板,总经理,部门经理,项目经理等多层,事情才分配到最底层的工人这里), View处理不了的,那么就再依次向上反馈给各级领导,看看谁去处理。

    该过程其实就是一个递归的过程。这是android系统中默认的事件分发规则,如果我们理解了这个规则,那么我们就可以解决很多实际问题,比如 我们可以在某一层ViewGroup中来拦截处理事件,而不用向下传递到View中。

    Notice:

    1. 因为ViewGroup extends View,所以从继承关系上来讲,ViewGroup也是一个View,而在本文当中说的ViewGroup指的是 RelativeLayoutLinearLayout 等容器类View,而View指的是Button,TextView等不会再有child的View(因为它们不会再包裹什么了)。之所以要明确区分,是因为它们的有些方法的实现是有区别的。
    2. 为了方便,在本文当中,称呼控件指的是View或ViewGroup(也可能是View和ViewGroup)。
    3. 在本文当中,一个控件处理了事件,并return了对应的boolean值,我们称之为 事件被 消费了。(如果没有return值,我们怎么知道这个事件是否被处理了呢?)

    既然是事件分发,那么不得不提到到几个主要方法。

    View中:

         //从名字中就可以看出来,是负责分发事件的方法。
         public boolean dispatchTouchEvent(MotionEvent event)
        
        //该方法中设置去消费事件,比如我们平时常用的OnClickView,OnLongClick等监听事件都是在这里被调用的。
        public boolean onTouchEvent(MotionEvent event)
    

    实际上,View当中也有具体负责消费事件的接口。

        public interface OnTouchListener {
            boolean onTouch(View v, MotionEvent event);
        }
        
        // 这个就是我们最常用的 点击事件方法。
        public interface OnClickListener {
            void onClick(View v);
        }
    

    说完了View,那么再来看看ViewGroup中的主要方法。

        //View中也有该方法,但是和ViewGroup中的实现方式不同
        public boolean dispatchTouchEvent(MotionEvent ev) 
        
        //该方法是ViewGroup特有的,决定是否拦截事件。(默认返回false),如果返回true,则事件不会再向child传递了,这个道理很简单,ViewGroup是属于领导, 它才有权利决定是否拦截事件。而View是最底层的工人,是没有决定权的。
        public boolean onInterceptTouchEvent(MotionEvent ev)
        
        
        
        //几个相关方法或接口中,ViewGroup实现了这两个,其他都没重写,也就是利用View当中的实现
    

    和事件分发有关的方法就这几个。基本的道理其实也不复杂。(联想老板给工人分配任务的场景就可以了)。
    而关于这个过程,可以使用伪代码描述如下:
    对于View:

    
    //@author www.yaoxiaowen.com
    boolean dispatchTouchEvent(MotionEvent event){
        boolean result = false;
        if (OnTouchListener.OnTouch(view, event)){
            result = true;
        }
        if (!result && onTouchEvent(event)){
            result = true;
        }
        return result;
    }
    
    //实际上这个类,处理的比较复杂,比如分为 ACTION_DOWN, ACTION_UP 之类的,
    //不过这里是仅仅是作为示例的伪代码。
    //@author www.yaoxiaowen.com
    boolean onTouchEvent(MotionEvent event){
        if (clickable || clickable){
            if (event.getAction()==ACTION_UP && OnClickListener!=null){
                OnClickListener.onClick();
            }
            return true;
        }
        return false;
    }
    
    

    而对于 ViewGroup,伪代码如下:

    
    //@author www.yaoxiaowen.com
    boolean dispatchTouchEvent(MotionEvent event){
        boolean result = false;
        if (onInterceptTouchEvent(event)){
            //Notice:这个地方不是调用 的this.onTouchEvent。调用自己那就是死循环了,既然调用了父类View的dispatchTouchEvent(),想想View的该方法做什么,所以实际上就是把事件分发给自己了。
            //(因为类似onTouchEvent, onTouchListener方法在ViewGroup当中没实现,只在父类View当中才实现)
            reuslt = super.dispatchTouchEvent();
        }else{
             //child可能是 ViewGroup,也可能是 View
            result = child.dispatchTouchEvent(event);
        }
        return result;
    }
    

    结合伪代码,再想象具体的分发流程。仔细琢磨一下,就可以大概的了解这个分发流程了。

    我看网上的很多关于事件分发的博客解释这个问题时,都是采用画图的方式来解释的,但是我觉的相比于图片或流程图,伪代码更加清晰明了,(当然,也许是我作为程序员,对代码更加敏感一些)。另外,任玉刚 的《Android开发艺术探索》中,是使用伪代码来表示的,但是只使用了一段伪代码,就是View和ViewGroup都采用这一段伪代码来表示,但是我觉得,虽然ViewGroup的确是View,但是它们在事件分发时,行为还是有很大差异的。所以使用不同的伪代码来表示更通俗易懂一些。

    虽然总结的内容的不复杂,但是实际上依旧有很多的细节。下面我们就结合具体的源码来进行分析。(具体的源码非常复杂,参考了不少博客,然后自己也读了几遍,但是也只是理解了一小部分,毕竟自己也水平有限。不过我们要分析的也就是流程的主干内容。)

    沿着事件分发的流程。Activity -> Window -> ViewGroup -> View.

    先看Activity:

     //Activity.java
    public boolean dispatchTouchEvent(MotionEvent ev) {
        //省略部分代码
    
        //从这句代码中,将事件分分发到了 Window 
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
    
     public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }
        return false;
    }
    

    在Activity中,事件被传递给了Window,但是此时要注意了,如果Window当中未消费事件。(就是返回了false)。那么此时就调用Activity自身的onToucheEvent()了。这在后面的流程中,原理是相同的。就是根据返回值来判断是否消费了事件,整个事件分发机制从上向下,再从下到上这套递归的回溯机制,就是如此。

    Activity当中把事件分发给了Window,不过Window类是abstract的,Window的唯一实现 就是PhoneWindow,然后这中间的传递过程就比较复杂(因为中间很多源码其实我也没看懂),反正最后就传递到了Activity布局中最外层的ViewGroup中。
    ViewGroup中的dispatchTouchEvent方法的实现就比较长了,所以我们这里就先从上到下分片段的来看这个方法的具体实现。

    
    //ViewGroup.java当中的代码
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        //首先,在该方法的开头,并没有 super()的代码,这说明 dispatchTouchEvent方法就是在ViewGroup当中实现的,View和ViewGroup当中各有一套实现规则。
    
        //......
    
        boolean handled = false;
       
            //......
    
            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
    
                    //Notice:注意 这个地方调用了 onInterceptTouchEvent 方法来判断在该ViewGroup当中是否拦截。
                    // onInterceptTouchEvent 默认的返回值 是false。
                    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);
            }
    
    

    而如果在ViewGroup中不拦截该事件,或者该事件不该在本ViewGroup当中处理,那么就会遍历它的child进行处理。

    
        //ViewGroup.java 的 dispatchTouchEvent(MotionEvent event)  方法
      // ...... 
    
                        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);
    
                                // 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;
                                }
    
                              //......
    
                                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;
                                }
    
                               //.....
    
    

    我们可以看到,这里调用了 dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)方法来进行事件分发,那么这个方法里面是做什么的呢。

     //ViewGroup.java  dispatchTouchEvent()中调用了 该方法
        /**
         * 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;
    
            // Canceling motions is a special case.  We don't need to perform any transformations
            // or filtering.  The important part is the action, not the contents.
            final int oldAction = event.getAction();
            if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
                event.setAction(MotionEvent.ACTION_CANCEL);
                if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
    
                    //这里将事件分发给了 child,并通过返回值来得到是否消费的结果 
                    handled = child.dispatchTouchEvent(event);
                }
                event.setAction(oldAction);
                return handled;
            }
            //.......
    
    

    自此,事件已经分发给了child,就看child会怎么进行处理了。
    不过关于上面的那个 dispatchTransformedTouchEvent()方法,除了把事件分发给child之外,还有其他作用,就是把事件分发给自己去处理。看代码逻辑的这两行

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

    因为在dispatchTouchEvent的方法代码中,后面有逻辑判断,如果child没有消费事件,(或者事件被拦截,或者压根就没有child等)。其实还会调用该方法,只是行参当中child的传入值为null。 那么事件传递到该ViewGroup中进行处理,

    以上的内容就是ViewGroup当中进行处理的相关代码,那么下面就看看View当中相关的处理方式。

    首先先看看是怎么进行分发的。

    
     //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;
    
           //......
    
            if (onFilterTouchEventForSecurity(event)) {
                if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                    result = true;
                }
    
                // 在这里,先查看是否可以调用 OnTouchListener.onTouch ()方法进行处理。
                //noinspection SimplifiableIfStatement
                ListenerInfo li = mListenerInfo;
                if (li != null && li.mOnTouchListener != null
                        && (mViewFlags & ENABLED_MASK) == ENABLED
                        && li.mOnTouchListener.onTouch(this, event)) {
                    result = true;
                }
    
                //如果没有调用 OnTouchListener.OnTouch()方法进行处理,那么则再将事件分发到onTouchEvent()当中进行处理.
                if (!result && onTouchEvent(event)) {
                    result = true;
                }
            }
    
           //......
            
            return result;
        }
    
    

    View中的 dispatchTouchEvent方法比着 ViewGroup当中的简洁了不少,也许是因为ViewGrop的主要功能就是包裹子元素所以功能比较复杂吧。

    而在View中的dispatchTouchEvent中,我们要注意的一点就是
    OnTouchListener.onTouch(View, MotionEvent)接口的调用,这个方法是在onTouchEvent()之前调用的。这也就是说,OnTouchListenerboolean onTouch(View, MotionEvent)方法的优先级是比较高的。 如果我们在代码当中设置了onTouch(View, MotionEvent) (并返回了true),那么我们事件就被消费了,就不会再分发到onTouchEvent(MotionEvent)了。

    再来看看onTouchEvent()的方法。

    
        //View 中的 onTouchEvent(MotionEvent)方法
         public boolean onTouchEvent(MotionEvent event) {
        
        //......
         
    
            if (((viewFlags & CLICKABLE) == CLICKABLE ||
                    (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                    (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
                switch (action) {
                    case MotionEvent.ACTION_UP:
                        boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                                
                                //......
    
                                // 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();
                                    }
                                }
                            }
    
                        //.....
                        break;
    
    

    在处理MotionEvent.ACTION_UP 当中,最后调用的是 performClick()方法。
    perfomClick()其实是这样的。

    
        /**
         * Call this view's OnClickListener, if it is defined.  Performs all normal
         * actions associated with clicking: reporting accessibility event, playing
         * a sound, etc.
         *
         * @return True there was an assigned OnClickListener that was called, false
         *         otherwise is returned.
         */
        public boolean performClick() {
            final boolean result;
            final ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnClickListener != null) {
                playSoundEffect(SoundEffectConstants.CLICK);
                li.mOnClickListener.onClick(this);
                result = true;
            } else {
                result = false;
            }
    
            //......
            return result;
        }
    
    
    

    在这个方法当中,我们终于看到了熟悉的 OnClickView接口,我们平时最常用的OnClickView接口的onClick(View)方法就是在这里被调用的。
    结合前面的分析,我们也可以知道。onTouchListener接口的优先级比 OnClickListener 要高。

    其实到这里,认真思索的朋友肯定会有疑问,我们刚才说,判断是否消费了事件,是根据返回值来判断的,boolean performClick()方法也 的确是有返回值的。但是认真看看在onTouchEvent(Motion) 方法中 调用到performClick()的地方, onTouchEvent()方法压根就没接收这个返回值啊。这是搞什么鬼呢。
    我们此时再来看看onTouchEvent(MotionEvent) 方法:

    
     //......
                       
       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) {
                   case MotionEvent.ACTION_UP:
                      //  ......
                       break;
    
                   case MotionEvent.ACTION_DOWN:
                         //  ......
                       break;
    
                   case MotionEvent.ACTION_CANCEL:
                       //  ......
                       break;
    
                   case MotionEvent.ACTION_MOVE:
                       //  ......
                       break;
               }
    
               return true;
           }
           return false;
    
    

    从代码里可以得到结论,只要 这个控件的 CLICKABLELONG_CLICKABLE 有一个为true,那么代码就会向下走,返回值就是true。也就是说 默认在这个View中,已经消费了这个事件了。

    但是这中间还有一个疑问, 其实对于View,LONG_CLICKABLE默认为false,而关于 CLICKABLE则不同的子类有不同的默认值。 比如 ButtonCLICKABLE默认为true,但是 TextViewCLICKABLE默认就是false。 可是对于 TextView,依旧可以正常使用setOnClickListener啊,其实原因也非常简单。
    看一下View的源码就知道了.

     
      /**
         * Register a callback to be invoked when this view is clicked. If this view is not
         * clickable, it becomes clickable.
         *
         * @param l The callback that will run
         *
         * @see #setClickable(boolean)
         */
        public void setOnClickListener(@Nullable OnClickListener l) {
            if (!isClickable()) {
                setClickable(true);
            }
            getListenerInfo().mOnClickListener = l;
        }
        
    

    顺便再看看 setOnLongClickListener方法。

      /**
         * Register a callback to be invoked when this view is clicked and held. If this view is not
         * long clickable, it becomes long clickable.
         *
         * @param l The callback that will run
         *
         * @see #setLongClickable(boolean)
         */
        public void setOnLongClickListener(@Nullable OnLongClickListener l) {
            if (!isLongClickable()) {
                setLongClickable(true);
            }
            getListenerInfo().mOnLongClickListener = l;
        }
     
    

    还有一个疑问,当控件处于disable时,还会消费事件吗?

    答案还是从源码里找,就在上面那个源码当中的 这几句。

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

    其实光看注释就够了。。

    A disabled view that is clickable still consumes the touch events, it just doesn't respond to them.

    disable情况下, 发生 MotionEvent 事件时,看上去没有反应,但是是否消费了事件,依旧看 CLICKABLELONG_CLICKABLE的值。

    以上只是源码肤浅的分析,在实际当中,还有几点需要了解。

    1. 正常情况下,对于一个事件序列,都是交给同一个view进行处理的,不能将同一个事件序列中的事件同时交给两个view进行处理,当然,我们是可以通过代码强制的来改变这一点。
    2. 如果一个View消费了ACTION_DOWN事件(onTouchEvent方法返回了true),那么(同一个事件序列中)剩下的事件也会交给它处理,并不会再调用onInterceptTouchEvent方法来询问是否拦截了。
    3. 如果一个View没有消费ACTION_DOWN事件,那么(同一个事件序列中)剩下的事件不会再交给它处理,剩下的事件会交给它的父元素处理,父元素的onTouchEvent方法会被调用。而如果该View虽然消费了ACTION_DOWN事件,但是并没有消费除ACTION_DOWN以外的其他事件,那么这些事件将会消失,并不会再调用它的父元素的onTouchEvent进行处理。
    4. 所以,ACITON_DOWN事件是事件序列的起源,也是非常重要的一个事件。
    5. 事件分发,总是从外到内的,就是先传递给父元素,然后父元素再传递给它包裹的子元素。但是其实子元素也可以使用requestDisallowTouchEvent方法来干涉父元素中的传递过程。(不过这点目前没弄清楚具体的原理和使用方法,所以就不多展开了,免得误导别人)。

    那么自此,关于View的事件分发机制就分析完了。。而关于源码部分,其实分析的也不够彻底和深刻。一来自己水平的确有限,二来了解大概的脉络,就能处理大部分问题了。

    具体关于事件分发的测试代码,可以参见https://github.com/yaowen369/BlogDemo/tree/master/Android/AndroidBlogDemo/app/src/main/java/com/yaoxiaowen/android_blog_demo/dispatch_event/dispatch_test 的代码,尝试修改各个地方的返回值,然后结合理论分析,来预测实际输出的后果。


    作者: www.yaoxiaowen.com

    github: https://github.com/yaowen369

    欢迎对于本人的博客内容批评指点,如果问题,可评论或邮件(yaowen369@gmail.com)联系

    <p >
    		 欢迎转载,转载请注明出处.谢谢
    </p>
    
    
    <script type="text/javascript">
     function    Curgo()   
     {   
         window.open(window.location.href);
     }   
    </script>
    
  • 相关阅读:
    解决url传递过程中加号变空格的问题<转>
    windows下CEF3的关闭流程《转》
    mfc封装cef浏览器 关闭整个窗口程序得时候又重启mfc 应用的程序
    雷神免费资源
    LCA的 RMQ解法模版
    最新的js焦点图库
    whereis+whatis+man
    Anroid 手机助手 详细解析 概述(二)
    <c:forEach varStatus="status">中 varStatus的属性简介
    JBoss 系列四十九:JBoss 7/WildFly 中端口使用列表
  • 原文地址:https://www.cnblogs.com/yaoxiaowen/p/6925702.html
Copyright © 2020-2023  润新知