Android 事件分发机制

2019-04-16  本文已影响0人  wind_sky

一. 介绍

在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 都是没有事件拦截的,这是因为:

在一个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

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

四. 滑动事件冲突

  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;
     }
 }
上一篇 下一篇

猜你喜欢

热点阅读