阅读 450

View的事件分发(一)分发流程

目录

作用

事件分发流程对于我们开发者来讲,有什么作用?

  • 根据我们自己的需求来自定义滑动触摸响应的规则。
  • 解决滑动冲突

相关知识

MotionEvent

引用官方介绍: Object used to report movement (mouse, pen, finger, trackball) events. Motion events may hold either absolute or relative movements and other data, depending on the type of device.

大致意思就是用于报告移动(鼠标,笔,手指,轨迹球)事件的对象。运动事件可以保持绝对或相对运动以及其他数据,具体取决于设备的类型。

在事件分发中,典型常用的事件总共有三个:

  • ACTION_DOWN ------按下操作
  • ACTION_MOVE -------移动操作
  • ACTION_UP -----------抬起操作

一般一个事件的完整流程是 ACTION_DOWN ---> ACTION_MOVE ---> ACTION_UP

常用方法 含义
getX/Y() 点击事件相对于 当前 View 左上角 的 x/y 轴距离
getRawX/Y() 点击事件相对于 手机屏幕左上角 的 x/y 轴距离

事件分发相关方法。

  • public boolean dispatchTouchEvent(MotionEvent ev)

    用于事件分发,将触摸事件向下传递给目标视图,如果它本身就是目标视图,则传递给自己来处理事件。返回结果受自己的 onTouchEvent 和下级 View 的 dispatchTouchEvent 方法影响。

  • public boolean onInterceptTouchEvent(MotionEvent ev)

    在 dispatchTouchEvent 中调用。是否进行事件拦截,如果返回 false ,则代表不拦截事件;如果返回事件为 true,则拦截事件,并且此事件的后续事件都交给自己来处理,不会再调用此方法询问是否拦截。

    只有 ViewGroup 有这个方法,默认返回 false,不拦截。

  • public boolean onTouchEvent(MotionEvent ev)

    在 dispatchTouchEvent 中调用。用来处理事件,返回 true 代表消费事件;返回 false 代表不消费事件

代表三者关系的伪代码:(摘抄于《Android艺术开发探索》第三章)

//伪代码,解释 dispatchTouchEvent 和 onInterceptTouchEvent 以及 onTouchEvent 的调用关系 
public boolean dispatchTouchEvent(MotionEvent ev){
    boolean consume;
    if(onInterceptTouchEvent(ev)){
        consume = onTouchEvent(ev);
    }else{
        consume = childView.dispatchTouchEvent(ev);
    }
    return consume ;
} 
复制代码

通过上面的伪代码,我们可以大致的了解事件的传递规则:对于一个根 ViewGroup 来说,点击事件产生后,首先会传递给它,这时它的 dispatchTouchEvent 就会被调用,如果它的 onInterceptTouchEvent 方法返回 true,就表示要拦截事件,接着事件就交给这个 ViewGroup 的 onTouchEvent 来处理,如果 onInterceptTouchEvent 返回 false,表示不拦截事件,当前事件就会传递给它的子元素,接着子元素的 dispatchTouchEvent 方法就会被调用,如此反复直到事件被最终处理。

分发流程

三个方法在具体项目中的具体的调用流程是什么呢?我们用个 Demo 来演示。

都只是简单重写这三个方法,并打印日志。

public class Group1 extends FrameLayout {
    public static final String TAG = "----------";
    //......代码省略......

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.i(TAG, "dispatchTouchEvent-----------------"+ Util.getMotionEvent(ev) + this.getClass().getSimpleName());
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.i(TAG, "onInterceptTouchEvent-----------------" + Util.getMotionEvent(ev)+ this.getClass().getSimpleName());
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i(TAG, "onTouchEvent-----------------" + Util.getMotionEvent(event)+ this.getClass().getSimpleName());
        return super.onTouchEvent(event);
    }
}
复制代码

View 的代码:

public class View1 extends View {
    public static final String TAG = "----------";
    //......代码省略......

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.i(TAG,"dispatchTouchEvent-----------------" + Util.getMotionEvent(event)+ this.getClass().getSimpleName());
        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i(TAG,"onTouchEvent-----------------"+ Util.getMotionEvent(event) + this.getClass().getSimpleName());
        return super.onTouchEvent(event);
    }
}
复制代码

Util 类代码(打印出事件的名称):

