MotionEvent

简介

MotionEvent 用于表示输入事件,比如由手指、智能笔等触发的输入事件。

单点触控

单个手指触摸屏幕就是属于单点触控的范畴,主要涉及以下几个事件:

  1. ACTION_DOWN,手指初次接触到屏幕时触发。
  2. ACTION_MOVE,手指在屏幕上滑动时触发,一般会多次触发。
  3. ACTION_UP,手指离开屏幕时触发。
  4. ACTION_CANCEL,事件被上层拦截时触发。
  5. ACTION_OUTSIDE,手指不在控件区域时触发。

主要涉及以下几个方法:

  1. getAction(),获取事件类型。
  2. getX()、getY(),获得触摸点在当前 View 范围内的相对坐标。
  3. 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) 开始支持多点触控,多点触控主要涉及以下几个事件:

  1. ACTION_DOWN,第一根手指初次接触到屏幕时触发。
  2. ACTION_MOVE,手指在屏幕上滑动时触发,一般会多次触发。
  3. ACTION_UP,最后一根手指离开屏幕时触发。
  4. ACTION_POINTER_DOWN,有非主要的手指按下(即按下之前已经有手指在屏幕上)时触发。
  5. ACTION_POINTER_UP,有非主要的手指抬起(即抬起之后仍然有手指在屏幕上)时触发。

主要涉及以下几个方法:

  1. getActionMasked(),与 getAction() 类似,多点触控必须使用这个方法获取事件类型。
  2. getActionIndex(),获取该事件的 pointerIndex。
  3. getPointerCount(),获取触摸的 Pointer(手指)的个数。
  4. getPointerId(int pointerIndex),获取 Pointer(手指)的 id,系统会给每根手指按下的时候分配一个唯一的 id(按下会触发 ACTION_DOWN 或者 ACTION_POINTER_DOWN),这个 id 直到该手指离开屏幕(触发 ACTION_UP 或者 ACTION_POINTER_UP)或者手势被 Cancel(触发 ACTION_CANCEL)才会失效。
  5. findPointerIndex(int pointerId),通过 pointerId 获取 pointerIndex,大部分 MotionEvent 中的方法都使用 pointerIndex 作为参数,pointerIndex 的值最小是 0 ,最大小于 getPointerCount();
  6. 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 中,与之对应的产生了以下方法:

  1. getHistorySize(),获取历史事件集合大小。
  2. getHistoricalX(int pos)、getHistoricalY(int pos),获取第 pos 个历史事件的 x 和 y 坐标 (pos < getHistorySize())。
  3. 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));
      }
  }

鼠标事件

触控笔事件和手指事件处理流程大致相同,鼠标事件则不一样,鼠标相关的几个事件如下:

  1. ACTION_HOVER_ENTER,指针移入到窗口或者 View 区域,但没有按下。
  2. ACTION_HOVER_MOVE,指针在窗口或者 View 区域移动,但没有按下。
  3. ACTION_HOVER_EXIT,指针移出到窗口或者 View 区域,但没有按下。
  4. 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。

参考资料

MotionEvent
MotionEvent 详解

相关推荐
xzkyd outpaper5 分钟前
onSaveInstanceState() 和 ViewModel 在数据保存能力差异
android·计算机八股
CYRUS STUDIO1 小时前
FART 脱壳某大厂 App + CodeItem 修复 dex + 反编译还原源码
android·安全·逆向·app加固·fart·脱壳
WAsbry1 小时前
现代 Android 开发自定义主题实战指南
android·kotlin·material design
xzkyd outpaper2 小时前
Android动态广播注册收发原理
android·计算机八股
唐墨1232 小时前
android与Qt类比
android·开发语言·qt
林林要一直努力3 小时前
Android Studio 向模拟器手机添加照片、视频、音乐
android·智能手机·android studio
AD钙奶-lalala3 小时前
Mac版本Android Studio配置LeetCode插件
android·ide·android studio
散人10244 小时前
Android Test3 获取的ANDROID_ID值不同
android·unit testing
雨白4 小时前
实现动态加载布局
android
帅得不敢出门5 小时前
Android设备推送traceroute命令进行网络诊断
android·网络