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 详解

相关推荐
雨白5 小时前
Jetpack系列(二):Lifecycle与LiveData结合,打造响应式UI
android·android jetpack
kk爱闹6 小时前
【挑战14天学完python和pytorch】- day01
android·pytorch·python
每次的天空8 小时前
Android-自定义View的实战学习总结
android·学习·kotlin·音视频
恋猫de小郭9 小时前
Flutter Widget Preview 功能已合并到 master,提前在体验毛坯的预览支持
android·flutter·ios
断剑重铸之日10 小时前
Android自定义相机开发(类似OCR扫描相机)
android
随心最为安10 小时前
Android Library Maven 发布完整流程指南
android
岁月玲珑10 小时前
【使用Android Studio调试手机app时候手机老掉线问题】
android·ide·android studio
还鮟14 小时前
CTF Web的数组巧用
android
小蜜蜂嗡嗡15 小时前
Android Studio flutter项目运行、打包时间太长
android·flutter·android studio
aqi0015 小时前
FFmpeg开发笔记(七十一)使用国产的QPlayer2实现双播放器观看视频
android·ffmpeg·音视频·流媒体