class Util {
    static String getMotionEvent(MotionEvent motionEvent) {
        int action = motionEvent.getAction();
        if (action == MotionEvent.ACTION_DOWN) {
            return "ACTION_DOWN----";
        } else if (action == MotionEvent.ACTION_UP) {
            return "ACTION_UP------";
        } else if (action == MotionEvent.ACTION_MOVE) {
            return "ACTION_MOVE----";
        } else if (action == MotionEvent.ACTION_CANCEL) {
            return "ACTION_CANCEL--";
        } else {
            return String.valueOf(motionEvent);
        }
    }
}
复制代码

MainActivity的代码:

public class MainActivity extends AppCompatActivity {
    public static final String TAG = "----------";
    //......代码省略......

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.i(TAG, "dispatchTouchEvent-----------------" + com.sjc.eventdispatch.Util.getMotionEvent(ev) + this.getClass().getSimpleName());
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i(TAG, "onTouchEvent-----------------" + com.sjc.eventdispatch.Util.getMotionEvent(event) + this.getClass().getSimpleName());
        return super.onTouchEvent(event);
    }

}
复制代码

MainActivity中的 xml 文件:

<?xml version="1.0" encoding="utf-8"?>
<com.sjc.eventdispatch.Group1 xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.sjc.eventdispatch.View1
        android:id="@+id/view1"
        android:layout_width="200dp"
        android:layout_height="300dp"
        android:layout_gravity="center"
        android:background="@color/colorPrimaryDark" />
</com.sjc.eventdispatch.Group1>
复制代码

具体页面呈现:

模拟一:

  • 条件:所有方法都保持返回 super.xxx(event) 状态;
  • 动作:点击 View1 ;
//Log日志
----------: dispatchTouchEvent--------ACTION_DOWN----MainActivity
----------: dispatchTouchEvent--------ACTION_DOWN----Group1
----------: onInterceptTouchEvent--------ACTION_DOWN----Group1
----------: dispatchTouchEvent--------ACTION_DOWN----View1
----------: onTouchEvent--------ACTION_DOWN----View1
----------: onTouchEvent--------ACTION_DOWN----Group1
----------: onTouchEvent--------ACTION_DOWN----MainActivity

----------: dispatchTouchEvent--------ACTION_MOVE----MainActivity
----------: onTouchEvent--------ACTION_MOVE----MainActivity

----------: dispatchTouchEvent--------ACTION_UP------MainActivity
----------: onTouchEvent--------ACTION_UP------MainActivity
复制代码

从事件的传递过程,我们可以看出,当用户触摸屏幕进行操作时,事件先传递给 Activity,然后通过 dispatchTouchEvent 分发给跟布局 Group1 ,Group1 进行事件分发,先进行调用自己的 onInterceptTouchEvent 进行询问是否拦截(默认不拦截),然后就传递给当前点击的子控件 View1,View1 通过自己的 dispatchTouchEvent 方法分发给自己,调用自己的 onTouchEvent 方法。View1 默认不消费事件,事件就传递给了Group1 自己的 onTouchEvent 方法,由于 Group1 也不消费事件,事件就回传给了 Activity,最终 Activity 的 onTouchEvent 接收了事件,并且后续事件也是由它直接接收。

结论:

  1. 如果一个控件不消费传递过来的 DOWN 事件,那么后续事件不会传递给它。
  2. 如果一个点击区域的所有控件都不消费事件,那么这个事件最终会传递个 Activity 。

模拟二:

  • 条件:View1 的 onTouchEvent 方法返回 true ;
  • 动作:点击 View1 ;
//log日志
----------: dispatchTouchEvent--------ACTION_DOWN----MainActivity
----------: dispatchTouchEvent--------ACTION_DOWN----Group1
----------: onInterceptTouchEvent--------ACTION_DOWN----Group1
----------: dispatchTouchEvent--------ACTION_DOWN----View1
----------: onTouchEvent--------ACTION_DOWN----View1

----------: dispatchTouchEvent--------ACTION_MOVE----MainActivity
----------: dispatchTouchEvent--------ACTION_MOVE----Group1
----------: onInterceptTouchEvent--------ACTION_MOVE----Group1
----------: dispatchTouchEvent--------ACTION_MOVE----View1
----------: onTouchEvent--------ACTION_MOVE----View1

