Android View事件分发机制

MotionEvent事件初探

我们对屏幕的点击,滑动,抬起等一系的动作都是由一个一个MotionEvent对象组成的。根据不同动作,主要有以下三种事件类型:
1.ACTION_DOWN:手指刚接触屏幕,按下去的那一瞬间产生该事件
2.ACTION_MOVE:手指在屏幕上移动时候产生该事件
3.ACTION_UP:手指从屏幕上松开的瞬间产生该事件

从ACTION_DOWN开始到ACTION_UP结束我们称为一个事件序列

正常情况下,无论你手指在屏幕上有多么骚的操作,最终呈现在MotionEvent上来讲无外乎下面两种。
1.点击后抬起,也就是单击操作:ACTION_DOWN -> ACTION_UP
2.点击后再风骚的滑动一段距离,再抬起:ACTION_DOWN -> ACTION_MOVE -> ... -> ACTION_MOVE -> ACTION_UP

举个栗子:

1.xml布局控件结构:

2.自定义控件MyTextView继承TextView

public class MyTextView extends TextView {
    private String tag = "MyTextView";

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

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

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

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        String aa = null;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                aa = "ACTION_DOWN";
                break;
            case MotionEvent.ACTION_MOVE:
                aa = "ACTION_MOVE";
                break;
            case MotionEvent.ACTION_UP:
                aa = "ACTION_UP";
                break;
        }
        Log.e(tag, "MyTextView---onTouchEvent---MotionEvent:" + aa);
        return super.onTouchEvent(event);
    }
}

3.自定义控件MyLinearLayoutA和MyLinearLayoutB继承LinearLayout

public class MyLinearLayoutA extends LinearLayout {
    private String tag = "MyLinearLayoutA";

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

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

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

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        String aa = null;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                aa = "ACTION_DOWN";
                break;
            case MotionEvent.ACTION_MOVE:
                aa = "ACTION_MOVE";
                break;
            case MotionEvent.ACTION_UP:
                aa = "ACTION_UP";
                break;
        }
        Log.e(tag, "MyLinearLayoutA---onTouchEvent---MotionEvent:" + aa);
        return super.onTouchEvent(event);
    }
}

4.MainActivity

public class MainActivity extends Activity {

    private String tag = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
    }

    private void initView() {
        MyLinearLayoutA lla = findViewById(R.id.lla);
        MyLinearLayoutB llb = findViewById(R.id.llb);
        MyTextView bb1 = findViewById(R.id.b1);

        lla.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                String aa = null;
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        aa = "ACTION_DOWN";
                        break;
                    case MotionEvent.ACTION_MOVE:
                        aa = "ACTION_MOVE";
                        break;
                    case MotionEvent.ACTION_UP:
                        aa = "ACTION_UP";
                        break;
                }
                Log.e(tag, "MyLinearLayoutA--setOnTouchListener---" + aa);
                return false;
            }
        });

        lla.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.e(tag, "MyLinearLayoutA-setOnClickListener");
            }
        });

        llb.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                String aa = null;
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        aa = "ACTION_DOWN";
                        break;
                    case MotionEvent.ACTION_MOVE:
                        aa = "ACTION_MOVE";
                        break;
                    case MotionEvent.ACTION_UP:
                        aa = "ACTION_UP";
                        break;
                }
                Log.e(tag, "MyLinearLayoutB--setOnTouchListener---" + aa);
                return false;
            }
        });

        llb.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.e(tag, "MyLinearLayoutB-setOnClickListener");
            }
        });


        bb1.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                String aa = null;
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        aa = "ACTION_DOWN";
                        break;
                    case MotionEvent.ACTION_MOVE:
                        aa = "ACTION_MOVE";
                        break;
                    case MotionEvent.ACTION_UP:
                        aa = "ACTION_UP";
                        break;
                }
                Log.e(tag, "MyTextView--setOnTouchListener---" + aa);
                return false;
            }
        });

        bb1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.e(tag, "MyTextView-setOnClickListener");
            }
        });
    }
}

总结下代码:

