Android 手把手進階自定義View(十五)- 多指觸摸

一、基礎準備


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)
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章