多点触控
多点触控这一块推荐看谷歌的官方文档 : 处理多点触控手势和Gcs的安卓自定义View进阶-多点触控详解,官方文档内容比较简单,很多知识点讲得不透彻,需要自己多使用总结才能明白得比较深,Gcs这篇文档讲得就很详细,该注意的点和Demo都写得很好,这里就不重复造轮子了,想了解多点触控的童鞋可以看看这两篇文章,相信会大有收益。
下面仅记录我自己的一些总结并分享一个三指截屏实现方案中多点触控事件的处理方式。
多点触控的事件和方法
| 事件 | 简介 |
|---|---|
| ACTION_DOWN | 第一个 手指 初次接触到屏幕 时触发。 |
| ACTION_MOVE | 手指 在屏幕上滑动 时触发,会多次触发。 |
| ACTION_UP | 最后一个 手指 离开屏幕 时触发。 |
| ACTION_POINTER_DOWN | 有非主要的手指按下(即按下之前已经有手指在屏幕上)。 |
| ACTION_POINTER_UP | 有非主要的手指抬起(即抬起之后仍然有手指在屏幕上)。 |
| 以下事件类型不推荐使用 | ---以下事件在 2.2 版本以上被标记为废弃--- |
| ACTION_POINTER_1_DOWN | 第 2 个手指按下,已废弃,不推荐使用。 |
| ACTION_POINTER_2_DOWN | 第 3 个手指按下,已废弃,不推荐使用。 |
| ACTION_POINTER_3_DOWN | 第 4 个手指按下,已废弃,不推荐使用。 |
| ACTION_POINTER_1_UP | 第 2 个手指抬起,已废弃,不推荐使用。 |
| ACTION_POINTER_2_UP | 第 3 个手指抬起,已废弃,不推荐使用。 |
| ACTION_POINTER_3_UP | 第 4 个手指抬起,已废弃,不推荐使用。 |
| 方法 | 简介 |
|---|---|
| getActionMasked() | 与 getAction() 类似,多点触控需要使用这个方法获取事件类型。 |
| getActionIndex() | 获取该事件是哪个指针(手指)产生的。 |
| getPointerCount() | 获取在屏幕上手指的个数。 |
| getPointerId(int pointerIndex) | 获取一个指针(手指)的唯一标识符ID,在手指按下和抬起之间ID始终不变。 |
| findPointerIndex(int pointerId) | 通过PointerId获取到当前状态下PointIndex,之后通过PointIndex获取其他内容。 |
| getX(int pointerIndex) | 获取某一个指针(手指)的X坐标 |
| getY(int pointerIndex) | 获取某一个指针(手指)的Y坐标 |
自己的一些总结
处理跟手指滑动拖动相关类型的问题时,需要考虑多手指触控的情况,避免出现单手指操作正常多手指操作异常的bug
单手指的情况,使用getAction即可,多手指的情况一定得使用getActionMasked,总的来说统一使用getActionMasked即可,如果不需要兼容到2.0版本的话
依次按下多个手指时,收到的down事件,第一个手指只会收到
ACTION_DOWN事件,从第二个手指开始,则都只会收到ACTION_POINTER_DOWN事件
三指截屏代码实践
个人初步看官方文档时,对于什么actionIndex,pointerIndex,pointerId,pointerIndex是一脸懵的,这里先简单说明下,后面再结合一个实例,希望能帮助到跟我有同样困惑的同学。
-
getPointerId(int pointerIndex)函数中传入的pointerIndex其实就是通过getActionIndex()函数得到的,这是一个索引值 -
pointerId是指针唯一标识id,可以通过前面拿到的索引值(actionIndex/pointerIndex),从getPointerId(int pointerIndex)中获取到; -
pointerIndex可以通过findPointerIndex(int pointerId)反向拿到指针索引
下面看一个应用实例吧,现在很多手机厂商做了三指截屏,该功能的实现就得用到多点触控的相关知识,我们来看看该功能的实现过程中,多点触控这一块的逻辑要怎么处理:
注意:下面的代码是有缺陷和不严谨的,这里主要是提供一种思路以及看看多点触控相关函数的应用是怎么样的。
private static final int FINGER_NUM_TRIGGER_SCREEN_SHOT = 3;
private boolean mShouldTriggerScreenShot = true;
private boolean mFinalShouldTriggerScreenShot = false;
private int mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
// 用于记录三指截图时最后一个手指按下的时间以判断手指按下的时间间隔是否超时
private long mLatestPointerPressTime = 0;
// 根据业务需要定义超时时间
private long mTimeout = xxx;
// 分别记录三个手指的按下/抬起时的坐标数据,以便计算各个手指的滑动距离
private HashMap<Integer, Float[][]> mPointersDown = new HashMap<Integer, Float[][]>();
private HashMap<Integer, Float[][]> mPointersUp = new HashMap<Integer, Float[][]>();
@Override
public boolean onTouchEvent(MotionEvent motionEvent) {
// 用一个1x2的二维数组表示每个 副指针 的x/y数据
Float[][] pointerCoordinate = new Float[1][2];
Float[][] pointerDown;
final pointerIndex = event.getActionIndex();
int pointerId = event.getPointerId(pointerIndex);
// 获得该指针(手指)的x/y值
final float pointX = event.getX(pointerIndex);
final float pointY = event.getY(pointerIndex);
pointerCoordinate[0][0] = pointX;
pointerCoordinate[0][1] = pointY;
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mLatestPointerPressTime = SystemClock.uptimeMillis();
mPointersDown.put(pointerId, pointerCoordinate);
break;
case MotionEvent.ACTION_POINTER_DOWN:
if (SystemClock.uptimeMillis() - mLatestPointerPressTime > mTimeout) {
mShouldTriggerScreenShot = false;
} else {
// 刷新副指针按下时间,用于判断手指依次按下的时间是否存在超时
mLatestPointerPressTime = SystemClock.uptimeMillis();
}
// 副指针到来时,将副指针的数据记录下来,方便后续使用
mPointersDown.put(pointerId, pointerCoordinate);
break;
case MotionEvent.ACTION_MOVE:
if(mShouldTriggerScreenShot && event.getPointerCount() == FINGER_NUM_TRIGGER_SCREEN_SHOT){
for(int i = 0; i < event.getPointerCount(); i++){
pointerId = event.getPointerId(i);
pointerDown = mPointersDown.get(pointerId);
// move事件需要检测当前的滑动是否符合触发截屏的条件,例如你们业务是否允许反方向滑动/横滑等条件下触发三至截屏等
......
mFinalShouldTriggerScreenShot = false / true;
}
}
break;
case MotionEvent.ACTION_UP:
mPointersUp.put(pointerId, pointerCoordinate);
// 前面的条件都满足触发截屏的条件时,还需要根据mPointersDown和mPointersUp这两个数据来计算每个手指滑动的距离是否满足TouchSlop等条件
if(mFinalShouldTriggerScreenShot){
// 进行业务判断
......
mFinalShouldTriggerScreenShot = false / true;
} else{
mFinalShouldTriggerScreenShot = false;
}
if(mFinalShouldTriggerScreenShot){
// 执行截屏操作
......
}
mPointersDown.clear();
mPointersUp.clear();
mShouldTriggerScreenShot = true;
mFinalShouldTriggerScreenShot = false;
break;
case MotionEvent.ACTION_CANCEL:
mPointersDown.clear();
mPointersUp.clear();
mShouldTriggerScreenShot = true;
mFinalShouldTriggerScreenShot = false;
break;
}
}
相关函数的使用原因以及相关事件中需要做的处理:
- event.getActionIndex():得到事件索引
- event.getPointerId(pointerIndex):利用前面获取到的索引,获取指针的唯一标识id
- event.getX(pointerIndex):根据id获取事件的坐标,单点触控中我们直接调用
event.getX()即可获取到,但是看其实现会发现该函数获取的是默认指针的数据,也就是id=0,所以多点触控中要获取坐标需要通过id来区分获取
public final float getX() {
return nativeGetAxisValue(mNativePtr, AXIS_X, 0/*def pointer*/, HISTORY_CURRENT);
}
- event.getActionMasked():多点触控需要用该函数获取事件,
getAction()拿到的是主指针事件 - MotionEvent.ACTION_DOWN:主指针按下会触发该事件,这里我们需要去获取主指针的相关坐标数据
- MotionEvent.ACTION_POINTER_DOWN:这里需要获取每个副指针的坐标数据
- MotionEvent.ACTION_MOVE:这里主要根据
getPointerCount()来判断当前指针数是否满足触发截屏的初始条件,满足的前提下再根据业务定义进一步判断是否要触发截屏操作,一般业务定义包括滑动方向,横滑等条件; - event.getPointerCount():用于获取总指针数
- MotionEvent.ACTION_UP:这里主要是在前面允许截图的基础上,在最后一个手指抬起的瞬间,根据业务定义去检查各个指针的滑动距离等,决定是否最终触发截屏操作,最后记得清除数据以保证下个序列事件的正确性
- MotionEvent.ACTION_CANCEL:cancel事件表示这一系列事件被中止了,尝试截屏事件被抛弃了,所以这里只需要简单清除数据即可。
以上内容纯属个人理解,若有纰漏烦请指出,共同进步!