----------: dispatchTouchEvent--------ACTION_UP------MainActivity
----------: dispatchTouchEvent--------ACTION_UP------Group1
----------: onInterceptTouchEvent--------ACTION_UP------Group1
----------: dispatchTouchEvent--------ACTION_UP------View1
----------: onTouchEvent--------ACTION_UP------View1
复制代码

典型的点击事件传递过程,用户点击一个按钮,按钮来响应操作,事件被这个按钮消费。并且每次事件传递过来时,Group1 都会调用 onInterceptTouchEvent 方法来进行检查是否拦截。

模拟三:

  • 条件1:View1 的 onTouchEvent 方法返回 true, Group1 的 onInterceptTouchEvent 返回 true 。
  • 动作1:点击 View1 ;
//Log日志
----------: dispatchTouchEvent--------ACTION_DOWN----MainActivity
----------: dispatchTouchEvent--------ACTION_DOWN----Group1
----------: onInterceptTouchEvent--------ACTION_DOWN----Group1
----------: onTouchEvent--------ACTION_DOWN----Group1
----------: onTouchEvent--------ACTION_DOWN----MainActivity
        
----------: dispatchTouchEvent--------ACTION_MOVE----MainActivity
----------: onTouchEvent--------ACTION_MOVE----MainActivity
        
----------: dispatchTouchEvent--------ACTION_UP------MainActivity
----------: onTouchEvent--------ACTION_UP------MainActivity
复制代码
  • 条件2:View1 的 onTouchEvent 方法返回 true ,Group1 的 onInterceptTouchEvent 返回 true ,Group1 的 onTouchEvent 返回 true 。
  • 动作2:点击 View1 。
//Log日志
----------: dispatchTouchEvent--------ACTION_DOWN----MainActivity
----------: dispatchTouchEvent--------ACTION_DOWN----Group1
----------: onInterceptTouchEvent--------ACTION_DOWN----Group1
----------: onTouchEvent--------ACTION_DOWN----Group1
    
----------: dispatchTouchEvent--------ACTION_MOVE----MainActivity
----------: dispatchTouchEvent--------ACTION_MOVE----Group1
----------: onTouchEvent--------ACTION_MOVE----Group1
    
----------: dispatchTouchEvent--------ACTION_UP------MainActivity
----------: dispatchTouchEvent--------ACTION_UP------Group1
----------: onTouchEvent--------ACTION_UP------Group1
复制代码

Group1 的 onInterceptTouchEvent 返回 true 拦截事件,它的 onTouchEvent 被调用。onTouchEvent 返回 true 代表事件被消费,后续事件都传递给它来处理;onTouchEvent 返回 false 代表事件没有消费,事件向上传递给 Activity ,后续事件都不会传递给 Group1 和 View1 来处理。

结论:

  1. ViewGroup 一旦拦截事件后,后续事件就会交给它来处理,并且不会再调用 onInterceptTouchEvent 方法询问是否拦截。
  2. 再次验证了 如果一个控件不消费传递过来的 DOWN 事件,那么后续事件不会传递给它

总结

其他知识点:

CANCEL 事件的理解

事件序列的非人为的提前结束。

模拟条件:

  • View1 的 onTouchEvent 方法返回 true ;
  • Group1 的 onInterceptTouchEvent 在 MOVE 事件上返回 true,其他事件返回false。
  • 其他方法保持返回 super.xxx()。

模拟动作: 点击 View1 ;

public class Group1 extends FrameLayout {
    public static final String TAG = "----------";
    //......代码省略......
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.i(TAG, "onInterceptTouchEvent--------" + Util.getMotionEvent(ev) + this.getClass().getSimpleName());
        boolean intercept;
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                intercept = false;
                break;
            case MotionEvent.ACTION_MOVE:
                intercept = true;
                break;
            case MotionEvent.ACTION_UP:
                intercept = false;
                break;
            default:
                intercept = false;
                break;
        }
        return intercept;
    }
}
复制代码
//log日志
----------: dispatchTouchEvent--------ACTION_DOWN----MainActivity
----------: dispatchTouchEvent--------ACTION_DOWN----Group1
----------: onInterceptTouchEvent--------ACTION_DOWN----Group1
----------: dispatchTouchEvent--------ACTION_DOWN----View1
----------: onTouchEvent--------ACTION_DOWN----View1
        
