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

Android 事件分发机制

作者: wind_sky | 来源:发表于2019-04-16 16:34 被阅读0次

    一. 介绍

    在Android 开发中,一个页面可以有很多View,这些View 可能会重叠,所以当我们点击某一个区域可以有多个View 可以响应时,这个事件该由谁来处理,这就涉及到事件的分发机制。

    二. View 的结构

    View 是一个树形结构,比如下图


    image.png

    这个页面的结构如下面的树形结构所示


    image.png

    可以看到在结构图中还有PhoneWindow 和DecorView ,这两个并没有在XML 布局文件中声明,那么他们是什么呢。

    Window:

    Window类是一个抽象类,它定义了顶级窗体样式和行为。一个Window实例应作为顶级View添加到WindowManager中。它提供标准的UI规则,例如背景、标题、默认关键过程等。每一个Activity 组件都有一个关联的Window对象,用来描述一个应用程序窗口。(window 的知识点也有很多,可以展开成一章,这里先简单介绍)

    PhoneWindow:

    上面提到Window 是一个抽象类,而PhoneWindow 则是Window 的唯一实现类。

    DecorView:

    DecorView 是PhoneWindow 的一个内部类,继承了FrameLayout,是一个Activity 的顶级View,内部会包含一个竖直方向的LinearLayout,这个LinearLayout有上下两部分,分为titlebar和contentParent两个子元素,contentParent的id是content,而我们自定义的Activity的布局就是contentParent里面的一个子元素。View层的所有事件都要先经过DecorView后才传递给我们的View。

    三. 事件的分发流程

    前面我们了解到了我们的View是树形结构的,基于这样的结构,我们的事件可以进行有序的分发。事件收集之后最先传递给 Activity, 然后依次向下传递,大致如下:

    Activity -> PhoneWindow -> DecorView -> ViewGroup -> ... -> View
    

    这样的事件分发机制逻辑非常清晰,如果最后分发到View,如果没有任何View消费掉事件,那么这个事件会按照反方向回传,最终传回给Activity,如果最后 Activity 也没有处理,本次事件才会被抛弃:

    Activity <- PhoneWindow <- DecorView <- ViewGroup <- ... <- View
    

    在事件分发过程中,有三个方法非常重要

    类型 相关方法 Activity ViewGroup View
    事件分发 dispatchTouchEvent
    事件拦截 onInterceptTouchEvent X X
    事件消费 onTouchEvent

    √ 代表有这个方法,X 代表没这个方法。

    这个三个方法均有一个 boolean(布尔) 类型的返回值,通过返回 true 和 false 来控制事件传递的流程。

    从上表可以看到 Activity 和 View 都是没有事件拦截的,这是因为:

    • Activity 作为原始的事件分发者,如果 Activity 拦截了事件会导致整个屏幕都无法响应事件,这肯定不是我们想要的效果。

    • View最为事件传递的最末端,要么消费掉事件,要么不处理进行回传,根本没必要进行事件拦截。

    在一个ViewGroup 中,事件的分发可以用下面的伪代码来表示

    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean result = false;             // 默认状态为没有消费过
    
        if (!onInterceptTouchEvent(ev)) {   // 如果没有拦截交给子View
            result = child.dispatchTouchEvent(ev);
        }
    
        if (!result) {                      // 如果事件没有被消费,询问自身onTouchEvent
            result = onTouchEvent(ev);
        }
    
        return result;
    }
    

    在View 中,没有onInterceptTouchEvent 方法,在dispatchTouchEvent 中调用onTouchEvent 来决定是否消费事件。当一个View需要处理事件时,如果它设置了OnTouchListener,那么onTouch方法会被调用,如果onTouch返回false,则当前View的onTouchEvent方法会被调用,返回true则不会被调用,同时,在onTouchEvent方法中如果设置了OnClickListener,那么他的onClick方法会被调用。由此可见处理事件时的优先级关系:onTouchListener > onTouchEvent > onClickListener

    关于事件传递的机制,这里给出一些结论:

    • 一个事件系列以down事件开始,中间包含数量不定的move事件,最终以up事件结束。

    • 正常情况下,一个事件序列只能由一个View拦截并消耗。

    • 某个View拦截了事件后,该事件序列只能由它去处理,并且它的onInterceptTouchEvent不会再被调用。

    • 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvnet返回false),那么同一事件序列中的其他事件都不会交给他处理,并且事件将重新交由他的父元素去处理,即父元素的onTouchEvent被调用。

    • 如果当前正在处理的事件被上层 View 拦截,会收到一个 ACTION_CANCEL,后续事件不会再传递过来。

    • 如果View不消耗 ACTION_DOWN 以外的其他事件,那么这个事件将会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终消失的点击事件会传递给Activity去处理。

    • ViewGroup默认不拦截任何事件。

    • View没有onInterceptTouchEvent方法,一旦事件传递给它,它的onTouchEvent方法会被调用。

    • View的onTouchEvent默认消耗事件,除非他是不可点击的(clickable和longClickable同时为false)。

    • View的enable不影响onTouchEvent的默认返回值。

    • onClick会发生的前提是当前View是可点击的,并且收到了down和up事件。

    • 事件传递过程总是由外向内的,即事件总是先传递给父元素,然后由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的分发过程,但是ACTION_DOWN事件除外。

    四. 滑动事件冲突

    1. 常见场景
    • 外部滑动和内部滑动方向不一致;

    • 外部滑动方向和内部滑动方向一致;

    上面两种情况的嵌套。

    1. 滑动冲突的处理规则

    对于场景一,处理的规则是:当用户左右(上下)滑动时,需要让外部的View拦截点击事件,当用户上下(左右)滑动的时候,需要让内部的View拦截点击事件。根据滑动的方向判断谁来拦截事件。

    对于场景二,由于滑动方向一致,这时候只能在业务上找到突破点,根据业务需求,规定什么时候让外部View拦截事件,什么时候由内部View拦截事件。

    场景三的情况相对比较复杂,同样根据需求在业务上找到突破点。

    1. 解决方式

    1)外部拦截,指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,否则就不拦截

     public boolean onInterceptTouchEvent (MotionEvent event){
         boolean intercepted = false;
         int x = (int) event.getX();
         int y = (int) event.getY();
         switch (event.getAction()) {
         case MotionEvent.ACTION_DOWN:
                 intercepted = false;
                 break;
         case MotionEvent.ACTION_MOVE:
                 if (父容器拦截条件) {
                     intercepted = true;
                 } else {
                     intercepted = flase;
                 }
                 break;
         }
         case MotionEvent.ACTION_UP:
                 intercepted = false;
                 break;
         default : break;
     }
     mLastXIntercept = x;
     mLastYIntercept = y;
     return intercepted;
    

    2)内部拦截,指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗,否则就交由父容器进行处理。这种方法与Android事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作

     public boolean dispatchTouchEvent ( MotionEvent event ) {
         int x = (int) event.getX();
         int y = (int) event.getY();
    
         switch (event.getAction) {
         case MotionEvent.ACTION_DOWN:
                 parent.requestDisallowInterceptTouchEvent(true);
                 break;
         case MotionEvent.ACTION_MOVE:
                 int deltaX = x - mLastX;
                 int deltaY = y - mLastY;
                 if (父容器拦截条件) {
                     parent.requestDisallowInterceptTouchEvent(false);
                 }
                 break;
         case MotionEvent.ACTION_UP:
                 break;
         default : break;        
         }
    
         mLastX = x;
         mLastY = y;
         return super.dispatchTouchEvent(event);
     }
    

    除了子元素需要做处理外,父元素也要默认拦截除了ACTION_DOWN 以外的其他事件,这样当子元素调用parent.requestDisallowInterceptTouchEvent(false)方法时,父元素才能继续拦截所需的事件。因此,父元素要做以下修改:

     public boolean onInterceptTouchEvent (MotionEvent event) {
         int action = event.getAction();
         if(action == MotionEvent.ACTION_DOWN) {
             return false;
         } else {
             return true;
         }
     }
    

    相关文章

      网友评论

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

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