美文网首页
手撸一个多手势处理器,移动、缩放、旋转

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

作者: 小风风吖 | 来源:发表于2023-06-28 17:55 被阅读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
            }
        }
    }
    

    相关文章

      网友评论

          本文标题:手撸一个多手势处理器,移动、缩放、旋转

          本文链接:https://www.haomeiwen.com/subject/uugoydtx.html