Android的View事件分发机制

2020-02-26  本文已影响0人  Vinson武

了解Activity的构成

一个Activity包含了一个Window对象,这个对象是由PhoneWindow来实现的。PhoneWindow将DecorView作为整个应用窗口的根View,而这个DecorView又将屏幕划分为两个区域:一个是TitleView,另一个是ContentView,而我们平时所写的就是展示在ContentView中的。

触摸事件的类型

触摸事件对应的是MotionEvent类,事件的类型主要有如下三种:

View事件分发本质就是对MotionEvent事件分发的过程。即当一个MotionEvent发生后,系统将这个点击事件传递到一个具体的View上。

事件分发流程

image.png

事件分发过程由三个方法共同完成:

  1. dispatchTouchEvent:如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent()方法的影响,表示是否消耗当前事件。方法返回值为true表示事件被当前视图消费掉;返回为super.dispatchTouchEvent表示继续分发该事件,返回为false表示交给父类的onTouchEvent处理。
  2. onInterceptTouchEvent:在dispatchTouchEvent()方法内部调用,用于判断是否拦截某个事件,返回true表示拦截当前事件并交由自身的onTouchEvent方法进行消费。如果当前view拦截了某个事件,那么在同一个事件序列中,此方法不会再次被调用。
    返回false表示不拦截,需要继续传递给子视图。如果return super.onInterceptTouchEvent(ev), 事件拦截分两种情况:
  1. onTouchEvent():同样在dispatchTouchEvent()方法内部调用,用来处理事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前view无法再次接收到事件。
    方法返回值为true表示当前视图可以处理对应的事件;返回值为false表示当前视图不处理这个事件,它会被传递给父视图的onTouchEvent方法进行处理。如果return super.onTouchEvent(ev),事件处理分为两种情况:

三个方法的关系用伪代码表示如下:

public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean consume = false;
    if (onInterceptTouchEvent(ev)) {
        consume = onTouchEvent(ev);
    } else {
        coonsume = child.dispatchTouchEvent(ev);
    }
    
    return consume;
}

点击事件的传递规则:对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,这是它的dispatchTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,这时如果它的mOnTouchListener被设置,则onTouch会被调用,如果onTouch返回false,那么onTouchEvent会被调用,如果onTouch返回true,那么onTouchEvent不会被调用。在onTouchEvent中,如果设置了mOnCLickListener,则onClick会被调用。只要View的CLICKABLE和LONG_CLICKABLE有一个为true,onTouchEvent()就会返回true消耗这个事件。如果这个ViewGroup的onInterceptTouchEvent方法返回false就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent方法就会被调用,如此反复直到事件被最终处理。

重要结论

源码分析

  1. 当一个点击事件发生时,事件最先传递给当前Activity,由Activity的dispatchTouchEvent进行事件派发,具体是由Activity内部的Window(具体实现是PhoneWindow)来完成。Window会将事件传递给DecorView(一般是当前界面的底层容器,即setContentView所设置的View的父容器)

Activity的dispatchTouchEvent


image.png

PhoneWindow的dispatchTouchEvent


image.png
  1. ViewGroup的dispatchTouchEvent处理逻辑


    image.png

从上面的代码可以看出,在两种情况下会判断是否要拦截当前事件:事件类型为ACTION_DOWN(点击事件序列的第一个事件)和mFirstTouchTarget!=null,(什么时候非空呢?当事件由viewGroup的子元素成功处理时,mFirstTouchTarget会被赋值,即viewGroup不拦截并交给子元素处理时非空,反之拦截则为空,那么后面的同序列事件都不会调用到onInterceptTouchEvent,而是直接默认拦截),说明onInterceptTouchEvent不是每次都会调用,如果想提前处理所有点击事件,要选择dispatchTouchEvent。

FLAG_DISALLOW_INTERCEPT标记位,可通过子view的requestDisallowInterceptTouchEvent方法来设置,后面滑动冲突会说到

  1. 当ViewGroup不拦截事件的情况下,事件hi向下分发由它的子view进行处理,遍历ViewGroup所有子元素,然后判断子元素是否能否接收到点击事件。


    image.png
    image.png

dispatchTransformedTouchEvent这个方法实际调用的是子元素的dispatchTouchEvent方法,这样事件就交给子元素处理了,在
addTouchTarget()方法中会前面说到的mFirstTouchTarget赋值

  1. 如果遍历所有子元素后事件都没有被处理(没有子元素,或者子元素处理了但dispatchTouchEvent返回false(一般是因为onTouchEvent返回false)),这种情况下ViewGroup会自己处理点击事件。由上面代码可知这里会转到View的dispatchTouchEvent方法,即交给View来处理
image.png

dispatchTransformedTouchEvent

