简介
MotionEvent 用于表示输入事件,比如由手指、智能笔等触发的输入事件。
单点触控
单个手指触摸屏幕就是属于单点触控的范畴,主要涉及以下几个事件:
- ACTION_DOWN,手指初次接触到屏幕时触发。
- ACTION_MOVE,手指在屏幕上滑动时触发,一般会多次触发。
- ACTION_UP,手指离开屏幕时触发。
- ACTION_CANCEL,事件被上层拦截时触发。
- ACTION_OUTSIDE,手指不在控件区域时触发。
主要涉及以下几个方法:
- getAction(),获取事件类型。
- getX()、getY(),获得触摸点在当前 View 范围内的相对坐标。
- getRawX()、getRawY(),获得触摸点在整个屏幕范围内的相对坐标。
其中有两个比较特殊的事件:ACTION_CANCEL 和 ACTION_OUTSIDE,它们是由系统自己发出来的。
在上层的 View 回收事件处理权的时候,子 View 会收到 ACTION_CANCEL 事件。
比如在垂直方向的 RecyclerView 中,如果在 Adapter 中给 ItemView 添加触摸事件监听,然后上下滑动 RecyclerView ,ItemView 会收到 ACTION_CANCEL 事件,代码如下:
kt
class MyAdapter(var mData: MutableList<Int>): RecyclerView.Adapter<MyAdapter.ViewHolder>() {
private val TAG = MyAdapter::class.java.simpleName
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(ItemMyBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
override fun getItemCount(): Int {
return mData.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.binding.tvNum.text = mData[position].toString()
// 给 ItemView 添加触摸事件监听
holder.binding.root.setOnTouchListener { v, event ->
Log.d(TAG, "itemView onTouchEvent:${event?.action}")
true
}
}
class ViewHolder(val binding: ItemMyBinding) : RecyclerView.ViewHolder(binding.root)
}
这里 RecyclerView 会先收到了一个 ACTION_DOWN 事件,由于这个可能是点击事件,所以它先传递给对应 ItemView,询问 ItemView 是否需要处理这个事件,接下来又收到一个 ACTION_MOVE 事件,且移动的方向和 RecyclerView 的可滑动方向一致,RecyclerView 判断这个事件是滚动事件,于是要收回事件处理权,这时候对应的 ItemView 会收到一个 ACTION_CANCEL,并且不会再收到后续事件了。
ACTION_OUTSIDE 事件用到的几率很小,有兴趣的自己去研究。
多点触控
从 Android 2.2 (API 8) 开始支持多点触控,多点触控主要涉及以下几个事件:
- ACTION_DOWN,第一根手指初次接触到屏幕时触发。
- ACTION_MOVE,手指在屏幕上滑动时触发,一般会多次触发。
- ACTION_UP,最后一根手指离开屏幕时触发。
- ACTION_POINTER_DOWN,有非主要的手指按下(即按下之前已经有手指在屏幕上)时触发。
- ACTION_POINTER_UP,有非主要的手指抬起(即抬起之后仍然有手指在屏幕上)时触发。
主要涉及以下几个方法:
- getActionMasked(),与 getAction() 类似,多点触控必须使用这个方法获取事件类型。
- getActionIndex(),获取该事件的 pointerIndex。
- getPointerCount(),获取触摸的 Pointer(手指)的个数。
- getPointerId(int pointerIndex),获取 Pointer(手指)的 id,系统会给每根手指按下的时候分配一个唯一的 id(按下会触发 ACTION_DOWN 或者 ACTION_POINTER_DOWN),这个 id 直到该手指离开屏幕(触发 ACTION_UP 或者 ACTION_POINTER_UP)或者手势被 Cancel(触发 ACTION_CANCEL)才会失效。
- findPointerIndex(int pointerId),通过 pointerId 获取 pointerIndex,大部分 MotionEvent 中的方法都使用 pointerIndex 作为参数,pointerIndex 的值最小是 0 ,最大小于 getPointerCount();
- getX(int pointerIndex)、getY(int pointerIndex),获取某根手指在当前 View 范围内的相对坐标。
getActionMasked()
多指触摸屏幕时,系统为了区分 Pointer 的同时区分 事件类型 ,使用了一个 int 类型(共 32 位)来同时表示 pointerIndex 和 事件类型,其中最低的 8 位(0x000000ff)表示事件类型,次低的 8 位(0x0000ff00)表示 pointerIndex。
运行如下代码:
kt
class MyLinearLayout @JvmOverloads constructor(
context: Context,
attributeSet: AttributeSet? = null,
defStyleAttr: Int = 0): LinearLayout(context, attributeSet, defStyleAttr) {
companion object{
private val TAG = MyLinearLayout::class.java.simpleName
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
return true
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
Log.d(TAG, "onTouchEvent action:${event?.action}")
Log.d(TAG, "onTouchEvent actionIndex:${event?.actionIndex}")
Log.d(TAG, "onTouchEvent actionMasked:${event?.actionMasked}")
return true
}
}
依次在 MyLinearLayout 中按下 4 根手指后打印如下:
onTouchEvent action:0
onTouchEvent actionIndex:0
onTouchEvent actionMasked:0
onTouchEvent action:261
onTouchEvent actionIndex:1
onTouchEvent actionMasked:5
onTouchEvent action:517
onTouchEvent actionIndex:2
onTouchEvent actionMasked:5
onTouchEvent action:773
onTouchEvent actionIndex:3
onTouchEvent actionMasked:5
把 action 的值转成 16 进制并与事件类型对应上,做成表格,如下所示:
手指按下 | 触发事件(数值) |
---|---|
第1个手指按下 | ACTION_DOWN (0x00000000) |
第2个手指按下 | ACTION_POINTER_DOWN (0x00000105) |
第3个手指按下 | ACTION_POINTER_DOWN (0x00000205) |
第4个手指按下 | ACTION_POINTER_DOWN (0x00000305) |
很容易就可以发现它们的对应关系,这里 event?.action 返回的就是这个 int 类型的值,event?.actionMasked 返回的是低 8 位的值用于表示事件类型,event?.actionIndex 返回的是次低的 8 位的值用于表示 pointerIndex。其实通过源码也可以发现它们的关系:
java
public static final int ACTION_MASK = 0xff;
public static final int ACTION_POINTER_INDEX_MASK = 0xff00;
public static final int ACTION_POINTER_INDEX_SHIFT = 8;
public final int getAction() {
return nativeGetAction(mNativePtr);
}
public final int getActionMasked() {
// 取低 8 位
return nativeGetAction(mNativePtr) & ACTION_MASK;
}
public final int getActionIndex() {
// 取次低的 8 位,然后右移 8 位
return (nativeGetAction(mNativePtr) & ACTION_POINTER_INDEX_MASK)
>> ACTION_POINTER_INDEX_SHIFT;
}
需要注意的是,getActionIndex() 只在 down 和 up 时有效,move 时是无效的。 要想在 move 的时候追踪事件就需要使用 pointerId。
触摸事件中 Pointer 出现的的次序不是确定的,所以 pointerIndex 的值也是不确定的,但是 pointerId 在 Pointer 保持 active 期间是一个确定的值,可以使用 getPointerId(pointerIndex: Int) 来获取获取其 id,这样就可以在接下来的一系列触摸事件中追踪这个 Pointer。可以使用 findPointerIndex(pointerId: Int) 来获取后面的触摸事件中这个 Pointer 的 pointerIndex。
历史数据(批处理)
由于我们的设备非常灵敏,手指稍微移动一下就会产生一个移动事件,所以移动事件会产生得特别频繁,为了提高效率,系统会将近期的多个移动事件(move)按照事件发生的顺序进行排序打包放在同一个 MotionEvent 中,与之对应的产生了以下方法:
- getHistorySize(),获取历史事件集合大小。
- getHistoricalX(int pos)、getHistoricalY(int pos),获取第 pos 个历史事件的 x 和 y 坐标 (pos < getHistorySize())。
- getHistoricalX(int pointerIndex, int pos)、getHistoricalY(int pointerIndex, int pos),获取第 pointerIndex 个手指的第 pos 个历史事件的 x 和 y 坐标(pointerIndex < getPointerCount(), pos < getHistorySize())。
注意历史数据只有 ACTION_MOVE 事件,历史数据单点触控和多点触控均可以用。
要按顺序处理这一批中的所有坐标,需要先处理这一批中的 historical 坐标,然后处理 current 坐标。current Pointer 的坐标可以通过 getX(int) 和 getY(int) 拿到,historical 坐标可以通过 getHistoricalX(int, int)和 getHistoricalY(int, int) 拿到。官方给出了代码示例:
java
void printSamples(MotionEvent ev) {
final int historySize = ev.getHistorySize();
final int pointerCount = ev.getPointerCount();
for (int h = 0; h < historySize; h++) {
System.out.printf("At time %d:", ev.getHistoricalEventTime(h));
for (int p = 0; p < pointerCount; p++) {
System.out.printf(" pointer %d: (%f,%f)",
ev.getPointerId(p), ev.getHistoricalX(p, h), ev.getHistoricalY(p, h));
}
}
System.out.printf("At time %d:", ev.getEventTime());
for (int p = 0; p < pointerCount; p++) {
System.out.printf(" pointer %d: (%f,%f)",
ev.getPointerId(p), ev.getX(p), ev.getY(p));
}
}
鼠标事件
触控笔事件和手指事件处理流程大致相同,鼠标事件则不一样,鼠标相关的几个事件如下:
- ACTION_HOVER_ENTER,指针移入到窗口或者 View 区域,但没有按下。
- ACTION_HOVER_MOVE,指针在窗口或者 View 区域移动,但没有按下。
- ACTION_HOVER_EXIT,指针移出到窗口或者 View 区域,但没有按下。
- ACTION_SCROLL,滚轮滚动,可以触发水平滚动(AXIS_HSCROLL)或者垂直滚动(AXIS_VSCROLL)。
注意:
1、这些事件类型是 安卓4.0 (API 14) 才添加的。
2、使用 getActionMasked() 获得这些事件类型。
3、这些事件不会传递到 onTouchEvent(MotionEvent),而是传递到 onGenericMotionEvent(MotionEvent)。
输入设备的类型
通过 getToolType(pointerIndex: Int) 可以获取 Pointer 的类型, 类型包括有:
- TOOL_TYPE_UNKNOWN(未知类型);
- TOOL_TYPE_FINGER(手指);
- TOOL_TYPE_STYLUS(笔);
- TOOL_TYPE_MOUSE(鼠标);
- TOOL_TYPE_ERASER(橡皮擦)等;
注意这里的参数是 pointerIndex,而不是 pointerId。