----------: dispatchTouchEvent--------ACTION_MOVE----MainActivity
----------: dispatchTouchEvent--------ACTION_MOVE----Group1
----------: onInterceptTouchEvent--------ACTION_MOVE----Group1
----------: dispatchTouchEvent--------ACTION_CANCEL--View1
----------: onTouchEvent--------ACTION_CANCEL--View1
        
----------: dispatchTouchEvent--------ACTION_MOVE----MainActivity
----------: dispatchTouchEvent--------ACTION_MOVE----Group1
----------: onTouchEvent--------ACTION_MOVE----Group1
----------: onTouchEvent--------ACTION_MOVE----MainActivity
        
----------: dispatchTouchEvent--------ACTION_UP------MainActivity
----------: dispatchTouchEvent--------ACTION_UP------Group1
----------: onTouchEvent--------ACTION_UP------Group1
----------: onTouchEvent--------ACTION_UP------MainActivity
复制代码

在这个事件流中,View1 首先消费了 DOWN 事件。当用户移动,产生了 MOVE 事件,由于 Group1 的 onInterceptTouchEvent 方法在 MOVE 事件中返回 true,表示开始拦截事件,MOVE 事件及后面的 UP 事件都交给 Group 1处理,不再传递给 View1。而此时 View1 只消费了 DOWN 事件,处于一个事件流的中途阶段,为了让 View1 有一个完整的事件流,就传递给 View1 一个 CANCEL 事件,从而告诉 View1 这个事件流对于它来说已经结束了。

requestDisallowInterceptTouchEvent

请求父控件不要拦截事件。属于 ViewParent 的方法,ViewParent是一个接口,ViewGroup 实现了 ViewParent 接口。

模拟条件:

  • View1 的 onTouchEvent 方法返回 true ,在 dispatchTouchEvent 方法中调用 requestDisallowInterceptTouchEvent 方法;
  • Group1 的 onInterceptTouchEvent 在 MOVE 事件上返回 true,其他事件返回false。
  • 其他方法保持返回 super.xxx()。

模拟动作: 点击 View1 ;

public class View1 extends View {
    public static final String TAG = "----------";
    //......代码省略......
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.i(TAG, "dispatchTouchEvent--------" + Util.getMotionEvent(event) + this.getClass().getSimpleName());
        getParent().requestDisallowInterceptTouchEvent(true);
        return super.dispatchTouchEvent(event);
    }
}
复制代码
----------: dispatchTouchEvent--------ACTION_DOWN----MainActivity
----------: dispatchTouchEvent--------ACTION_DOWN----Group1
----------: onInterceptTouchEvent--------ACTION_DOWN----Group1
----------: dispatchTouchEvent--------ACTION_DOWN----View1
----------: onTouchEvent--------ACTION_DOWN----View1
        
----------: dispatchTouchEvent--------ACTION_MOVE----MainActivity
----------: dispatchTouchEvent--------ACTION_MOVE----Group1
----------: dispatchTouchEvent--------ACTION_MOVE----View1
----------: onTouchEvent--------ACTION_MOVE----View1
        
----------: dispatchTouchEvent--------ACTION_UP------MainActivity
----------: dispatchTouchEvent--------ACTION_UP------Group1
----------: onInterceptTouchEvent--------ACTION_UP------Group1
----------: dispatchTouchEvent--------ACTION_UP------View1
----------: onTouchEvent--------ACTION_UP------View1
复制代码

我们可以看到 View1 调用 requestDisallowInterceptTouchEvent 后,能够接收到 MOVE 事件以及后续其他事件,Group1 的拦截并没有起作用。从而我们可以使用 requestDisallowInterceptTouchEvent 来解决一些开发商的滑动冲突之类的问题。

注意: View 调用 requestDisallowInterceptTouchEvent 请求父控件不拦截生效有意义的前提是: View接收到了 DOWN 事件,并且消费了 DOWN 事件。如果一个控件不消费 DOWN 事件,那么后续事件也不会传递给它。

源码分析

View的事件分发(二)源码分析


本篇文章用于记录学习过程中的理解和笔记,如有错误,请批评指正,万分感谢!

事件分发系列文章:

View的事件分发(二)源码分析

View的事件分发(三)源码分析

参考文档

《Android开发艺术探索》第三章

Android事件分发机制,大表哥带你慢慢深入