(原创)Android事件分发机制详解

之前有写过一篇简单的博客

解决滑动冲突问题

(原创)巧妙解决ViewPager和ScrollView冲突_Android_xiong_st的博客-CSDN博客

今天对冲突背后的事件分发机制,做一个详细的介绍

下面开始!


Android的事件分发机制相关的类:

public boolean dispatchTouchEvent(event):用于进行点击事件的分发

public boolean onInterceptTouchEvent(event):用于进行点击事件的拦截

public boolean onTouchEvent(event):用于处理点击事件

三个函数的参数均为even,即上面所说的3种类型的输入事件,返回值均为boolean 类型
上面的三种方法的调用关系大致可以用下面的伪代码来描述

    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean consume = false;//事件是否被消费
        if (onInterceptTouchEvent(ev)){//调用onInterceptTouchEvent判断是否拦截事件
            consume = onTouchEvent(ev);//如果拦截则调用自身的onTouchEvent方法
        }else{
            consume = child.dispatchTouchEvent(ev);//不拦截调用子View的dispatchTouchEvent方法
        }
        return consume;//返回值表示事件是否被消费,true事件终止,false调用父View的onTouchEvent方法
    }

一、事件分发的执行顺序流程图(默认情况下,不考虑事件拦截和处理)

(原创)Android事件分发机制详解_第1张图片


如上图,事件的分发顺序由1开始依次分发到8。

事件分发的规则,隧道式下发。

在ViewGroup中如果有N个子控件可以处理该事件

(比如帧布局,很多子控件叠层在一起,那么这些叠层在一起的子控件都可以处理该布局上的触摸事件,

除非触摸事件发生在子控件的范围外),

那么最开始会分发给最后一个子控件,然后依次往前分发,如上图。

如果其中一个子控件又是一个ViewGroup,会先分发给该ViewGroup下的子控件,

按同样的规则依次分发,子控件处理完成后,再继续分发给同级的下一个View,

如上图(第二个View下的子控件全部分发完成后,才分发给第一个View)。 

我们可以把事件分发的过程理解成一个二叉树的遍历过程。

dispatchTouchEvent()返回值详解:

dispatchTouchEvent()直接返回true,后续事件(ACTION_MOVE、ACTION_UP)会再传递,

如果直接返回false,dispatchTouchEvent()就接收不到ACTION_UP、ACTION_MOVE。

1、直接返回true:表示终止整个事件链的传递,后续事件(ACTION_MOVE、ACTION_UP)会再传递。

比如上图中第二个View(红色边框表示的ViewGroup),如果该ViewGroup对应的dispatchTouchEvent()方法返回true,

则事件不在往后分发,后面的控件也不会对事件进行处理,

第二个View的dispatchTouchEvent()成为此次事件分发链中最后被执行的一个方法(在这之前已经分发和处理了事件的控件不会受影响)。

第二个View的dispatchTouchEvent()也会收到后续事件(ACTION_MOVE、ACTION_UP)

2、直接返回false:表示停止当前链路的事件下发,但是不停止整个事件链的传递。也接受不到后续事件了。

比如按下上图中的第二个View(红色边框表示的ViewGroup),如果该ViewGroup对应的dispatchTouchEvent()方法返回false,

则事件在该事件链上停止下发,即5、6、7对应的控件(黄色边框表示的子View),不再接收到分发的事件,

也不能对事件进行处理。但是8对应的“第一个View”(绿色边框表示的子View),仍然可以接受到父控件分发下来的action_down事件并且进行处理,

因为8对应的“第一个View”不在“第二个View”所处的事件链中,他们是同级关系,因此不受“第二个View”的停止分发影响。

但是8对应的“第一个View”再也收不到后续事件ACTION_UP、ACTION_MOVE了。所以其实受到的影响就是只接受到了一次的down事件。

注意:以上两种直接返回true和false的情况,View自身也不会再调用拦截和处理的方法了

因为拦截和处理都是在super.dispatchTouchEvent()里面做的,做完再返回结果

相当于View自身也没有事件拦截和处理的资格了

3、(默认的情况)返回super.dispatchTouchEvent():继续分发,表示接受这个事件,此时又分三种情况:

    1)当前View是一个ViewGroup,那么继续执行该ViewGroup的onInterceptTouchEvent()拦截方法进行拦截,根据拦截方法的返回值来决定后续动作。

    2)当前View不是一个ViewGroup(比如TextView,不存在子控件),那么直接调用当前控件的onTouchEvent()方法来处理事件,并且根据处理事件方法的返回值来决定后续动作。

    3)当前控件是一个Activity,那么如果当前Activity没有子控件(当触摸事件发生在最外层布局之外时,只有activity能处理该事    件,即activity不存在子控件)时,

直接调用本身的onTouchEvent()方法进行处理,否则分发给下一个子控件(即activity包含的    最外层布局对象)。

二、事件拦截流程图

事件拦截只会发生在ViewGroup中,Activity和View没有事件拦截这个方法。


(原创)Android事件分发机制详解_第2张图片

