手擼一個多手勢處理器,移動、縮放、旋轉(zhuǎn)

原諒我真的懶得寫字了,還是把代碼直接貼出來,也方便自己以后需要的時候來抄。

首先是處理器本體:

/**
 * 手勢幫助類,處理手勢的移動、縮放、旋轉(zhuǎn),在 onTouch 事件中把 [MotionEvent] 委托給此類處理。
 * @param onStart 開始手勢處理,在 down 時調(diào)用,調(diào)用方應在此初始化要處理的 View 的初始狀態(tài)。
 * @param onEnd 本次手勢處理結(jié)束,在 up 時調(diào)用,可以在此進行一些狀態(tài)恢復等操作。
 * @param onMove 單指移動事件,基于 [onStart] 時的相對移動位置(是累積量,不是相對上次觸發(fā)的變化量)
 * @param onScale 兩指縮放事件,以 [onStart] 時為基準的相對縮放量(累積量,不是相對上次觸發(fā)的變化量)
 * @param onRotate 單指移動事件,以 [onStart] 時為基準的相對旋轉(zhuǎn)角度(累積量,不是相對上次觸發(fā)的變化量)
 */
class GestureHelper(
    var onStart: (() -> Unit)? = null,
    var onEnd: (() -> Unit)? = null,
    var onMove: ((Float, Float) -> Unit)? = null,
    var onScale: ((Float) -> Unit)? = null,
    var onRotate: ((Float) -> Unit)? = null
) {

    private val moveHandler = MoveHandler { x, y ->
        this.onMove?.invoke(x, y)
    }

    private val scaleHandler = ScaleHandler(
        onScale = {
            this.onScale?.invoke(it)
        },
        onRotate = {
            this.onRotate?.invoke(it)
        }
    )

    fun onTouch(event: MotionEvent) {
        if (event.actionMasked == MotionEvent.ACTION_POINTER_DOWN && event.pointerCount == 2 && !scaleHandler.isStart) {
            scaleHandler.pointDown(1, event.getX(1), event.getY(1))
        }
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                onStart?.invoke()
                moveHandler.pointDown(event.x, event.y)
                scaleHandler.pointDown(0, event.x, event.y)
            }

            MotionEvent.ACTION_MOVE -> {
                if (event.pointerCount == 1 && !scaleHandler.isStart) {
                    moveHandler.handleMove(event.x, event.y)
                }
                if (event.pointerCount == 2) {
                    scaleHandler.dispatch(event.getX(0), event.getY(0), event.getX(1), event.getY(1))
                }
            }

            MotionEvent.ACTION_UP -> {
                scaleHandler.isStart = false
                onEnd?.invoke()
            }
        }
    }
}

本著面向?qū)ο蟮脑瓌t,把單指和兩指的后續(xù)處理分別交給對應的接收器。

單指移動處理:

/**
 * 處理移動事件
 * @param onMove 移動回調(diào),入?yún)⑹窍鄬?DOWN 事件的偏移量
 */
class MoveHandler(val onMove: (Float, Float) -> Unit) {

    private val downPoint = PointF(0f, 0f)

    private var startMove = false

    fun pointDown(x: Float, y: Float) {
        downPoint.x = x
        downPoint.y = y
    }

    fun handleMove(x: Float, y: Float) {
        if (startMove) {
            onMove(x - downPoint.x, y - downPoint.y)
        } else {
            if (max(abs(x - downPoint.x), abs(y - downPoint.y)) > 25)
                startMove = true
        }
    }
}

兩指縮放和旋轉(zhuǎn):

/**
 * 處理縮放事件
 * @param onScale 縮放回調(diào),相對 DOWN 事件的縮放比例
 * @param onRotate 旋轉(zhuǎn)回調(diào),相對 DOWN 事件的旋轉(zhuǎn)角度
 */
class ScaleHandler(val onScale: (Float) -> Unit, val onRotate: (Float) -> Unit) {

    // x1, y1, x2, y2
    private val downPoints = arrayOf(0f, 0f, 0f, 0f)

    private var startScale = false
    private var startRotate = false

    var isStart: Boolean
        get() = startScale || startRotate
        set(value) {
            if (!value) {
                startScale = false
                startRotate = false
            }
        }

    fun pointDown(index: Int, x: Float, y: Float) {
        if (startScale || startRotate) return
        if (index == 0) {
            downPoints[0] = x
            downPoints[1] = y
        }
        if (index == 1) {
            downPoints[2] = x
            downPoints[3] = y
        }
    }

    fun dispatch(x1: Float, y1: Float, x2: Float, y2: Float) {
        if (handleRotate(x1, y1, x2, y2)) return
        if (handleScale(x1, y1, x2, y2)) return
    }

    private fun handleScale(x1: Float, y1: Float, x2: Float, y2: Float): Boolean {
        if (startScale) onScale(calcScale(x1, y1, x2, y2))
        else if (!isStart) {
            val scale = calcScale(x1, y1, x2, y2)
            if (abs(scale - 1) > 0.06f) {
                startScale = true
                onScale(scale)
            }
        }
        return startScale
    }

    private fun handleRotate(x1: Float, y1: Float, x2: Float, y2: Float): Boolean {
        if (startRotate) onRotate(calcRotate(x1, y1, x2, y2))
        else if (!isStart) {
            val rotation = calcRotate(x1, y1, x2, y2)
            if (abs(rotation) > 6f) {
                startRotate = true
                onRotate(rotation)
            }
        }
        return startRotate
    }

