导言
Android支持多点触摸事件,平时比较常见的可能就是放大和缩小手势,其次常见的可能就是自定义一些滑动视图,为了避免一些“意外”出现(比方说瞬移),还是考虑一下多点触摸
基本概念
Android对于事件的概念都封装在了MotionEvent这个类中,首先了解一些基本的信息
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() {
return nativeGetAction(mNativePtr) & ACTION_MASK;
}
public final int getActionIndex() {
return (nativeGetAction(mNativePtr) & ACTION_POINTER_INDEX_MASK)
>> ACTION_POINTER_INDEX_SHIFT;
}
对比上面的三个方法,举个例子看看:
getAction()本身返回的是一个32位的int,即
0000 0001 0000 0011
getActionMasked()进行了低8位与运算,从而结果只会剩下低8位
0000 0000 0000 0011
getActionIndex()进行了高8位与运算,然后再右移8位,相当于把高8位移到低8位,然后高8位填0
0000 0001 0000 0000
0000 0000 0000 0001
从上述计算可以看出,实际上getAction()返回的是ActionIndex和事件类型的组合
ActionIndex:
取值为0、1、2...N,在手指的移出后Index还会进行自动调整,始终保持0、1...手指数量-1这个序列,也就是说index=0只能标识有一个手指,但是不能说明这个手指一定是第一次按下的手指,同样也可以是从两根手指的情况下,松开第一次按下的手指从而剩下的那个手指
对应于这个还有一个概念
public final int getPointerId(int pointerIndex) {
return nativeGetPointerId(mNativePtr, pointerIndex);
}
pointerId:
区别于ActionIndex,pointerId能够做到保证同一手指的值不变,也就是说可以通过pointerId来标识具体的手指
小结
方法 | 描述 |
---|---|
getAction() | 获得ActionIndex和事件类型组合的值,当且仅当只有一个手指,此时获得的是事件类型,那么也就不能处理多点触摸 |
getActionMasked() | 获得事件类型 |
getActionIndex() | 获得事件Index |
个人来说,推荐在OnTouchEvent中使用getActionMasked()来判断事件类型,这样才是符合MotionEvent的设计原理
多点触摸实践
我们知道处理触摸事件的逻辑是在onTouchEvent(MotionEvent event)中处理
也就是说每一次只能处理单独的一个MotionEvent事件
这里考虑一个滑动视图,比方说
https://github.com/dda135/PullRefreshLayout这一个布局刷新自定义控件
首先明确几个概念:
1.滑动的时候拦截事件的可能是任何一根手指
2.滑动以最后一个按下来的手指为准
3.最后一个滑动的手指松开之后,后续的滑动应该尽量以这个手指之前按下的最后一个手指为准,如果这个手指之前已经松开,那么再往前追溯
概念明确之后,实现思路也就清晰了:
1.应该记录当前最后一个手指的pointerId
2.应该提供一个列表存储按照手指按下的顺序存储pointerId
private static final int INVALID_POINTID = -1;
//用于记录上一次事件的坐标
private Point mLastPoint;
//用于记录当前最后一个手指的pointerId
private int mActivePointId;
//用于按顺序存储按下手指的pointerId
private List mPointIdList = new ArrayList<>();
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
...
int pointerIndex = event.getActionIndex();
int pointerId = event.getPointerId(pointerIndex);
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
mActivePointId = pointerId;
mLastPoint.set((int) event.getX(pointerIndex), (int) event.getY(pointerIndex));
mPointIdList.add(pointerId);
break;
case MotionEvent.ACTION_MOVE:
int activeIndex = event.findPointerIndex(mActivePointId);
if(activeIndex < 0){
return false;
}
int x = (int) event.getX(activeIndex);
int y = (int) event.getY(activeIndex);
int deltaY = (y - mLastPoint.y);
int dy = Math.abs(deltaY);
int dx = Math.abs(x - mLastPoint.x);
if (dy > mTouchSlop && dy >= dx) {
canUp = mOption.canUpToDown();
canDown = mOption.canDownToUp();
canUpIntercept = (deltaY > 0 && canUp);
canDownIntercept = (deltaY < 0 && canDown);
return canUpIntercept || canDownIntercept;
}
return false;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mActivePointId = INVALID_POINTID;
mPointIdList.clear();
break;
case MotionEvent.ACTION_POINTER_UP:
resetTouch(event);
break;
default:
break;
}
return false;
}
/**
* 通过列表结构进行手指回溯操作
* @param event 当前事件对象
*/
private void resetTouch(MotionEvent event){
int currentReleaseIndex = event.getActionIndex();
int currentReleaseId = event.getPointerId(currentReleaseIndex);
mPointIdList.remove((Integer) currentReleaseId);//先移除当前松开的手指
while(mPointIdList.size() > 0){
mActivePointId = mPointIdList.get(mPointIdList.size() - 1);
int pointIndex = event.findPointerIndex(mActivePointId);
if (pointIndex < 0){//当前Id无效,废弃
mPointIdList.remove(mPointIdList.size() - 1);
continue;
}
mLastPoint.set((int)event.getX(pointIndex),(int)event.getY(pointIndex));//当前活动手指变化,记录当前活动的最新坐标
return;
}
mActivePointId = INVALID_POINTID;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
...
int pointIndex = event.getActionIndex();
int pointId = event.getPointerId(pointIndex);
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
mActivePointId = pointId;
mLastPoint.set((int)event.getX(pointIndex),(int)event.getY(pointIndex));
mPointIdList.add(mActivePointId);
break;
case MotionEvent.ACTION_MOVE:
int activePointIndex = event.findPointerIndex(mActivePointId);
if(activePointIndex < 0){
return false;
}
isOnTouch = true;
updatePos((int) (mOption.getMoveRatio() * (event.getY(activePointIndex) - mLastPoint.y)));
mLastPoint.set((int)event.getX(activePointIndex),(int)event.getY(activePointIndex));
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mActivePointId = INVALID_POINTID;
mPointIdList.clear();
...
break;
case MotionEvent.ACTION_POINTER_UP:
resetTouch(event);
break;
default:
break;
}
return true;
}
实际上思路很简单,当手指按下的时候(单指或多指),标记当前手指为活动手指,MOVE事件的时候以活动手指的移动为准,当有手指松开的时候,尝试寻找新的活动手指即可。
总结
Android源码的一些例子可以参考RecyclerView等滑动视图的源码,里面也有一些类似的操作,多点触摸实际上和单点触摸差不多,只要明确基本概念,处理起来也会相对轻松