文章目錄
1 事件攔截機制
閱讀下面的多點觸控原理知識,需要了解一定的事件攔截機制原理,可以參考我編寫的文章:Android自定義View系列:事件攔截機制原理
2 getAction()和getActionMasked()的區別
現在網上還是有很多博客在 onTouchEvent()
處理觸摸反饋判斷時使用的 MotionEvent.getAction()
,那麼 MotionEvent.getAction()
和 MotionEvent.getActionMasked()
有什麼區別呢?爲什麼推薦我們使用 MotionEvent.getActionMasked()
?
MotionEvent.getAction()
是在早期Android版本就已經存在,在只有單點觸控的時候只包含事件信息,比如 MotionEvent.ACTION_DOWN
、MotionEvent.ACTION_UP
、MotionEvent.ACTION_MOVE
;但在多點觸控時它就多了一個信息:還需要知道按下的時候你是第一個手指還是非第一個手指,擡起的時候是最後一個手指擡起了還是非最後一個手指擡起了。因爲多點觸控的處理邏輯和單點觸控不同,比如 MotionEvent.ACTION_POINTER_DOWN
、MotionEvent.ACTION_POINTER_UP
,所以就要區分兩種分別處理,Android因此在API 8提供了 MotionEvent.getActionMasked()
和 MotionEvent.getActionIndex()
。那麼 MotionEvent.getAction()
就要將兩個信息壓縮到32位的 int
類型裏面,而 MotionEvent.getActionMasked()
是拆分了信息只有事件信息。
因此在現在的單點觸控情況下使用 MotionEvent.getAction()
是可以正常使用的,但在多點觸控情況下,它沒有將多點觸控的信息拆分全部壓在32位 int
類型裏面,所以處理的信息就會是錯誤的。實際開發中,我們推薦使用 MotionEvent.getActionMasked()
和 MotionEvent.getActionIndex()
來分別判斷回調的事件信息和獲取哪個手指處理事件。
單點觸控事件序列:
ACTION_DOWN P(x, y)
ACTION_MOVE P(x, y)
ACTION_MOVE P(x, y)
ACTION_MOVE P(x, y)
ACTION_MOVE P(x, y)
ACTION_MOVE P(x, y)
ACTION_MOVE P(x, y)
ACTION_MOVE P(x, y)
ACTION_UP P(x, y)
多點觸控事件序列:
ACTION_DOWN:第一個手指按下(之前沒有任何手指觸摸到View)
ACTION_MOVE:有手指發生移動
ACTION_MOVE
ACTION_POINTER_DOWN:額外手指按下(按下之前已經有別的手指觸摸到View)
ACTION_MOVE
ACTION_MOVE
ACTION_POINTER_UP:有手指擡起,但不是最後一個(擡起之後,仍然還有別的手指在觸摸着View)
ACTION_MOVE
ACTION_UP:最後一個手指擡起(擡起之後沒有任何手指觸摸到View,這個手指未必是ACTION_DOWN的那個手指)
可以發現相比單點觸控,處理多點觸控時多了 ACTION_POINTER_DOWN
和 ACTION_POINTER_UP
。
2 多點觸控事件序列分析
在說明多點觸控的三種使用場景之前,有必要理解多點觸控事件序列在處理過程中與單點觸控的不同。
2.1 index和id
上面的多點觸控事件序列我們再將詳細信息追加上:
ACTION_DOWN P(x, y, index, id)
ACTION_MOVE P(x, y, index, id)
ACTION_MOVE P(x, y, index, id)
ACTION_POINTER_DOWN P(x, y, index, id) P(x, y, index, id)
ACTION_MOVE P(x, y, index, id) P(x, y, index, id)
ACTION_MOVE P(x, y, index, id) P(x, y, index, id)
ACTION_POINTER_UP P(x, y, index, id) P(x, y, index, id)
ACTION_MOVE P(x, y, index, id)
ACTION_UP P(x, y, index, id)
pointer多了一些數據:index和id。
- index:當事件序列開始
ACTION_DOWN
的時候,該手指默認會分配一個索引index=0
,表示在多點觸控時該手指的索引,通過該索引我們可以在onTouchEvent()
時通過MotionEvent.getX(index)
或MotionEvent.getY(index)
獲取手指在View的座標
需要特別注意的是:index它並不是固定的,它根據事件序列的改變而改變,它總是有序的。
比如:
ACTION_DOWN P(x, y, 0, 0)
ACTION_MOVE P(x, y, 0, 0)
ACTION_MOVE P(x, y, 0, 0)
ACTION_POINTER_DOWN P(x, y, 0, 0) P(x, y, 1, 1)
ACTION_MOVE P(x, y, 0, 0) P(x, y, 1, 1)
ACTION_MOVE P(x, y, 0, 0) P(x, y, 1, 1)
ACTION_POINTER_UP P(x, y, 0, 0) P(x, y, 1, 1)
ACTION_MOVE P(x, y, 0, 1)
ACTION_UP P(x, y, 0, 1)
上面的事件序列假設有兩個手指,觸發 ACTION_DOWN
手指爲 index=0,id=0
;在事件處理過程中 ACTION_POINTER_DOWN
被回調,說明另一個手指加入事件序列,手指爲 index=1, id=1
;在事件處理過程中 ACTION_POINTER_UP
被回調,有手指鬆開,鬆開的手指是 index=0, id=0
,所以在最後是手指 id=1
的手指繼續事件序列,並且它的index變爲0。
我們可以看下 getX()
和 getY()
的源碼:
public final float getX() {
return nativeGetAxisValue(mNativePtr, AXIS_X, 0, HISTORY_CURRENT);
}
public final float getY() {
return nativeGetAxisValue(mNativePtr, AXIS_Y, 0, HISTORY_CURRENT);
}
getX()
和 getY()
的源碼其中 pointerIndex
的值默認爲0,就是上面舉例的index。
所以在多點觸控中,getX()
和 getY()
是不可用的,它已經將 pointerIndex
寫死值爲0,所以無法準確獲取到需要手指的座標位置。需要更改爲使用 getX(pointerIndex)
和 getY(pointerIndex)
來獲取:
public final float getX(int pointerIndex) {
return nativeGetAxisValue(mNativePtr, AXIS_X, pointerIndex, HISTORY_CURRENT);
}
public final float getY(int pointerIndex) {
return nativeGetAxisValue(mNativePtr, AXIS_Y, pointerIndex, HISTORY_CURRENT);
}
既然index是隨時都在變化的,那麼你可能會疑問:那在多點觸控的時候,對應手指的索引要怎麼拿?我應該怎麼才能拿到觸摸View手指的座標?
答:id。
上面事件序列的pointer還有id,這個id在整個事件序列中它都是固定不變的,通過它我們就可以獲取手指當前的index,繼而通過index再調用 getX(index)
和 getY(index)
獲取到座標點。
Android給我們提供了幾個API在多點觸控場景下使用:
-
MotionEvent.getX(pointerIndex):通過pointerIndex獲取觸摸View的某個手指的橫座標
-
MotionEvent.getY(pointerIndex):通過pointerIndex獲取觸摸View的某個手指的縱座標
-
MotionEvent.getPointerId(pointerIndex):通過pointerIndex獲取觸摸View的某個手指id
-
MotionEvent.findPointerIndex(id):通過id獲取觸摸View的某個手指當前的pointerIndex索引
-
MotionEvent.getActionIndex():在
MotionEvent.ACTION_POINTER_DOWN
獲取非第一個手指按下的手指索引,在MotionEvent.ACTION_POINTER_UP
獲取非最後一個手指按下的索引。該API在多點觸控只適用於上面兩種常見回調使用
2.2 getActionIndex()
getActionIndex()
這個API比較特殊,它在多點觸控的場景只適用於 MotionEvent.ACTION_POINTER_DOWN
和 MotionEvent.ACTION_POINTER_UP
使用,那爲什麼不能在 MotionEvent.ACTION_MOVE
獲取調用?
當多個手指觸摸的時候,其實多個手指是在同時移動的(手指的輕微移動都會回調 MotionEvent.ACTION_MOVE
),我們上面說到pointer的索引index是會根據事件序列的改變而改變的,也就是說導致 MotionEvent.ACTION_MOVE
的手指索引是不斷切換的。所以在 MotionEvent.ACTION_MOVE
並不方便獲取正在導致那個事件的手指,也是沒有意義的,在 MotionEvent.ACTION_MOVE
調用 MotionEvent.getActionIndex()
總是返回0,該值沒有任何意義不代表哪個手指的索引。
3 多點觸控的三種使用場景
3.1 接力型
接力型:同一時刻只有一個pointer起作用,即最新的pointer。典型:ListView、RecyclerView。
接力型的場景說明簡單來說就是:假設有兩個手指,一個手指在觸摸移動View的時候,另一個手指介入此次事件序列,然後其中一個手指鬆手,另一個手指接管了事件序列直到結束。
實現方式:在 ACTION_POINTER_DOWN
和 ACTION_POINTER_UP
時記錄下最新的pointer,在之後的 ACTION_MOVE
事件中使用這個pointer來判斷位置。
demo:多點觸控接力移動圖片
public class MultiTouchView extends View {
private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private Bitmap bitmap;
private float downX;
private float downY;
private float originalOffsetX;
private float originalOffsetY;
private float offsetX;
private float offsetY;
private int trackingPointerId;
public MultiTouchView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.picture);
}
// 多指接力型(兩個手指滑動時,一個手指放開另外一個手指接手滑動)
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN: // 第一根手指按下回調
trackingPointerId = event.getPointerId(0); // 第一根手指index爲0
updateOffset(event, 0);
break;
case MotionEvent.ACTION_MOVE:
int index = event.findPointerIndex(trackingPointerId); // 根據id獲取索引
offsetX = event.getX(index) - downX + originalOffsetX;
offsetY = event.getY(index) - downY + originalOffsetY;
invalidate();
break;
case MotionEvent.ACTION_POINTER_DOWN: // 非第一根手指按下回調
// 有新手指加入事件序列,更新跟蹤的手指id
int actionIndex = event.getActionIndex();
trackingPointerId = event.getPointerId(actionIndex);
// 更新爲新手指的座標,讓新手指接管事件
updateOffset(event, actionIndex);
break;
case MotionEvent.ACTION_POINTER_UP: // 非第一個手指鬆手回調
actionIndex = event.getActionIndex();
int pointerId = event.getPointerId(actionIndex);
// 鬆手的手指是當前正在跟蹤的手指
if (pointerId == trackingPointerId) {
int newIndex;
// getPointerCount()此時還是包含鬆手的手指數算在內
if (actionIndex == event.getPointerCount() - 1) {
newIndex = event.getPointerCount() - 2;
} else {
newIndex = event.getPointerCount() - 1;
}
trackingPointerId = event.getPointerId(newIndex);
updateOffset(event, newIndex);
}
break;
}
return true;
}
private void updateOffset(MotionEvent event, int index) {
downX = event.getX(index);
downY = event.getY(index);
originalOffsetX = offsetX;
originalOffsetY = offsetY;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(bitmap, offsetX, offsetY, paint);
}
}
3.2 配合型/協作型
配合型/協作型:所有觸摸到View的pointer共同起作用。典型:ScaleGestureDetector、GestureDetector的onScroll()判斷。
配合型/協作型的場景說明簡單來說就是:假設有多個手指觸摸View,選擇多個手指的中心位置作爲焦點座標處理View。
實現方式:在每個 MotionEvent.ACTION_DOWN
、MotionEvent.ACTION_POINTER_DOWN
、MotionEvent.ACTION_POINTER_UP
、MotionEvent.ACTION_UP
事件中使用所有pointer的座標來共同更新焦點座標,並在 MotionEvent.ACTION_MOVE
中使用所有pointer的座標來判斷位置。
demo:多個手指觸摸View協同移動圖片
public class MultiTouchView extends View {
private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private Bitmap bitmap;
private float downX;
private float downY;
private float originalOffsetX;
private float originalOffsetY;
private float offsetX;
private float offsetY;
public MultiTouchView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.picture);
}
// 多指協作型(通過多個手指的橫縱座標計算出她們的中心點座標,以中心點座標作爲滑動座標)
@Override
public boolean onTouchEvent(MotionEvent event) {
float sumX = 0;
float sumY = 0;
// 多指滑動鬆開時,因爲ACTION_POINTER_UP此時還在進行中,所以獲取的event.getPointerCount()數量是不對的
// 比如現在兩個手指在滑動bitmap,現在鬆開一個手指,event.getPointerCount()還是2
// 如果不過濾ACTION_POINTER_UP鬆開的手指數量就會導致計算錯誤,出現bitmap位置跳一下到當前手指觸摸位置的問題
boolean isPointerUp = event.getActionMasked() == MotionEvent.ACTION_POINTER_UP;
for (int i = 0; i < event.getPointerCount(); i++) {
// 不是擡起事件,計算沒有擡起的手指的橫縱座標
if (!isPointerUp || i != event.getActionIndex()) {
sumX += event.getX(i);
sumY += event.getY(i);
}
}
int pointerCount = event.getPointerCount();
if (isPointerUp) pointerCount--; // 將擡起的手指的數量去掉
// 計算多個手指的協同的中心位置,即用多個手指的橫縱座標的和 / 點數量 求出平均中心點座標
// 用中心點座標作爲bitmap的移動位置
float focusX = sumX / pointerCount;
float focusY = sumY / pointerCount;
// 因爲現在只要關注多指觸摸計算的中心點座標,所以按下、多指按下、多指鬆開的操作都是重新計算中心點座標
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
case MotionEvent.ACTION_POINTER_UP:
downX = focusX;
downY = focusY;
originalOffsetX = offsetX;
originalOffsetY = offsetY;
break;
case MotionEvent.ACTION_MOVE:
offsetX = focusX - downX + originalOffsetX;
offsetY = focusY - downY + originalOffsetY;
invalidate();
break;
}
return true;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(bitmap, offsetX, offsetY, paint);
}
}
3.3 各自爲戰型
各自爲戰型:各個pointer做不同的事,互不影響。典型:支持多畫筆的畫板應用。
實現方式:在每個 MotionEvent.ACTION_DOWN
、MotionEvent.ACTION_POINTER_DOWN
中記錄下每個pointer的id,在 MotionEvent.ACTION_MOVE
中使用id對它們進行跟蹤。
demo:多指觸摸View實現畫板
public class MultiTouchView extends View {
private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private SparseArray<Path> paths = new SparseArray<>();
public MultiTouchView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
{
paint.setStrokeWidth(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4, getResources().getDisplayMetrics()));
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStrokeJoin(Paint.Join.ROUND);
}
// 多指畫板
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
int actionIndex = event.getActionIndex();
int pointerId = event.getPointerId(actionIndex);
Path path = new Path();
path.moveTo(event.getX(actionIndex), event.getY(actionIndex));
paths.append(pointerId, path);
invalidate();
break;
case MotionEvent.ACTION_MOVE:
for (int i = 0; i < event.getPointerCount(); i++) {
path = paths.get(i);
if (path != null) {
path.lineTo(event.getX(i), event.getY(i));
}
}
invalidate();
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
actionIndex = event.getActionIndex();
pointerId = event.getPointerId(actionIndex);
paths.remove(pointerId);
invalidate();
break;
}
return true;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < paths.size(); i++) {
Path path = paths.valueAt(i);
if (path != null) {
canvas.drawPath(path, paint);
}
}
}
}
4 結合多點觸控的觸摸事件結構總結
-
觸摸事件是按序列來分組的,每一組事件必然以
ACTION_DOWN
開頭,以ACTION_UP
或ACTION_CANCEL
結束 -
ACTION_POINTER_DOWN
和ACTION_POINTER_UP
和ACTION_MOVE
一樣,只是事件序列中的組成部分,並不會單獨分出新的事件序列 -
觸摸事件序列是針對View的,而不是針對pointer的。[某個pointer的事件]這種說法是不正確的
-
在一個觸摸事件裏,每個pointer除了x和y之外,還有index和id
-
[移動的那個手指]這個概念是僞概念,[尋找移動的那個手指]這個需求是個僞需求