一、基礎準備
1.1、MotionEvent.getActionMasked()
MotionEvent.getAction() 只能用於單指,考慮多指觸控時需要用 MotionEvent.getActionMasked()。常見值:
- ACTION_DOWN:第一個手指按下(之前沒有任何手指觸摸到 View)
- ACTION_UP:最後一個手指擡起(擡起之後沒有任何手指觸摸到 View,這個手指未必是 ACTION_DOWN 的那個手指)
- ACTION_MOVE:有手指發生移動
- ACTION_POINTER_DOWN:額外手指按下(按下之前已經有別的手指觸摸到 View)
- ACTION_POINTER_UP:有手指擡起,但不是最後一個(擡起之後,仍然還有別的手指在觸摸着 View)
1.2、觸摸事件的結構
- 觸摸事件是按序列來分組的,每一組事件必然以 ACTION_DOWN 開頭,以 ACTION_UP 或ACTION_CANCEL 結束。
- ACTION_POINTER_DOWN 和 ACTION_POINTER_UP 和 ACTION_MOVE 一樣,只是事件序列中的組成部分,並不會單獨分出新的事件序列
- 觸摸事件序列是針對 View 的,而不是針對 pointer 的。「某個 pointer 的事件」這種說法是不正確的。
- 同一時刻,一個 View 要麼沒有事件序列,要麼只有一個事件序列。
1.3、多點觸控的三種類型
- 接力型:同一時刻只有一個 pointer 起作用,即最新的 pointer。 典型: ListView、RecyclerView。 實現方式:在 ACTION_POINTER_DOWN 和 ACTION_POINTER_UP 時記錄下最新的 pointer,在之後的 ACTION_MOVE 事件中使用這個 pointer 來判斷位置。
- 配合型 / 協作型 所有觸摸到 View 的 pointer 共同起作用。典型: ScaleGestureDetector,以及 GestureDetector 的 onScroll() 方法判斷。 實現方式:在每個 DOWN、 POINTER_DOWN、 POINTER_UP、 UP 事件中使用所有 pointer 的座標來共同更新焦點座標,並在 MOVE 事件中使用所有 pointer 的座標來判斷位置。
- 各自爲戰型:各個 pointer 做不同的事,互不影響。 典型:支持多畫筆的畫板應用。 實現方式:在每個 DOWN、 POINTER_DOWN 事件中記錄下每個 pointer 的 id,在 MOVE 事件中使用 id 對它們進行跟蹤。
下面我們會分別看一下這三種類型的多點觸控如何使用。
1.4、actionIndex 和 pointerId
多指觸摸時,MotionEvent 會爲每個手指分配一個 actionIndex 和 pointerId:
- actionIndex:代表手指序號,它是會變的,序號從 0 開始。一般用來循環遍歷我們的手指。
- pointerId:它是不會變的,一般用來追蹤手指。
舉個例子,第一個手指觸摸時,它的 actionIndex 和 pointerId 都爲 0,新增第二個手指觸摸時,它的 actionIndex 都爲 1:
第一個手指 | 第二個手指 | |
---|---|---|
actionIndex |
0 | 1 |
pointerId | 0 | 1 |
此時第一個手指離開屏幕後:
第一個手指(離開) | 第二個手指 | |
---|---|---|
actionIndex |
0 | |
pointerId | 1 |
可以看到,第二個手指的 actionIndex 變成了 0,而 pointerId 沒變。
actionIndex 的獲取可以直接用 MotionEvent.getActionIndex() 方法獲取,而 pointerId 需要用 actionIndex 來獲取 :event.getPointerId(actionIndex)。通過 pointerId 也可以獲取 actionIndex :findPointerIndex(int pointerId)。
另外,在 View 地 onTouchEvent 方法中,我們平常用 event.getX()、event.getY() 是獲取第一個手指的 x 和 y。而如果要獲取其他手指,則需要用 getX(int pointerIndex)、getY(int pointerIndex) 獲取指定手指的 x、y。
二、接力型
接力型的效果是這樣的,比如一個 RecyclerView,第一個手指在上面滑動一段距離後,此時第二個手指也放上來了,那麼此時由第二個手指控制滑動,第二個手指鬆開後,又交由第一個手機控制滑動。簡單地說就是同一時刻只有最新地那個手指能起作用地。我們先看一下實現代碼:
class MultiTouchView1(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
companion object {
val IMAGE_WIDTH = Utils.dp2px(200)
}
private var mPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private var mBitmap: Bitmap
//手指按下時的座標
private var mDownPoint = PointF()
//canvas偏移座標
private var mCanvasOffsetPoint = PointF()
//canvas上一次偏移座標
private var mCanvasLastOffsetPoint = PointF()
//當前正在監控的手指
private var mTrackingPointerId = 0
init {
mBitmap = Utils.decodeBitmap(resources, R.drawable.avatar, IMAGE_WIDTH.toInt())
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> {
setTrackingPointer(event.actionIndex, event)//分析1
}
MotionEvent.ACTION_MOVE -> {
val index = event.findPointerIndex(mTrackingPointerId)
mCanvasOffsetPoint.x = mCanvasLastOffsetPoint.x + event.getX(index) - mDownPoint.x
mCanvasOffsetPoint.y = mCanvasLastOffsetPoint.y + event.getY(index) - mDownPoint.y
invalidate()
}
MotionEvent.ACTION_POINTER_UP -> {
//擡起手指時切換監控的手指
//當前擡起的手指index
val actionIndex = event.actionIndex
//當前擡起的手指id
val pointerId = event.getPointerId(actionIndex)
//看擡起的手指是否是當前正在監控的手指
if (pointerId == mTrackingPointerId) {
//判斷擡起的手指是否是最後一個,並將當前追蹤點設置爲最後一個index對應的point
val newIndex =
if (actionIndex == event.pointerCount - 1) event.pointerCount - 2 else event.pointerCount - 1//分析2
setTrackingPointer(newIndex, event)
}
}
}
return true
}
/**
* 設置當前追蹤的 Pointer
*/
private fun setTrackingPointer(newPointIndex: Int, event: MotionEvent) {
mTrackingPointerId = event.getPointerId(newPointIndex)
mDownPoint.x = event.getX(newPointIndex)
mDownPoint.y = event.getY(newPointIndex)
mCanvasLastOffsetPoint.x = mCanvasOffsetPoint.x
mCanvasLastOffsetPoint.y = mCanvasOffsetPoint.y//分析3
}
override fun onDraw(canvas: Canvas) {
canvas.drawBitmap(mBitmap, mCanvasOffsetPoint.x, mCanvasOffsetPoint.y, mPaint)
}
}
接下來我們來分析一下:
- 分析1:當新手指按下時,我們要設置當前監控的手指爲這個新手指
- 分析2:當擡起當前監控的手指時(非當前監控的手指擡起不用做任何處理),我們要切換當前監控的手指爲最後一個按下的手指。此時要分兩種情況,即當前擡起的手指是否是最後一個按下的手指。我們畫圖分析一下:
假如當前擡起的手指是最後一個按下的手指(即index爲4的手指),那麼擡起後應切換當前監控的 index 爲 pointerCount - 2 (即index爲3的手指) 。如果不是最後一個按下的手指(假如是index爲3的手指),那麼擡起後應切換當前監控的 index 爲 pointerCount - 1(即 index 爲4的手指)。
- 分析3:切換監控的手指時,要記錄當前的偏移值,避免切換後跳動。
三、配合型 / 協作型
配合型 / 協作型即所有觸摸到 View 的 pointer 共同起作用,比如可以多指滑動列表,滑動的距離即多指的焦點(中點)
/**
* 多指觸控:協作型
* 忽略個體,只看整體的焦點
*/
class MultiTouchView2(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
companion object {
val IMAGE_WIDTH = Utils.dp2px(200)
}
private var mPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private var mBitmap: Bitmap
//手指按下時的座標
private var mDownPoint = PointF()
//canvas偏移座標
private var mCanvasOffsetPoint = PointF()
//canvas上一次偏移座標
private var mCanvasLastOffsetPoint = PointF()
//所有pointer的焦點(中心點)
private var mPointerFocusPoint = PointF()
//pointer數量
private var mPointerCount = 0
init {
mBitmap = Utils.decodeBitmap(resources, R.drawable.avatar, IMAGE_WIDTH.toInt())
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
//pointer數量
mPointerCount = event.pointerCount
//所有pointer的x、y總和
var sumX = 0f
var sumY = 0f
//是否是pointer_up事件
val isPointerUp = event.actionMasked == MotionEvent.ACTION_POINTER_UP
for (i in 0 until mPointerCount) {
//擡起的那個pointer不用計算
if (!(isPointerUp && i == event.actionIndex)) {
sumX += event.getX(i)
sumY += event.getY(i)
}
}
if (isPointerUp) {
//如果是pointer_up擡起事件,則pointer總數量-1
mPointerCount -= 1
}
//計算焦點
mPointerFocusPoint.x = sumX / mPointerCount
mPointerFocusPoint.y = sumY / mPointerCount
when (event.actionMasked) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_POINTER_DOWN,
MotionEvent.ACTION_POINTER_UP -> {
mDownPoint.x = mPointerFocusPoint.x
mDownPoint.y = mPointerFocusPoint.y
mCanvasLastOffsetPoint.x = mCanvasOffsetPoint.x
mCanvasLastOffsetPoint.y = mCanvasOffsetPoint.y
}
MotionEvent.ACTION_MOVE -> {
mCanvasOffsetPoint.x = mCanvasLastOffsetPoint.x + mPointerFocusPoint.x - mDownPoint.x
mCanvasOffsetPoint.y = mCanvasLastOffsetPoint.y + mPointerFocusPoint.y - mDownPoint.y
invalidate()
}
}
return true
}
override fun onDraw(canvas: Canvas) {
canvas.drawBitmap(mBitmap, mCanvasOffsetPoint.x, mCanvasOffsetPoint.y, mPaint)
}
}
四、各自爲戰型
主要的應用場景是繪圖應用,多個手指可以同時個不干擾的繪製,實現也較簡單,我們直接看下代碼就能明白:
/**
* 多指觸控:各自爲戰型
*/
class MultiTouchView3(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
private var mPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private var mPaths = SparseArray<Path>()
init {
mPaint.style = Paint.Style.STROKE
mPaint.strokeWidth = Utils.dp2px(4)
mPaint.strokeCap = Paint.Cap.ROUND
mPaint.strokeJoin = Paint.Join.ROUND
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_POINTER_DOWN -> {
val actionIndex = event.actionIndex
val pointerId = event.getPointerId(actionIndex)
val path = Path()
path.moveTo(event.getX(actionIndex), event.getY(actionIndex))
mPaths.append(pointerId, path)
}
MotionEvent.ACTION_MOVE -> {
for (i in 0 until event.pointerCount) {
val path = mPaths.get(event.getPointerId(i))
path.lineTo(event.getX(i), event.getY(i))
}
invalidate()
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_POINTER_UP -> {
//擡起手指時刪除繪製
val pointerId = event.getPointerId(event.actionIndex)
mPaths.remove(pointerId)
invalidate()
}
}
return true
}
override fun onDraw(canvas: Canvas) {
for (i in 0 until mPaths.size()) {
canvas.drawPath(mPaths.valueAt(i), mPaint)
}
}
}