事件拦截方法只存在于容器布局中,即ViewGroup。在ViewGroup中,事件拦截是在事件分发方法后被调用(前提是事件分发方法返回super)。

onInterceptTouchEvent()返回值详解:

1)返回true:表示拦截该事件,事件不在往下分发,

该ViewGroup的所有子控件都无法接收到事件(即子控件的dispatchTouchEvent()方法不会被调用),并且直接调用ViewGroup本身的onTouchEvent();

2)返回false:表示不对事件进行拦截,继续往下分发,

再根据子控件的事件处理结果决定是否调用自己的onTouchEvent()方法(如果子控件终止了分发或者消费了事件,都将导致ViewGroup的onTouchEvent()方法无法被调用,如果子控件没有消费事件,则调用自身的onTouchEvent()方法)。

3)返回super.onInterceptTouchEvent():如果继承的是ViewGroup,效果和返回false一样,我们可以进去看到源码,ViewGroup.onInterceptTouchEvent()返回的就是false。

当然,一些控件会做修改,比如我们继承ViewPager,ViewPager的onInterceptTouchEvent()就在自己内部做了处理,这时候就需要我们自己根据实际业务来决定onInterceptTouchEvent()的返回值了

三、事件处理顺序流程图
 

(原创)Android事件分发机制详解_第3张图片

事件处理的规则,默认情况下(假设不存在消费的情况),子View先依次处理完毕,父控件再处理事件的一个过程。如上图。

onTouchEvent()返回值详解:

1)返回true:表示消费事件,即表示这个事件交由自己来处理,还未执行事件处理方法的控件将不再调用事件处理方法。事件链到此结束。

2)返回false:表示透传事件,即把这个事件交由其他控件来处理,这样事件链会继续传递,直到有一个控件消费该事件或交由最上层控件来处理(即activity)。

3)返回super.onTouchEvent():根据这个View有没有设置监听来决定返回值,如果设置了一些监听(比如click监听),并且符合了该监听的条件则返回true,否则返回false。

四、最后,是事件分发解决方式

假设有父布局A,里面嵌套一个子布局B,二者发生了滑动冲突,解决方案有两个:

1、外部解决,从自定义父布局开始解决

在父布局的onInterceptTouchEvent  拦截方法里,判断该子布局滑动的时候,把这个拦截方法的返回值返回false,意为不拦截,比如
 

(原创)Android事件分发机制详解_第4张图片

注意:这里只是一个示例,可以看到没有返回true的情况,因为这里是拿一个Scrollview来示例。Scrollview在自己的onInterceptTouchEvent()里面做了处理,如果我们继承的是ViewGroup,就要根据实际业务自己决定什么情况下返回true了。

2、内部解决,从自定义子布局开始解决

在子布局的dispatchTouchEvent拦截方法里,判断该子布局滑动的时候,请求父布局不要拦截自己,比如

(原创)Android事件分发机制详解_第5张图片

另外在父布局的拦截方法里还要处理以下ACTION_DOWN事件

在ACTION_DOWN时返回false

确保子View拿到action_down事件,然后调用requestDisallowInterceptTouchEvent(true)

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

内部拦截的处理方式我写了个小demo:HorizontalScrollView嵌套一个ListView

xml如下:



    
        

            
        
    

两个自定义控件如下:

public class MyHorizontalScrollView extends HorizontalScrollView {


    public MyHorizontalScrollView(Context context) {
        super(context);
    }

    public MyHorizontalScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyHorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.d("MyHorizontalScrollView", "父dispatchTouchEvent:"+ev.getAction());
//        return false;
        return super.dispatchTouchEvent(ev);
    }



    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.d("MyHorizontalScrollView", "父onInterceptTouchEvent:"+ev.getAction());
        if(ev.getAction() == MotionEvent.ACTION_DOWN) {
            return false;
        }
        return true;
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d("MyHorizontalScrollView", "父onTouchEvent:"+event.getAction());
        return super.onTouchEvent(event);
    }
}
public class MyListView extends ListView {


    public MyListView(Context context) {
        super(context);
    }

    public MyListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyListView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.d("MyHorizontalScrollView", "子dispatchTouchEvent:" + ev.getAction());
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);

                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                //横向偏移大,判定为横向滑动,交给父容器处理
                if (Math.abs(deltaX) > Math.abs(deltaY)) {
                    //父容器恢复事件拦截,事件交给父容器处理
                    Log.d("MyHorizontalScrollView", "交给父布局");
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        mLastX = x;
        mLastY = y;
//        return false;
        boolean b = super.dispatchTouchEvent(ev);
        return b;
    }

    //分别记录上次滑动的坐标
    private int mLastX = 0;
    private int mLastY = 0;
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d("MyHorizontalScrollView", "子onTouchEvent:" + event.getAction());

        return super.onTouchEvent(event);
    }
}

写在最后:

在开发的过程中,除了要解决问题外

还要多关注问题背后的事情

比如问题产生的根本原因

系统的一些机制等等

只有这样,才能进步啊~

加油!

最后贴一些参考博客

Android事件分发机制_你的坚定的博客-CSDN博客_android 事件分发

你可能感兴趣的:(Android开发,android,java,apache)