Android 事件分发机制-ViewGroup篇

作者: 橘子周二 | 来源:发表于2017-04-13 17:52 被阅读81次

    触摸事件分发机制学习 -ViewGroup篇

    在上一篇事件分发机制的学习-View篇基本算是逐行解析了单独view的事件传递和处理。
    本篇在此基础上来分析ViewGroup对事件的分发和拦截。

    有必要先简单回顾下单独View的事件处理机制:
    1. dipatchTouchEvent

    2. onTouch
    ↓false
    3. onTouchEvent


    选取一个ViewGroup来做小白鼠-LinearLayout

    • 自定义View继承LinearLayout,重写事件处理的三个方法。
      三个方法分别是,触摸事件调度、触摸事件、拦截触摸事件
        @Override
           public boolean dispatchTouchEvent(MotionEvent ev) {
               switch (ev.getAction()){
                   case MotionEvent.ACTION_DOWN:
                       L.println("TLineraLayout -dispatchTouchEvent-  MotionEvent.ACTION_DOWN");
                       break;
       
                   case MotionEvent.ACTION_MOVE:
                       L.println("TLineraLayout - dispatchTouchEvent- MotionEvent.ACTION_MOVE");
                       break;
       
                   case MotionEvent.ACTION_UP:
                       L.println("TLineraLayout - dispatchTouchEvent- MotionEvent.ACTION_UP");
                       break;
                   default:
       
                       break;
               }
       
               return super.dispatchTouchEvent(ev);
           }
       
           @Override
           public boolean onTouchEvent(MotionEvent event) {
               switch (event.getAction()){
                   case MotionEvent.ACTION_DOWN:
                       L.println("TLineraLayout -onTouchEvent  MotionEvent.ACTION_DOWN");
                       break;
       
                   case MotionEvent.ACTION_MOVE:
                       L.println("TLineraLayout -onTouchEvent  MotionEvent.ACTION_MOVE");
                       break;
       
                   case MotionEvent.ACTION_UP:
                       L.println("TLineraLayout -onTouchEvent  MotionEvent.ACTION_UP");
                       break;
                   default:
       
                       break;
               }
               return super.onTouchEvent(event);
           }
       
           @Override
           public boolean onInterceptTouchEvent(MotionEvent ev) {
               switch (ev.getAction()){
                   case MotionEvent.ACTION_DOWN:
                       L.println("TLineraLayout -onInterceptTouchEvent  MotionEvent.ACTION_DOWN");
                       return true;
       
                   case MotionEvent.ACTION_MOVE:
                       L.println("TLineraLayout -onInterceptTouchEvent  MotionEvent.ACTION_MOVE");
                       break;
       
                   case MotionEvent.ACTION_UP:
                       L.println("TLineraLayout -onInterceptTouchEvent  MotionEvent.ACTION_UP");
                       break;
                   default:
       
                       break;
               }
               return super.onInterceptTouchEvent(ev);
           }
       
    

    看看日志打印:

        D/L-: TLineraLayout -dispatchTouchEvent-  MotionEvent.ACTION_DOWN
        D/L-: TLineraLayout -onInterceptTouchEvent  MotionEvent.ACTION_DOWN
        D/L-: TButton - dispatchTouchEvent = ACTON_DOWN
        D/L-: ACtivity - onTouch = ACTON_DOWN 
        D/L-: TButton - onTouchEvent = ACTON_DOWN
    
        D/L-: TLineraLayout - dispatchTouchEvent- MotionEvent.ACTION_MOVE
        D/L-: TLineraLayout -onInterceptTouchEvent  MotionEvent.ACTION_MOVE
        D/L-: TButton - dispatchTouchEvent = ACTION_MOVE
        D/L-: ACtivity - onTouch = ACTION_MOVE
        D/L-: TButton - onTouchEvent = ACTION_MOVE
    
        D/L-: TLineraLayout - dispatchTouchEvent- MotionEvent.ACTION_UP
        D/L-: TLineraLayout -onInterceptTouchEvent  MotionEvent.ACTION_UP
        D/L-: TButton - dispatchTouchEvent = ACTION_UP
        D/L-: ACtivity - onTouch = ACTION_UP
        D/L-: TButton - onTouchEvent = ACTION_UP
    

    上面日志说明从ViewGroup → View的事件调用顺序依次是
    ViewGroup[dispatchTouchEvent → onInterceptTouchEvent]

    View[dispatchTouchEvent → onTouch → onTouchEvent]
    暂时直观理解:触摸事件是从ViewGroup从上到下传递到子View的。
    这里有一个疑问了,onTouchEvent事件怎么没有触发呢?

    下面就杀进源码看个究竟。

    这里我们只需要着重关心dispatchTouchEvent即可,因为所有暗箱操作都在他这里

    dispatchTouchEvent方法`(源码版本:sdk\sources\android23)

    >这里代码比较长,只截取重要代码片断,分段来解析
    >按照我自己的理解,重点围绕ViewGroup如何拦截的事件,如何分发事件这两点即可。
    
    • 1 .拦截判断
      intercepted初始化,只有当事件为ACTION_DOWN,
      或者 mFirstTouchTarget != null
      mFirstTouchTarget总是在Down事件开始被清空,再处理事件后被赋值为next(接受后续事件的TouchTarget)
      mFirstTouchTarget != null:不是ACTION_DOWN事件,可能是MOVE,UP等事件传递进来(也就是二次接收事件了)。
      mFirstTouchTarget == null:ViewGroup压根没向下传递事件给子View,直接交给了起父类View类来处理了。
      (利用以上两点结论,有助于更快理解后面用mFirstTouchTarget做的一些if判断。)
        public boolean dispatchTouchEvent(MotionEvent ev) {
            ...省略
    
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                // ==0 拦截false  !=0 不拦截true
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                //允许拦截
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action);
                } else {
                    intercepted = false;
                }
            } else {
                intercepted = true;
            }
        }
    
    }
                     
    

    小结
    onInterceptTouchEvent方法在父类默认返回false 子类可以覆盖重写。
    上面有一个flag非常关键 FLAG_DISALLOW_INTERCEPT “不拦截”标记。
    disallowIntercept = false 时调用onInterceptTouchEvent方法,返回父类是否拦截。
    mGroupFlags 添加 不拦截的tag是在 requestDisallowInterceptTouchEvent方法中,传入true则添加不许拦截的tag。

    • 2.派发事件
                ···省略
        final boolean canceled = resetCancelNextUpFlag(this)
                || actionMasked == MotionEvent.ACTION_CANCEL;   
              if(!canceled &&!intercepted)
    
        {
            //按下、多指、触点保持
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
    
                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    //记录事件触发坐标
                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
    
                    //确认子View显示或者包涵动画||触发坐标没有移除view的范围
                    //否则跳过此次事件处理
                    if (!canViewReceivePointerEvents(child)
                            || !isTransformedTouchPointInView(x, y, child, null)) {
                        ev.setTargetAccessibilityFocus(false);
                        continue;
                    }
    
                    //子View消费
                    //分发事件给子类,进入dispatchTransformedTouchEvent得知 child!=null 时交给子View处理
                    //如果子类选择消费此次事件,则结束此次事件传递,不再询问其他子View。
                    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                        //为mFirstTarget重新赋值
                        newTouchTarget = addTouchTarget(child, idBitsToAssign);
                        alreadyDispatchedToNewTouchTarget = true;
                        break;
                    }
                }
            }
    
            //子View未消费,交给父类自己处理。ViewGroup的父类本身也是一个View
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {//下面为了保险,如果已分发的事件traget!=newTouchTraget则再分发一次
                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;
                        }
                    }
                }
    
                return handled;
            }
                
            ···省略
    

    源码分析结束。

    总结

    • 1.ViewGroup如何拦事件自己处理
      重写onInterceptTouchEvent(ev)方法

    • 2.ViewGroup如何拦截子View事件,不向下分发
      重写onInterceptTouchEvent(ev)方法并返回true
      或者直接调用requestDisallowInterceptTouchEvent(false)

    • 3.ViewGroup如何控制事件分发
      在不拦截时,如果子View不为Null,则交给子view去处理
      如果子View没有选择消费,或者子View为空则向上交给父类View来处理。也就是把自己当成一个View来处理了。

    • 3.子View事件被父类拦截后如何保证依然能获取到事件
      调用getParent().requestDisallowInterceptTouchEvent(true),保证自己一定能获取到事件
      如果还拿不到那就两个getParent().getParent()

    实用场景(来自鸿神 -实力Carry):
    比如你需要写一个类似slidingmenu的左侧隐藏menu,主Activity上有个Button、ListView或者任何可以响应点击的View。
    你在当前View上死命的滑动,菜单栏也出不来;因为MOVE事件被子View处理了~
    你需要这么做:在ViewGroup的dispatchTouchEvent中判断用户是不是想显示菜单。
    如果是,则在onInterceptTouchEvent(ev)拦截子View的事件;自己进行处理。
    这样自己的onTouchEvent就可以顺利展现出菜单栏了~~

        简单来说就是在你需要的时候,拦截事件并利用ViewGroup的逻辑:child= null时自行处理的套路,来保证整个事件的随意使用。
    

    有关事件分发更深入的认识可以参考博客事件分发流程-张兴业

    相关文章

      网友评论

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

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