導(dǎo)言
Android支持多點(diǎn)觸摸事件,平時(shí)比較常見(jiàn)的可能就是放大和縮小手勢(shì),其次常見(jiàn)的可能就是自定義一些滑動(dòng)視圖,為了避免一些“意外”出現(xiàn)(比方說(shuō)瞬移),還是考慮一下多點(diǎn)觸摸
基本概念
Android對(duì)于事件的概念都封裝在了MotionEvent這個(gè)類中,首先了解一些基本的信息
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;
}
對(duì)比上面的三個(gè)方法,舉個(gè)例子看看:
getAction()本身返回的是一個(gè)32位的int,即
0000 0001 0000 0011
getActionMasked()進(jìn)行了低8位與運(yùn)算,從而結(jié)果只會(huì)剩下低8位
0000 0000 0000 0011
getActionIndex()進(jìn)行了高8位與運(yùn)算,然后再右移8位,相當(dāng)于把高8位移到低8位,然后高8位填0
0000 0001 0000 0000
0000 0000 0000 0001
從上述計(jì)算可以看出,實(shí)際上getAction()返回的是ActionIndex和事件類型的組合
ActionIndex:
取值為0、1、2...N,在手指的移出后Index還會(huì)進(jìn)行自動(dòng)調(diào)整,始終保持0、1...手指數(shù)量-1這個(gè)序列,也就是說(shuō)index=0只能標(biāo)識(shí)有一個(gè)手指,但是不能說(shuō)明這個(gè)手指一定是第一次按下的手指,同樣也可以是從兩根手指的情況下,松開(kāi)第一次按下的手指從而剩下的那個(gè)手指
對(duì)應(yīng)于這個(gè)還有一個(gè)概念
public final int getPointerId(int pointerIndex) {
return nativeGetPointerId(mNativePtr, pointerIndex);
}
pointerId:
區(qū)別于ActionIndex,pointerId能夠做到保證同一手指的值不變,也就是說(shuō)可以通過(guò)pointerId來(lái)標(biāo)識(shí)具體的手指
小結(jié)
| 方法 | 描述 |
|---|---|
| getAction() | 獲得ActionIndex和事件類型組合的值,當(dāng)且僅當(dāng)只有一個(gè)手指,此時(shí)獲得的是事件類型,那么也就不能處理多點(diǎn)觸摸 |
| getActionMasked() | 獲得事件類型 |
| getActionIndex() | 獲得事件Index |
個(gè)人來(lái)說(shuō),推薦在OnTouchEvent中使用getActionMasked()來(lái)判斷事件類型,這樣才是符合MotionEvent的設(shè)計(jì)原理
多點(diǎn)觸摸實(shí)踐
我們知道處理觸摸事件的邏輯是在onTouchEvent(MotionEvent event)中處理
也就是說(shuō)每一次只能處理單獨(dú)的一個(gè)MotionEvent事件
這里考慮一個(gè)滑動(dòng)視圖,比方說(shuō)
https://github.com/dda135/PullRefreshLayout這一個(gè)布局刷新自定義控件
首先明確幾個(gè)概念:
1.滑動(dòng)的時(shí)候攔截事件的可能是任何一根手指
2.滑動(dòng)以最后一個(gè)按下來(lái)的手指為準(zhǔn)
3.最后一個(gè)滑動(dòng)的手指松開(kāi)之后,后續(xù)的滑動(dòng)應(yīng)該盡量以這個(gè)手指之前按下的最后一個(gè)手指為準(zhǔn),如果這個(gè)手指之前已經(jīng)松開(kāi),那么再往前追溯
概念明確之后,實(shí)現(xiàn)思路也就清晰了:
1.應(yīng)該記錄當(dāng)前最后一個(gè)手指的pointerId
2.應(yīng)該提供一個(gè)列表存儲(chǔ)按照手指按下的順序存儲(chǔ)pointerId
private static final int INVALID_POINTID = -1;
//用于記錄上一次事件的坐標(biāo)
private Point mLastPoint;
//用于記錄當(dāng)前最后一個(gè)手指的pointerId
private int mActivePointId;
//用于按順序存儲(chǔ)按下手指的pointerId
private List<Integer> 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;
}
/**
* 通過(guò)列表結(jié)構(gòu)進(jìn)行手指回溯操作
* @param event 當(dāng)前事件對(duì)象
*/
private void resetTouch(MotionEvent event){
int currentReleaseIndex = event.getActionIndex();
int currentReleaseId = event.getPointerId(currentReleaseIndex);
mPointIdList.remove((Integer) currentReleaseId);//先移除當(dāng)前松開(kāi)的手指
while(mPointIdList.size() > 0){
mActivePointId = mPointIdList.get(mPointIdList.size() - 1);
int pointIndex = event.findPointerIndex(mActivePointId);
if (pointIndex < 0){//當(dāng)前Id無(wú)效,廢棄
mPointIdList.remove(mPointIdList.size() - 1);
continue;
}
mLastPoint.set((int)event.getX(pointIndex),(int)event.getY(pointIndex));//當(dāng)前活動(dòng)手指變化,記錄當(dāng)前活動(dòng)的最新坐標(biāo)
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;
}
實(shí)際上思路很簡(jiǎn)單,當(dāng)手指按下的時(shí)候(單指或多指),標(biāo)記當(dāng)前手指為活動(dòng)手指,MOVE事件的時(shí)候以活動(dòng)手指的移動(dòng)為準(zhǔn),當(dāng)有手指松開(kāi)的時(shí)候,嘗試尋找新的活動(dòng)手指即可。
總結(jié)
Android源碼的一些例子可以參考RecyclerView等滑動(dòng)視圖的源碼,里面也有一些類似的操作,多點(diǎn)觸摸實(shí)際上和單點(diǎn)觸摸差不多,只要明確基本概念,處理起來(lái)也會(huì)相對(duì)輕松