![image] image.png
  1. View对点击事件的处理比较简单,它没有子元素向下传递只能自己处理

先看dispatchTouchEvent方法


image.png

会先判断有没有设置OnTouchListener,如果OnTouchListener的onTouch返回true,那么onTouchEvent不会被调用,可见onTouch的优先级比onTouchEvent高

接下来看onTouchEvent


image.png

可以看到Disabled状态下的View同样会消耗点击事件

再看onTouchEvent中对点击事件的具体处理

image.png

只要CLICKABLE或者LONG_CLICKABLE一个为true,那么都会消耗 这个事件,即onTouchEvent方法返回true,而不管是否是DISABLE。

当ACTION_UP 事件发生时,会触发performClick方法,如果View设置来OnClickListener,那么performClick方法内部会调用它的onClick方法。
由此也可以知道几个方法调用的优先级onTouch > onTouchEvent > onClick

问题

  1. ACTION_CANCEL什么时候触发,触摸button然后滑动到外部抬起会触发点击事件吗,再滑动回去抬起会么?
  1. 点击事件被拦截,但是想传到下面的View,如何操作?

重写子类的requestDisallowInterceptTouchEvent()方法返回true就不会执行父类的onInterceptTouchEvent(),即可将点击事件传到下面的View。

  1. ViewGroup分发时会遍历ViewGroup的子元素,有多个子元素如何判断那个子元素接收点击事件?

在ViewGroup的分发方法中有一段逻辑:首先遍历ViewGroup的所有子元素,然后判断子元素是否能够接收到点击事件。是否能够接收由两点来衡量:子元素是否在播放动画和点击事件的坐标是否落在子元素的区域内。(没有在播放动画且坐标落在子元素区域内则分发给子元素)。遍历过程中如果某个子元素的dispatchTouchEvent返回true,则交给子元素处理并跳出循环。如果返回false则继续分发给下一个子元素(如果有的话)或自己处理(没有子元素或者子元素处理了,但是在dispatchTouchEvent返回了false)

滑动冲突

常见滑动冲突场景可分为以下3种:

  1. 外部滑动方向和内部滑动方向不一致(类似ViewPager和Fragment组合,每个页面又有ListView)
  2. 外部滑动方向和内部滑动方向一致(ScrollView和ListView组合)
  3. 以上两种情况的嵌套

滑动冲突的处理规则:

滑动冲突的解决方法

  1. 外部拦截法

指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,否则就不拦截。具体方法:需要重写父容器的onInterceptTouchEvent方法,在内部做出相应的拦截。

@Override
    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: {
            int deltaX = x - mLastXIntercept;
            int deltaY = y - mLastYIntercept;
            if (Math.abs(deltaX) > Math.abs(deltaY)) {  //左右滑动距离大于上下,则拦截
                intercepted = true;
            } else {
                intercepted = false;
            }
            break;
        }
        case MotionEvent.ACTION_UP: {
            intercepted = false; //不拦截
            break;
        }
        default:
            break;
        }

        mLastXIntercept = x;
        mLastYIntercept = y;

        return intercepted;
    }

上述代码,在onInterceptTouchEvent方法中,首先是ACTION_DOWN这个事件中,父容器必须返回false,即不拦截ACTION_DOWN事件,因为一旦父容器拦截了ACTION_DOWN,那么后续的事件都会直接交由父容器处理,这个时候事件就没法再传递给子元素了;其次ACTION_MOVE事件根据需求来决定;最好ACTION_UP事件,这里必须返回false,如果返回true会导致子元素无法接收到ACTION_UP事件,这个时候子元素中的onClick事件就无法触发。但父容器比较特殊,一旦拦截了任何一个事件,后续事件都会交由它来处理。

  1. 内部拦截法

指父容器不拦截任何事件,而将所有的事件都传递给子容器,如果子容器需要此事件就直接消耗,否则就交由父容器进行处理。具体方法:需要配合requestDisallowInterceptTouchEvent方法(影响标记位FLAG_DISALLOW_INTERCEPT)。
,需要重写子元素的dispatchTouchEvent方法以及父元素的InterceptTouchEvent

子元素重写dispatchTouchEvent方法,配合requestDisallowInterceptTouchEvent方法决定是否允许父元素拦截


image.png

父元素重新InterceptTouchEvent方法


image.png

容器需要拦截处理ACTION_DOWN之外的所有事件,这样当调用requestDisallowInterceptTouchEvent(false)时,父容器才能继续拦截所需的事件。因为ACTION_DOWN事件是不受FLAG_DISALLOW_INTERCEPT标记位控制的,所以一旦父容器拦截了ACTION_DOWN,那么所有事件都无法传递到子元素中了。

以上demo在move时会被父元素处理,其他给子元素处理。

上一篇 下一篇

猜你喜欢

热点阅读