• 自定义View:多点触控(二)-- 多指协作


    本篇为系列文章
    一个跟随手指滑动的图片 – 单点触摸
    自定义 View:多点触控 (一)-- 手指接力
    自定义View:多点触控(二)-- 多指协作
    自定义 View:多点触控 (三)-- 各自为战

    本文是系列文章,建议先去看之前的文章,否则可能会无法理解接下来的内容

    协作型的本质是,找到所有手指的中心坐标,也就是焦点,围绕焦点坐标搞事情

    我们先贴一下上一篇文章的最重代码,我们需要在上一篇的基础上改造

    package com.example.viewtest.view
    
    import android.content.Context
    import android.graphics.BitmapFactory
    import android.graphics.Canvas
    import android.graphics.Paint
    import android.util.AttributeSet
    import android.view.MotionEvent
    import android.view.View
    import com.example.viewtest.R
    
    class MultiTouchView (context: Context, attrs: AttributeSet?) : View(context, attrs) {
    
        private val bitmap = BitmapFactory.decodeResource(resources, R.drawable.ic_choice_select)
        private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
        private var offsetX = 0f
        private var offsetY = 0f
        private var downX = 0f
        private var downY = 0f
        private var originOffsetX = 0f
        private var originOffsetY = 0f
    
        private var trackingPointerId = 0
    
        override fun onDraw(canvas: Canvas?) {
            super.onDraw(canvas)
            // 绘制时候使用更新后的坐标
            canvas?.drawBitmap(bitmap, offsetX, offsetY, paint)
        }
    
        override fun onTouchEvent(event: MotionEvent?): Boolean {
            when (event?.actionMasked) {
                MotionEvent.ACTION_DOWN -> {
                    trackingPointerId = event.getPointerId(0)
                    // 记录手指按下的坐标
                    downX = event.x
                    downY = event.y
    
                    originOffsetX = offsetX
                    originOffsetY = offsetY
                }
                // 新增的手指按下会触发
                MotionEvent.ACTION_POINTER_DOWN -> {
                    val actionIndex = event.actionIndex
                    trackingPointerId = event.getPointerId(actionIndex)
                    // 记录新手指按下的坐标
                    downX = event.getX(actionIndex)
                    downY = event.getY(actionIndex)
    
                    originOffsetX = offsetX
                    originOffsetY = offsetY
                }
                // 当前 view 上所剩手指大于 1 时,有手指抬起会触发
                MotionEvent.ACTION_POINTER_UP -> {
                    val actionIndex = event.actionIndex
                    val pointerId = event.getPointerId(actionIndex)
                    // 判断抬起手指的 ID 是否是正在操作的手指 ID,如果是,则需要选一根手指来接管操作
                    if (pointerId == trackingPointerId) {
                        // 需要选一根手指来接管操作,具体选哪个手指,需要我们自己写算法,这里选最后一根手指
                        // 注意,这个时候去获取当前View的所有手指的时候,是包括当前正在抬起的手指的
                        // 如果抬起的手指就是最后一根手指,那么我自己是不可能接棒我自己的
                        val newIndex = if (actionIndex == event.pointerCount - 1) {
                            event.pointerCount - 2
                        } else {
                            event.pointerCount - 1
                        }
    
                        trackingPointerId = event.getPointerId(newIndex)
    
                        downX = event.getX(newIndex)
                        downY = event.getY(newIndex)
    
                        originOffsetX = offsetX
                        originOffsetY = offsetY
                    }
    
                }
                MotionEvent.ACTION_MOVE -> {
                    val index = event.findPointerIndex(trackingPointerId)
                    // 记录最新的手指位置
                    offsetX = event.getX(index) - downX + originOffsetX
                    offsetY = event.getY(index) - downY + originOffsetY
                    // 触发重绘
                    invalidate()
                }
            }
            return true
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89

    多指的中心点,也就是焦点怎么算呢?

    举例,两个手指的中心点就是两个两个点的横坐标相加/2,两个点的纵坐标相加/2

    三个点呢?也是一样的

    n个点也一样

    那我们就可以写代码了

        override fun onTouchEvent(event: MotionEvent): Boolean {
            val focusX: Float
            val focusY: Float
            val pointerCount = event.pointerCount
            var sumX = 0f
            var sumY = 0f
    
            for (i in 0 until pointerCount) {
                sumX += event.getX(i)
                sumY += event.getY(i)
            }
    
            focusX = sumX / pointerCount
            focusY = sumY / pointerCount
            
            ......
            
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    以上就是计算一个焦点的过程,可以发现,焦点计算出来之后,本质是View跟随这个焦点移动,具体有多少个手指都没有关系了,所以说又成了最初的单指操作

    那么我们把剩下的代码的单指逻辑使用的手指换成这个焦点就好了

        override fun onTouchEvent(event: MotionEvent): Boolean {
            val focusX: Float
            val focusY: Float
            val pointerCount = event.pointerCount
            var sumX = 0f
            var sumY = 0f
    
            for (i in 0 until pointerCount) {
                sumX += event.getX(i)
                sumY += event.getY(i)
            }
    
            focusX = sumX / pointerCount
            focusY = sumY / pointerCount
    
            when (event.actionMasked) {
                MotionEvent.ACTION_DOWN -> {
                    // 记录焦点的坐标
                    downX = focusX
                    downY = focusY
    
                    originOffsetX = offsetX
                    originOffsetY = offsetY
                }
                .......
                MotionEvent.ACTION_MOVE -> {
                    // 记录最新的焦点位置
                    offsetX = focusX - downX + originOffsetX
                    offsetY = focusY - downY + originOffsetY
                    // 触发重绘
                    invalidate()
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    以上主要体现的是获取焦点替换成虚拟焦点

    那么还用处理多指吗?如果不处理的话,在有新的手指按下的时候,会瞬间改变焦点的位置,会导致图片瞬间移动,所以说我们还是需要处理多指的

    但是这里的处理就不需要那么复杂了,不管是第几根手指按下还是抬起,都只需要做相同的事情,也是之前单点触摸的逻辑:重置焦点坐标

        override fun onTouchEvent(event: MotionEvent): Boolean {
            val focusX: Float
            val focusY: Float
            val pointerCount = event.pointerCount
            var sumX = 0f
            var sumY = 0f
    
            for (i in 0 until pointerCount) {
                sumX += event.getX(i)
                sumY += event.getY(i)
            }
    
            focusX = sumX / pointerCount
            focusY = sumY / pointerCount
    
            when (event.actionMasked) {
                MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN, MotionEvent.ACTION_POINTER_UP -> {
                    trackingPointerId = event.getPointerId(0)
                    // 记录焦点的坐标
                    downX = focusX
                    downY = focusY
    
                    originOffsetX = offsetX
                    originOffsetY = offsetY
                }
    
                MotionEvent.ACTION_MOVE -> {
                    // 记录最新的焦点位置
                    offsetX = focusX - downX + originOffsetX
                    offsetY = focusY - downY + originOffsetY
                    // 触发重绘
                    invalidate()
                }
            }
            return true
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    我们运行以上代码测试:

    1. 一个手指滑动,没有问题
    2. 增加一个手指,一个动一个不动(同向),图片半速移动,没有问题
    3. 两个手指一起向相同方向移动,全速,没有问题
    4. 抬起一个手指,坏了,图像出现了跳跃

    我明明处理了多指的抬起啊,为什么还会出现问题呢,其实问题出现在event.pointerCount,之前我们讲过,这个获取的数量是包括正在抬起的手指的,所以说我们手指抬起的时候,返回的数量和实际的数量是对应不上的

    所以说我们需要处理两个地方

    一个是ACTION_POINTER_UP时,我们需要将返回的手指数量 -1,另一个是在循环计算偏移的时候,要减去这个偏移

        override fun onTouchEvent(event: MotionEvent): Boolean {
            val focusX: Float
            val focusY: Float
            var pointerCount = event.pointerCount
            var sumX = 0f
            var sumY = 0f
    
            // 标记,是否是多指的手指抬起
            val isPointerUp = event.actionMasked == MotionEvent.ACTION_POINTER_UP
    
            for (i in 0 until pointerCount) {
                // 判断是否是抬起,并且是当前手指,当前手指的抬起不应该参与偏移计算
                val b = (isPointerUp && i == event.actionIndex)
                if (!b) {
                    sumX += event.getX(i)
                    sumY += event.getY(i)
                }
            }
    
            // 如果是多指抬起,则应该少一个数量参与计算
            if (isPointerUp) {
                pointerCount--
            }
            focusX = sumX / pointerCount
            focusY = sumY / pointerCount
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    测试

    1. 一个手指滑动,没有问题
    2. 增加一个手指,一个动一个不动(同向),图片半速移动,没有问题
    3. 两个手指一起向相同方向移动,全速,没有问题
    4. 抬起一个手指,没有问题

    好了,我们协作类型的到此讲解完毕,我们贴一下全部代码

    package com.example.viewtest.view
    
    import android.content.Context
    import android.graphics.BitmapFactory
    import android.graphics.Canvas
    import android.graphics.Paint
    import android.util.AttributeSet
    import android.view.MotionEvent
    import android.view.View
    import com.example.viewtest.R
    
    class MultiTouchView2 (context: Context, attrs: AttributeSet?) : View(context, attrs) {
    
        private val bitmap = BitmapFactory.decodeResource(resources, R.drawable.ic_choice_select)
        private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
        private var offsetX = 0f
        private var offsetY = 0f
        private var downX = 0f
        private var downY = 0f
        private var originOffsetX = 0f
        private var originOffsetY = 0f
        
        override fun onDraw(canvas: Canvas?) {
            super.onDraw(canvas)
            // 绘制时候使用更新后的坐标
            canvas?.drawBitmap(bitmap, offsetX, offsetY, paint)
        }
    
        override fun onTouchEvent(event: MotionEvent): Boolean {
            val focusX: Float
            val focusY: Float
            var pointerCount = event.pointerCount
            var sumX = 0f
            var sumY = 0f
    
            // 标记,是否是多指的手指抬起
            val isPointerUp = event.actionMasked == MotionEvent.ACTION_POINTER_UP
    
            for (i in 0 until pointerCount) {
                // 判断是否是抬起,并且是当前手指,当前手指的抬起不应该参与偏移计算
                val b = (isPointerUp && i == event.actionIndex)
                if (!b) {
                    sumX += event.getX(i)
                    sumY += event.getY(i)
                }
            }
    
            // 如果是多指抬起,则应该少一个数量参与计算
            if (isPointerUp) {
                pointerCount--
            }
            focusX = sumX / pointerCount
            focusY = sumY / pointerCount
    
            when (event.actionMasked) {
                MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN, MotionEvent.ACTION_POINTER_UP -> {
                    // 记录焦点的坐标
                    downX = focusX
                    downY = focusY
    
                    originOffsetX = offsetX
                    originOffsetY = offsetY
                }
    
                MotionEvent.ACTION_MOVE -> {
                    // 记录最新的焦点位置
                    offsetX = focusX - downX + originOffsetX
                    offsetY = focusY - downY + originOffsetY
                    // 触发重绘
                    invalidate()
                }
            }
            return true
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
  • 相关阅读:
    Linux简单使用的服务器日常巡检脚本
    CTCLoss原理解读
    用 ChatGPT 做一个 Chrome 扩展 | 京东云技术团队
    Java配置40-配置ELK+Kafka集成
    如何用ChatGPT学或教英文?5个使用ChatGPT的应用场景!
    【Rust 日报】2022-09-18 Linus提议将Rust添加到Linux6.1内核
    【微信小程序入门到精通】— AppID和个性配置你学会了么?
    ELK集群 日志中心集群、kafka、logstash
    java 线程安全问题 三种线程同步方案 线程通信(了解)
    大厂前端面试考什么?
  • 原文地址:https://blog.csdn.net/qq_35178391/article/details/132711706