1.xml布局文件,1个LinearLayout下包含1个自定义MyLinearLayoutA(lla);MyLinearLayoutA(lla)下包含2个自定义MyTextView(a1、a2)和1个自定义MyLinearLayoutB(llb);MyLinearLayoutB(llb)下包含2个自定义MyTextView(b1、b2)。

2.自定义MyTextView继承TextView,重写dispatchTouchEvent()、和onTouchEvent();

3.自定义MyLinearLayoutA、MyLinearLayoutB继承LinearLayout,重写dispatchTouchEvent()、onTouchEvent()和onInterceptTouchEvent()(这个方法只有ViewGroup有,View没有);

4.在MainActivity里初始化MyLinearLayoutA(lla)、MyLinearLayoutB(llb)和MyTextView(a1),设置这三个控件的onTouchEventListener和onClickListener监听;

当我们单击MyTextView(b1)时:

07-25 16:01:47.161 3895-3895/com.mytest E/MainActivity: MyTextView--setOnTouchListener---ACTION_DOWN
07-25 16:01:47.161 3895-3895/com.mytest E/MyTextView: MyTextView---onTouchEvent---MotionEvent:ACTION_DOWN---isTrue:true
07-25 16:01:47.259 3895-3895/com.mytest E/MainActivity: MyTextView--setOnTouchListener---ACTION_UP
07-25 16:01:47.259 3895-3895/com.mytest E/MyTextView: MyTextView---onTouchEvent---MotionEvent:ACTION_UP---isTrue:true
07-25 16:01:47.259 3895-3895/com.mytest E/MainActivity: MyTextView-setOnClickListener

当我们在MyTextView(b1)上滑动时:

07-25 16:02:25.454 3895-3895/com.mytest E/MainActivity: MyTextView--setOnTouchListener---ACTION_DOWN
07-25 16:02:25.455 3895-3895/com.mytest E/MyTextView: MyTextView---onTouchEvent---MotionEvent:ACTION_DOWN
07-25 16:02:25.596 3895-3895/com.mytest E/MainActivity: MyTextView--setOnTouchListener---ACTION_MOVE
07-25 16:02:25.596 3895-3895/com.mytest E/MyTextView: MyTextView---onTouchEvent---MotionEvent:ACTION_MOVE
07-25 16:02:25.612 3895-3895/com.mytest E/MainActivity: MyTextView--setOnTouchListener---ACTION_MOVE
07-25 16:02:25.612 3895-3895/com.mytest E/MyTextView: MyTextView---onTouchEvent---MotionEvent:ACTION_MOVE
07-25 16:02:25.617 3895-3895/com.mytest E/MainActivity: MyTextView--setOnTouchListener---ACTION_MOVE
07-25 16:02:25.618 3895-3895/com.mytest E/MyTextView: MyTextView---onTouchEvent---MotionEvent:ACTION_MOVE
07-25 16:02:25.619 3895-3895/com.mytest E/MainActivity: MyTextView--setOnTouchListener---ACTION_UP
07-25 16:02:25.619 3895-3895/com.mytest E/MyTextView: MyTextView---onTouchEvent---MotionEvent:ACTION_UP
07-25 16:02:25.620 3895-3895/com.mytest E/MainActivity: MyTextView-setOnClickListener

细心的同学一定发现了我们常用的按钮的onclick事件都是在ACTION_UP以后才被调用的这和View的事件分发机制是不是有某种不可告人的关系呢?!

上面代码我们给TextView设置了OnTouchListener并重写了onTouch方法,方法返回值默认为false。如果这里我们返回true,那么你会发现onclick方法不执行了(log就不贴了)!!!What?
这些随着我们的深入探讨,结论就会浮出水面!针对MotionEvent,我们先说这么多。

MotionEvent事件分发

当一个MotionEvent产生了以后,就是你的手指在屏幕上做一系列动作的时候,系统需要把这一系列的MotionEvent分发给一个具体的View。我们重点需要了解这个分发的过程,那么系统是如何去判断这个事件要给哪个View,也就是说是如何进行分发的呢?

事件分发需要View的三个重要方法来共同完成