    private fun calcScale(x1: Float, y1: Float, x2: Float, y2: Float): Float {
        val downLength = GraphUtil.calcLength(GraphVector(downPoints[0], downPoints[1], downPoints[2], downPoints[3]))
        val nowLength = GraphUtil.calcLength(GraphVector(x1, y1, x2, y2))
        return nowLength / downLength
    }

    private fun calcRotate(x1: Float, y1: Float, x2: Float, y2: Float): Float {
        return GraphUtil.calcVectorDegree(
            GraphVector(downPoints[0], downPoints[1], downPoints[2], downPoints[3]),
            GraphVector(x1, y1, x2, y2)
        )
    }
}

下面是重點了,一些二維向量的相關(guān)計算:

data class GraphVector(
    val x1: Float,
    val y1: Float,
    val x2: Float,
    val y2: Float,
)

object GraphUtil {

    /**
     * 計算兩點間距(向量模)
     */
    fun calcLength(v: GraphVector) = hypot(v.x2 - v.x1.toDouble(), v.y2 - v.y1.toDouble()).toFloat()

    /**
     * 兩個向量點積
     */
    fun calcDotProduct(a: GraphVector, b: GraphVector): Float {
        val ax = a.x2 - a.x1
        val ay = a.y2 - a.y1
        val bx = b.x2 - b.x1
        val by = b.y2 - b.y1
        return ax * bx + ay * by
    }

    /**
     * 兩個向量叉積
     */
    fun calcCrossProduct(a: GraphVector, b: GraphVector): Float {
        val ax = a.x2 - a.x1
        val ay = a.y2 - a.y1
        val bx = b.x2 - b.x1
        val by = b.y2 - b.y1
        return ax * by - bx * ay
    }

    /**
     * 計算兩個向量夾角,有符號
     * 公式:A×B = |A|·|B|·Cos(Θ) 兩向量點積等于兩向量模與夾角余弦值的乘積
     * @return 兩個向量夾角 -180~180
     */
    fun calcVectorDegree(a: GraphVector, b: GraphVector): Float {
        val degreeAbs = calcVectorDegreeAbs(a, b)
        val crossProduct = calcCrossProduct(a, b)
        return if (crossProduct > 0) degreeAbs else -degreeAbs
    }

    /**
     * 計算兩個向量絕對夾角,無符號
     * 公式:A×B = |A|·|B|·Cos(Θ) 兩向量點積等于兩向量模與夾角余弦值的乘積
     * @return 兩個向量所在直線的夾角 0~180,需要結(jié)合叉積另行判斷正負
     */
    private fun calcVectorDegreeAbs(a: GraphVector, b: GraphVector): Float {
        val dotProduct = calcDotProduct(a, b)
        val aLength = calcLength(a)
        val bLength = calcLength(b)

        return Math.toDegrees(acos(dotProduct.toDouble() / (aLength * bLength))).toFloat()
    }

}

最后在貼一個使用樣例:


/**
 * 手勢拖動、縮放、旋轉(zhuǎn)樣例
 */
class TestMatrixFrag : Fragment(R.layout.fragment_test_matrix) {

    private val vb by viewBinding(FragmentTestMatrixBinding::bind)

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        initView()
    }

    // transX, transY, scale, rotation,記錄開始處理手勢時的 View 狀態(tài)
    private var startParams = arrayOf(0f, 0f, 0f, 0f)
    
    private val gestureHelper = GestureHelper(
        onStart = {
            startParams[0] = vb.viewTarget.translationX
            startParams[1] = vb.viewTarget.translationY
            startParams[2] = vb.viewTarget.scaleX
            startParams[3] = vb.viewTarget.rotation
        },
        onMove = { x, y ->
            vb.viewTarget.translationX = startParams[0] + x
            vb.viewTarget.translationY = startParams[1] + y
        },
        onScale = {
            vb.viewTarget.scaleX = startParams[2] * it
            vb.viewTarget.scaleY = startParams[2] * it
        },
        onRotate = {
            vb.viewTarget.rotation = startParams[3] + it
        }
    )

    @SuppressLint("ClickableViewAccessibility")
    private fun initView() = with(vb) {
        viewMark.background = GradientDrawable().also {
            it.setStroke(10, (0xFF0057B3).toInt())
        }

        viewTarget.setBackgroundColor((0x59FF5A5A).toInt())

        // 重點在這里,設(shè)置 OnTouchListener 然后把 MotionEvent 交給 GestureHelper 處理
        viewMark.setOnTouchListener { _, event ->
            gestureHelper.onTouch(event)
            true
        }
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 手勢識別器是附加到視圖的對象,將低級別事件處理代碼轉(zhuǎn)換為更高級別的操作,它允許視圖以控件執(zhí)行的方式響應操作。 手勢...
    坤坤同學閱讀 4,288評論 0 9
  • 效果圖: Github鏈接:https://github.com/boycy815/PinchImageView ...
    CQ_TYL閱讀 2,363評論 0 0
  • 概述 手勢識別器是處理視圖中的觸摸或者按壓事件最簡單的方法,我們可以在任意視圖上附加一個或多個手勢識別器。手勢識別...
    漸z閱讀 3,212評論 0 2
  • 這是500Lines項目中的A 3D modeller文章的翻譯版,講述如何使用Python,OpenGL,GLU...
    今天又忘記密碼閱讀 1,346評論 0 2
  • [Unity]技術(shù)學習路線圖(長期更新) Unity技術(shù)面試題 一:什么是協(xié)同程序?答:在主線程運行時同時開啟另一...
    肖浩唄閱讀 23,871評論 15 244

友情鏈接更多精彩內(nèi)容