手撸一个多手势处理器,移动、缩放、旋转

2023-06-28  本文已影响0人  小风风吖

原谅我真的懒得写字了,还是把代码直接贴出来,也方便自己以后需要的时候来抄。

首先是处理器本体:

/**
 * 手势帮助类,处理手势的移动、缩放、旋转,在 onTouch 事件中把 [MotionEvent] 委托给此类处理。
 * @param onStart 开始手势处理,在 down 时调用,调用方应在此初始化要处理的 View 的初始状态。
 * @param onEnd 本次手势处理结束,在 up 时调用,可以在此进行一些状态恢复等操作。
 * @param onMove 单指移动事件,基于 [onStart] 时的相对移动位置(是累积量,不是相对上次触发的变化量)
 * @param onScale 两指缩放事件,以 [onStart] 时为基准的相对缩放量(累积量,不是相对上次触发的变化量)
 * @param onRotate 单指移动事件,以 [onStart] 时为基准的相对旋转角度(累积量,不是相对上次触发的变化量)
 */
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()
            }
        }
    }
}

本着面向对象的原则,把单指和两指的后续处理分别交给对应的接收器。

单指移动处理:

/**
 * 处理移动事件
 * @param onMove 移动回调,入参是相对 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
        }
    }
}

两指缩放和旋转:

/**
 * 处理缩放事件
 * @param onScale 缩放回调,相对 DOWN 事件的缩放比例
 * @param onRotate 旋转回调,相对 DOWN 事件的旋转角度
 */
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)
        )
    }
}

下面是重点了,一些二维向量的相关计算:

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,需要结合叉积另行判断正负
     */
    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()
    }

}

最后在贴一个使用样例:


/**
 * 手势拖动、缩放、旋转样例
 */
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 状态
    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())

        // 重点在这里,设置 OnTouchListener 然后把 MotionEvent 交给 GestureHelper 处理
        viewMark.setOnTouchListener { _, event ->
            gestureHelper.onTouch(event)
            true
        }
    }
}
上一篇下一篇

猜你喜欢

热点阅读