View 事件体系详解(1)

1、什么是 View

在我们的日常工作中,你可能已经接触过了很多 View ,View 是 Android 中所有控件的基类,例如 Buttton、TextView、LinearLayout,这些类的继承关系如下图的所示(使用 Ctrl + H 快捷键组合可以查看选中类的继承关系):

Buttton
TextView
LinearLayout

上图可以看出 LinearLayout 与 TextView 还有些不同,LinearLayout 的父类是 ViewGroup, ViewGroup 的父类也是 View。

查看 View 源代码可以看到官方对 View 有这样一段说明:

This class represents the basic building block for user interface components. A View
occupies a rectangular area on the screen and is responsible for drawing and
event handling. View is the base class for widgets, which are
used to create interactive UI components (buttons, text fields, etc.). The
{@link android.view.ViewGroup} subclass is the base class for layouts, which
are invisible containers that hold other Views (or other ViewGroups) and define
their layout properties.

View 类是用户界面的基本组成块。一个 View 在屏幕上占据一个矩形区域,负责绘制视图与事件处理。View 是控件的基类,它是用于创建交互式UI组件(按钮、文本域等)。其子类 ViewGroup 类是所有布局(Layout)的基类,布局(Layout)是一个可持有其他 View(或其他 ViewGroup)并能定义它们布局属性的看不见的容器。

2、View 的点击事件

View 类中有一个回调方法为 **onTouchEvent**,该回调方法会在触摸屏幕事件发生时调用,在 View 类中可以通过实现该回调方法来处理 View 的一系列滑动事件的处理。

我们可以自定义一个 MyView 类继承自 View 类,重写 onTouchEvent 方法:

1
2
3
4
5
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG, "onTouch");
return super.onTouchEvent(event);
}

修改布局文件,添加我们自定义的 View,需要注意的是,这里必须要写该类的完整包名:

1
2
3
4
<com.junerver.viewdemo.MyView
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#ffddee"/>

运行后,点击该区域,Logcat 中输出如下:
logcat 输出

可以看出,每次我们点击该 View 所在区域时就会调用一次 onTouchEvent 回调方法。

2.1、MotionEvent

onTouchEvent 回调方法中我们可以发现,该方法接受了一个 MotionEvent 参数,该参数是手指接触屏幕后所产生的的一系列事件。
我们依旧选择查看源码介绍来简单了解该类:

1
2
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.

该对象用于报告运动(鼠标、笔、手指、轨迹球)事件。
运动事件可以持有绝对或相对运动和其他数据,这取决于设备的类型。

细心的你可能会发现,上一小节我们的代码存在一个小问题,那就是如果我们点击 View 后拖动一段距离,并不会产生新的 Log 日志,这与我们预想的不一样,按道理应该会在滑动的过程中产生一系列的事件,并不断的调用 onTouchEvent 回调方法。

其实原因很简单,注意 onTouchEvent 回调方法,这是一个有返回值的方法,它的返回值是一个 boolean 类型,而上一节我们代码的最后一行是 return super.onTouchEvent(event);
该表达式的返回值其实是一个 false,我们可以查看 View 源码中的 onTouchEvent 方法的介绍,其中有这样一句话:

@return True if the event was handled, false otherwise.
如果事件被处理(消费)则返回 true,否则返回 false。

需要注意的是,我们在 View 上点击、滑动的时候,其实会产生很多的 MotionEvent,这些事件如同队列一样,依次的通过 onTouchEvent 回调方法来处理。这里因为我们使用的默认返回值 false,所以系统认为本次事件尚未处理完毕,所以我们后续的滑动事件,并没有被处理,只需将该方法的返回值设为 true 就可以不断的接收到点击事件。

2.2、MotionEvent 事件类型

查看源码可以得知,点击事件本身是区分事件类型的,常用的点击事件类型有:

1
2
3
4
5
6
7
public static final int ACTION_DOWN             = 0;//单点触摸动作
public static final int ACTION_UP = 1;//单点触摸离开动作
public static final int ACTION_MOVE = 2;//触摸点移动动作
public static final int ACTION_CANCEL = 3;//触摸动作取消
public static final int ACTION_OUTSIDE = 4;//触摸动作超出边界
public static final int ACTION_POINTER_DOWN = 5;//多点触摸动作
public static final int ACTION_POINTER_UP = 6;//多点离开动作

正常情况下,触摸一次屏幕都会产生一系列的触摸事件,我们可以使用 MotionEventCompat.getActionMasked(event) 方法来获知本次触摸时间的事件类型。

PS:可能你会在一些代码中看到使用 event.getAction() 方法来获取触摸事件的事件类型,但是需要注意的是,如果该 View 产生的是一个多点触摸事件,那么 event.getAction() 的返回值将不同于 MotionEventCompat.getActionMasked(event)。原因其实很简单,大家可以查看这两个方法的源码,或是参考getAction、getActionMask、getActionIndex区别这篇文章。

修改 onTouchEvent 回调方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = MotionEventCompat.getActionMasked(event);
event.getAction();
switch(action) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "down");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "move");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "up");
break;
}
return true;
}

再次点击该 View 并滑动,可以看到 Logcat 上输出了一系列的触摸事件被触发的日志。
一系列的触摸事件