1.public boolean dispatchTouchEvent(MotionEvent event)
这个方法就负责事件的分发。如果一个MotionEvent传递给了View,那么dispatchTouchEvent方法一定会被调用!
返回值:表示是否消费了当前事件。可能是View本身的onTouchEvent方法消费,也可能是子View的dispatchTouchEvent方法中消费。返回true表示事件被消费,本次的事件终止。返回false表示View以及子View均没有消费事件,将调用父View的onTouchEvent方法

2.public boolean onInterceptTouchEvent(MotionEvent ev)
这个方法负责事件拦截,当一个ViewGroup在接到MotionEvent事件序列时候,首先会调用此方法判断是否需要拦截。特别注意,这是ViewGroup特有的方法,View并没有此拦截方法
返回值:是否拦截事件传递,返回true表示拦截了事件,那么事件将不再向下分发而是调用View本身的onTouchEvent方法。返回false表示不做拦截,事件将向下分发到子View的dispatchTouchEvent方法。

3.public boolean onTouchEvent(MotionEvent ev)
真正对MotionEvent进行处理或者说消费的方法。在dispatchTouchEvent里进行调用。
返回值:返回true表示事件被消费,本次的事件终止。返回false表示事件没有被消费,将调用父View的onTouchEvent方法

上面的三个方法可以用以下的伪代码来表示其之间的关系。

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方法
    }

下面简单说下ViewGroup和View在事件分发上的区别:

ViewGroup是View的子类,也就是说ViewGroup本身就是一个View,但是它可以包含子View(当然子View也可能是一个ViewGroup),所以不难理解,上面所展示的伪代码表示的是ViewGroup 处理事件分发的流程。而View本身是不存在分发,所以也没有拦截方法(onInterceptTouchEvent),它只能在onTouchEvent方法中进行处理消费或者不消费。

下面我们通过流程图梳理下事件分发机制:

可以看出事件的传递过程都是从父View到子View。这里有三点需要特别强调下:

1.子View可以通过requestDisallowInterceptTouchEvent方法干预父View的事件分发过程(ACTION_DOWN事件除外),而这就是我们处理滑动冲突常用的关键方法

2.对于View(注意!ViewGroup也是View)而言,如果设置了onTouchListener,那么OnTouchListener方法中的onTouch方法会被回调。onTouch方法返回true,则onTouchEvent方法不会被调用(onClick事件是在onTouchEvent中调用)所以三者优先级是onTouch->onTouchEvent->onClick

3.View 的onTouchEvent 方法默认都会消费掉事件(返回true),除非它是不可点击的(clickable和longClickable同时为false),View的longClickable默认为false,clickable需要区分情况,如Button的clickable默认为true,而TextView的clickable默认为false

下面通过文章开头举的例子继续改进来更深刻的理解

步骤1.ViewGroup中,事件拦截
通过修改自定义控件中onInterceptTouchEvent方法的返回值,返回true表示该控件拦截了该事件。修改MyLinearLayoutB中代码如下:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return true;
    }

运行代码,打印log:

07-25 16:05:47.862 4245-4245/com.mytest E/MainActivity: MyLinearLayoutB--setOnTouchListener---ACTION_DOWN
07-25 16:05:47.864 4245-4245/com.mytest E/MyLinearLayoutB: MyLinearLayoutB---onTouchEvent---MotionEvent:ACTION_DOWN
07-25 16:05:47.939 4245-4245/com.mytest E/MainActivity: MyLinearLayoutB--setOnTouchListener---ACTION_UP
07-25 16:05:47.939 4245-4245/com.mytest E/MyLinearLayoutB: MyLinearLayoutB---onTouchEvent---MotionEvent:ACTION_UP
07-25 16:05:47.950 4245-4245/com.mytest E/MainActivity: MyLinearLayoutB-setOnClickListener

结论:在LinearLayoutB中重写onInterceptTouchEvent方法,并把返回值改为true,此时LinearLayoutB确实拦截了此事件。

步骤2.在步骤1的基础上,事件消费

