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

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

首先是处理器本体:

/**
 * 手势帮助类,处理手势的移动、缩放、旋转,在 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
        }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 197,737评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,103评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 144,710评论 0 326
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,909评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,794评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,557评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,939评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,572评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,852评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,871评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,692评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,490评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,939评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,114评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,409评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,971评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,176评论 2 339

推荐阅读更多精彩内容

  • 手势识别器是附加到视图的对象,将低级别事件处理代码转换为更高级别的操作,它允许视图以控件执行的方式响应操作。 手势...
    坤坤同学阅读 4,024评论 0 9
  • 效果图: Github链接:https://github.com/boycy815/PinchImageView ...
    CQ_TYL阅读 2,192评论 0 0
  • 概述 手势识别器是处理视图中的触摸或者按压事件最简单的方法,我们可以在任意视图上附加一个或多个手势识别器。手势识别...
    渐z阅读 2,954评论 0 2
  • 这是500Lines项目中的A 3D modeller文章的翻译版,讲述如何使用Python,OpenGL,GLU...
    今天又忘记密码阅读 1,138评论 0 2
  • [Unity]技术学习路线图(长期更新) Unity技术面试题 一:什么是协同程序?答:在主线程运行时同时开启另一...
    肖浩呗阅读 23,290评论 15 243