美文网首页
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