通过修改LinearLayoutB中onTouchEvent方法的返回值,返回值为true表示事件被消费,false表示事件不被消费,会向上层ViewGroup的onTouchEvent方法传递。

a.修改LinearLayoutB中onInterceptTouchEvent返回值为true,onTouchEvent返回值为true。

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return true;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        String aa = null;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                aa = "ACTION_DOWN";
                break;
            case MotionEvent.ACTION_MOVE:
                aa = "ACTION_MOVE";
                break;
            case MotionEvent.ACTION_UP:
                aa = "ACTION_UP";
                break;
        }
        Log.e(tag, "MyLinearLayoutB---onTouchEvent---MotionEvent:" + aa);
        return true;
    }

打印Log为:

07-25 16:16:43.512 4614-4614/com.mytest E/MainActivity: MyLinearLayoutB--setOnTouchListener---ACTION_DOWN
07-25 16:16:43.514 4614-4614/com.mytest E/MyLinearLayoutB: MyLinearLayoutB---onTouchEvent---MotionEvent:ACTION_DOWN
07-25 16:16:43.571 4614-4614/com.mytest E/MainActivity: MyLinearLayoutB--setOnTouchListener---ACTION_UP
07-25 16:16:43.571 4614-4614/com.mytest E/MyLinearLayoutB: MyLinearLayoutB---onTouchEvent---MotionEvent:ACTION_UP
07-25 16:16:43.598 4614-4614/com.mytest E/MainActivity: MyLinearLayoutB-setOnClickListener

结论:onInterceptTouchEvent返回值为true,拦截了该事件;onTouchEvent返回值为true,消费了该事件。

b.修改onInterceptTouchEvent返回值为true,onTouchEvent返回值为false。

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return true;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        String aa = null;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                aa = "ACTION_DOWN";
                break;
            case MotionEvent.ACTION_MOVE:
                aa = "ACTION_MOVE";
                break;
            case MotionEvent.ACTION_UP:
                aa = "ACTION_UP";
                break;
        }
        Log.e(tag, "MyLinearLayoutB---onTouchEvent---MotionEvent:" + aa);
        return false;
    }

打印Log为:

07-25 16:19:11.675 4937-4937/com.mytest E/MainActivity: MyLinearLayoutB--setOnTouchListener---ACTION_DOWN
07-25 16:19:11.677 4937-4937/com.mytest E/MyLinearLayoutB: MyLinearLayoutB---onTouchEvent---MotionEvent:ACTION_DOWN
07-25 16:19:11.677 4937-4937/com.mytest E/MainActivity: MyLinearLayoutA--setOnTouchListener---ACTION_DOWN
07-25 16:19:11.679 4937-4937/com.mytest E/MyLinearLayoutA: MyLinearLayoutA---onTouchEvent---MotionEvent:ACTION_DOWN
07-25 16:19:11.766 4937-4937/com.mytest E/MainActivity: MyLinearLayoutA--setOnTouchListener---ACTION_UP
07-25 16:19:11.766 4937-4937/com.mytest E/MyLinearLayoutA: MyLinearLayoutA---onTouchEvent---MotionEvent:ACTION_UP
07-25 16:19:11.775 4937-4937/com.mytest E/MainActivity: MyLinearLayoutA-setOnClickListener

结论:onInterceptTouchEvent返回值为true,拦截了该事件;onTouchEvent返回值为false,该事件没有被消费,被传到了上传父ViewGroup(LinearLayoutA)的onTouchEvent方法中。

步骤3.我们再看下子View的事件处理。

ViewGroup的onInterceptTouchEvent返回值为false,事件被传给ViewGroup的子View(MYTextView),因为View没有自己的拦截方法onInterceptTouchEvent,所以View会直接调用自己的onTouchEvent方法;onTouchEvent返回值为true表示该事件被消费,false表示事件不被消费,继续向上传View的onTouchEvent传递。

a.LinearLayoutB(llb)的onInterceptTouchEvent返回值为false,MyTextView(b1)的onTouchEvent返回值为true.

代码比较简单就不贴了,直接看Log:

07-25 16:32:23.974 5306-5306/com.mytest E/MainActivity: MyTextView--setOnTouchListener---ACTION_DOWN
07-25 16:32:23.976 5306-5306/com.mytest E/MyTextView: MyTextView---onTouchEvent---MotionEvent:ACTION_DOWN
07-25 16:32:24.091 5306-5306/com.mytest E/MainActivity: MyTextView--setOnTouchListener---ACTION_UP
07-25 16:32:24.091 5306-5306/com.mytest E/MyTextView: MyTextView---onTouchEvent---MotionEvent:ACTION_UP
07-25 16:32:24.102 5306-5306/com.mytest E/MainActivity: MyTextView-setOnClickListener

结论:可以看出,View没有onInterceptTouchEvent方法拦截事件,通过设置onTouchEvent返回值为true,可以消费事件。

b.LinearLayoutB(llb)的onInterceptTouchEvent返回值为false,MyTextView(b1)的onTouchEvent返回值为false.

07-25 16:34:58.415 5623-5623/com.mytest E/MainActivity: MyTextView--setOnTouchListener---ACTION_DOWN
07-25 16:34:58.416 5623-5623/com.mytest E/MyTextView: MyTextView---onTouchEvent---MotionEvent:ACTION_DOWN
07-25 16:34:58.416 5623-5623/com.mytest E/MainActivity: MyLinearLayoutB--setOnTouchListener---ACTION_DOWN
07-25 16:34:58.417 5623-5623/com.mytest E/MyLinearLayoutB: MyLinearLayoutB---onTouchEvent---MotionEvent:ACTION_DOWN
07-25 16:34:58.536 5623-5623/com.mytest E/MainActivity: MyLinearLayoutB--setOnTouchListener---ACTION_UP
07-25 16:34:58.536 5623-5623/com.mytest E/MyLinearLayoutB: MyLinearLayoutB---onTouchEvent---MotionEvent:ACTION_UP
07-25 16:34:58.542 5623-5623/com.mytest E/MainActivity: MyLinearLayoutB-setOnClickListener

结论:MyTextView(b1)的onTouchEvent返回值为false,MyTextView没有消费该事件,该事件被传递到上层的LinearLayoutB(llb)的onTouchEvent方法中。

3.在步骤3-b的基础上(即LinearLayoutB(llb)的onInterceptTouchEvent返回值为false,MyTextView(b1)的onTouchEvent返回值为false),此时如果LinearLayoutB(llb)的onTouchEvent也为false。

Log:

07-25 16:41:39.172 6211-6211/com.mytest E/MainActivity: MyTextView--setOnTouchListener---ACTION_DOWN
07-25 16:41:39.172 6211-6211/com.mytest E/MyTextView: MyTextView---onTouchEvent---MotionEvent:ACTION_DOWN---isTrue:true
07-25 16:41:39.172 6211-6211/com.mytest E/MainActivity: MyLinearLayoutB--setOnTouchListener---ACTION_DOWN
07-25 16:41:39.173 6211-6211/com.mytest E/MyLinearLayoutB: MyLinearLayoutB---onTouchEvent---MotionEvent:ACTION_DOWN---isTrue:true
07-25 16:41:39.173 6211-6211/com.mytest E/MainActivity: MyLinearLayoutA--setOnTouchListener---ACTION_DOWN
07-25 16:41:39.173 6211-6211/com.mytest E/MyLinearLayoutA: MyLinearLayoutA---onTouchEvent---MotionEvent:ACTION_DOWN---isTrue:true
07-25 16:41:39.282 6211-6211/com.mytest E/MainActivity: MyLinearLayoutA--setOnTouchListener---ACTION_UP
07-25 16:41:39.285 6211-6211/com.mytest E/MyLinearLayoutA: MyLinearLayoutA---onTouchEvent---MotionEvent:ACTION_UP---isTrue:true
07-25 16:41:39.294 6211-6211/com.mytest E/MainActivity: MyLinearLayoutA-setOnClickListener

结论:此时,事件由LinearLayoutB继续向上传递,传递给父View(LinearLayoutA).

下一篇:Android View事件分发机制之源码解析

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值