Android 手把手进阶自定义View(十五)- 多指触摸

一、基础准备


1.1、MotionEvent.getActionMasked()

MotionEvent.getAction() 只能用于单指,考虑多指触控时需要用 MotionEvent.getActionMasked()。常见值:

  • ACTION_DOWN:第一个手指按下(之前没有任何手指触摸到 View)
  • ACTION_UP:最后一个手指抬起(抬起之后没有任何手指触摸到 View,这个手指未必是 ACTION_DOWN 的那个手指)
  • ACTION_MOVE:有手指发生移动
  • ACTION_POINTER_DOWN:额外手指按下(按下之前已经有别的手指触摸到 View)
  • ACTION_POINTER_UP:有手指抬起,但不是最后一个(抬起之后,仍然还有别的手指在触摸着 View)

1.2、触摸事件的结构

  • 触摸事件是按序列来分组的,每一组事件必然以 ACTION_DOWN 开头,以 ACTION_UP 或ACTION_CANCEL 结束。
  • ACTION_POINTER_DOWN 和 ACTION_POINTER_UP 和 ACTION_MOVE 一样,只是事件序列中的组成部分,并不会单独分出新的事件序列
  • 触摸事件序列是针对 View 的,而不是针对 pointer 的。「某个 pointer 的事件」这种说法是不正确的。
  • 同一时刻,一个 View 要么没有事件序列,要么只有一个事件序列。

1.3、多点触控的三种类型

  • 接力型:同一时刻只有一个 pointer 起作用,即最新的 pointer。 典型: ListView、RecyclerView。 实现方式:在 ACTION_POINTER_DOWN 和 ACTION_POINTER_UP 时记录下最新的 pointer,在之后的 ACTION_MOVE 事件中使用这个 pointer 来判断位置。
  • 配合型 / 协作型 所有触摸到 View 的 pointer 共同起作用。典型: ScaleGestureDetector,以及 GestureDetector 的 onScroll() 方法判断。 实现方式:在每个 DOWN、 POINTER_DOWN、 POINTER_UP、 UP 事件中使用所有 pointer 的坐标来共同更新焦点坐标,并在 MOVE 事件中使用所有 pointer 的坐标来判断位置。
  • 各自为战型:各个 pointer 做不同的事,互不影响。 典型:支持多画笔的画板应用。 实现方式:在每个 DOWN、 POINTER_DOWN 事件中记录下每个 pointer 的 id,在 MOVE 事件中使用 id 对它们进行跟踪。

下面我们会分别看一下这三种类型的多点触控如何使用。

1.4、actionIndex 和 pointerId

多指触摸时,MotionEvent 会为每个手指分配一个 actionIndex 和 pointerId:

  • actionIndex:代表手指序号,它是会变的,序号从 0 开始。一般用来循环遍历我们的手指。
  • pointerId:它是不会变的,一般用来追踪手指。

举个例子,第一个手指触摸时,它的 actionIndex 和 pointerId 都为 0,新增第二个手指触摸时,它的 actionIndex 都为 1:

 第一个手指第二个手指

actionIndex

01
pointerId01

此时第一个手指离开屏幕后:

 第一个手指(离开)第二个手指

actionIndex

 0
pointerId 1

可以看到,第二个手指的 actionIndex 变成了 0,而 pointerId 没变。

actionIndex 的获取可以直接用 MotionEvent.getActionIndex() 方法获取,而 pointerId 需要用 actionIndex 来获取 :event.getPointerId(actionIndex)。通过 pointerId 也可以获取 actionIndex :findPointerIndex(int pointerId)。

另外,在 View 地 onTouchEvent 方法中,我们平常用 event.getX()、event.getY() 是获取第一个手指的 x 和 y。而如果要获取其他手指,则需要用 getX(int pointerIndex)、getY(int pointerIndex) 获取指定手指的 x、y。

 

二、接力型


接力型的效果是这样的,比如一个 RecyclerView,第一个手指在上面滑动一段距离后,此时第二个手指也放上来了,那么此时由第二个手指控制滑动,第二个手指松开后,又交由第一个手机控制滑动。简单地说就是同一时刻只有最新地那个手指能起作用地。我们先看一下实现代码:

class MultiTouchView1(context: Context?, attrs: AttributeSet?) : View(context, attrs) {

    companion object {
        val IMAGE_WIDTH = Utils.dp2px(200)
    }

    private var mPaint = Paint(Paint.ANTI_ALIAS_FLAG)
    private var mBitmap: Bitmap

    //手指按下时的坐标
    private var mDownPoint = PointF()
    //canvas偏移坐标
    private var mCanvasOffsetPoint = PointF()
    //canvas上一次偏移坐标
    private var mCanvasLastOffsetPoint = PointF()
    //当前正在监控的手指
    private var mTrackingPointerId = 0

    init {
        mBitmap = Utils.decodeBitmap(resources, R.drawable.avatar, IMAGE_WIDTH.toInt())
    }

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.actionMasked) {
            MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> {
                setTrackingPointer(event.actionIndex, event)//分析1
            }
            MotionEvent.ACTION_MOVE -> {
                val index = event.findPointerIndex(mTrackingPointerId)
                mCanvasOffsetPoint.x = mCanvasLastOffsetPoint.x + event.getX(index) - mDownPoint.x
                mCanvasOffsetPoint.y = mCanvasLastOffsetPoint.y + event.getY(index) - mDownPoint.y
                invalidate()
            }
            MotionEvent.ACTION_POINTER_UP -> {
                //抬起手指时切换监控的手指

                //当前抬起的手指index
                val actionIndex = event.actionIndex
                //当前抬起的手指id
                val pointerId = event.getPointerId(actionIndex)
                //看抬起的手指是否是当前正在监控的手指
                if (pointerId == mTrackingPointerId) {
                    //判断抬起的手指是否是最后一个,并将当前追踪点设置为最后一个index对应的point
                    val newIndex =
                        if (actionIndex == event.pointerCount - 1) event.pointerCount - 2 else event.pointerCount - 1//分析2
                    setTrackingPointer(newIndex, event)
                }
            }
        }
        return true
    }

    /**
     * 设置当前追踪的 Pointer
     */
    private fun setTrackingPointer(newPointIndex: Int, event: MotionEvent) {
        mTrackingPointerId = event.getPointerId(newPointIndex)
        mDownPoint.x = event.getX(newPointIndex)
        mDownPoint.y = event.getY(newPointIndex)
        mCanvasLastOffsetPoint.x = mCanvasOffsetPoint.x
        mCanvasLastOffsetPoint.y = mCanvasOffsetPoint.y//分析3
    }

    override fun onDraw(canvas: Canvas) {
        canvas.drawBitmap(mBitmap, mCanvasOffsetPoint.x, mCanvasOffsetPoint.y, mPaint)
    }
}

接下来我们来分析一下:

  • 分析1:当新手指按下时,我们要设置当前监控的手指为这个新手指
  • 分析2:当抬起当前监控的手指时(非当前监控的手指抬起不用做任何处理),我们要切换当前监控的手指为最后一个按下的手指。此时要分两种情况,即当前抬起的手指是否是最后一个按下的手指。我们画图分析一下:

假如当前抬起的手指是最后一个按下的手指(即index为4的手指),那么抬起后应切换当前监控的 index 为 pointerCount - 2 (即index为3的手指) 。如果不是最后一个按下的手指(假如是index为3的手指),那么抬起后应切换当前监控的 index 为 pointerCount - 1(即 index 为4的手指)。

  • 分析3:切换监控的手指时,要记录当前的偏移值,避免切换后跳动。

 

三、配合型 / 协作型


配合型 / 协作型即所有触摸到 View 的 pointer 共同起作用,比如可以多指滑动列表,滑动的距离即多指的焦点(中点)

/**
 * 多指触控:协作型
 * 忽略个体,只看整体的焦点
 */
class MultiTouchView2(context: Context?, attrs: AttributeSet?) : View(context, attrs) {

    companion object {
        val IMAGE_WIDTH = Utils.dp2px(200)
    }

    private var mPaint = Paint(Paint.ANTI_ALIAS_FLAG)
    private var mBitmap: Bitmap

    //手指按下时的坐标
    private var mDownPoint = PointF()
    //canvas偏移坐标
    private var mCanvasOffsetPoint = PointF()
    //canvas上一次偏移坐标
    private var mCanvasLastOffsetPoint = PointF()
    //所有pointer的焦点(中心点)
    private var mPointerFocusPoint = PointF()
    //pointer数量
    private var mPointerCount = 0

    init {
        mBitmap = Utils.decodeBitmap(resources, R.drawable.avatar, IMAGE_WIDTH.toInt())
    }

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {
        //pointer数量
        mPointerCount = event.pointerCount
        //所有pointer的x、y总和
        var sumX = 0f
        var sumY = 0f
        //是否是pointer_up事件
        val isPointerUp = event.actionMasked == MotionEvent.ACTION_POINTER_UP

        for (i in 0 until mPointerCount) {
            //抬起的那个pointer不用计算
            if (!(isPointerUp && i == event.actionIndex)) {
                sumX += event.getX(i)
                sumY += event.getY(i)
            }
        }

        if (isPointerUp) {
            //如果是pointer_up抬起事件,则pointer总数量-1
            mPointerCount -= 1
        }
        //计算焦点
        mPointerFocusPoint.x = sumX / mPointerCount
        mPointerFocusPoint.y = sumY / mPointerCount

        when (event.actionMasked) {
            MotionEvent.ACTION_DOWN,
            MotionEvent.ACTION_POINTER_DOWN,
            MotionEvent.ACTION_POINTER_UP -> {
                mDownPoint.x = mPointerFocusPoint.x
                mDownPoint.y = mPointerFocusPoint.y
                mCanvasLastOffsetPoint.x = mCanvasOffsetPoint.x
                mCanvasLastOffsetPoint.y = mCanvasOffsetPoint.y
            }
            MotionEvent.ACTION_MOVE -> {
                mCanvasOffsetPoint.x = mCanvasLastOffsetPoint.x + mPointerFocusPoint.x - mDownPoint.x
                mCanvasOffsetPoint.y = mCanvasLastOffsetPoint.y + mPointerFocusPoint.y - mDownPoint.y
                invalidate()
            }
        }
        return true
    }

    override fun onDraw(canvas: Canvas) {
        canvas.drawBitmap(mBitmap, mCanvasOffsetPoint.x, mCanvasOffsetPoint.y, mPaint)
    }
}

 

四、各自为战型


主要的应用场景是绘图应用,多个手指可以同时个不干扰的绘制,实现也较简单,我们直接看下代码就能明白:

/**
 * 多指触控:各自为战型
 */
class MultiTouchView3(context: Context?, attrs: AttributeSet?) : View(context, attrs) {

    private var mPaint = Paint(Paint.ANTI_ALIAS_FLAG)
    private var mPaths = SparseArray<Path>()

    init {
        mPaint.style = Paint.Style.STROKE
        mPaint.strokeWidth = Utils.dp2px(4)
        mPaint.strokeCap = Paint.Cap.ROUND
        mPaint.strokeJoin = Paint.Join.ROUND
    }

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {

        when (event.actionMasked) {
            MotionEvent.ACTION_DOWN,
            MotionEvent.ACTION_POINTER_DOWN -> {
                val actionIndex = event.actionIndex
                val pointerId = event.getPointerId(actionIndex)
                val path = Path()
                path.moveTo(event.getX(actionIndex), event.getY(actionIndex))
                mPaths.append(pointerId, path)
            }
            MotionEvent.ACTION_MOVE -> {
                for (i in 0 until event.pointerCount) {
                    val path = mPaths.get(event.getPointerId(i))
                    path.lineTo(event.getX(i), event.getY(i))
                }
                invalidate()
            }
            MotionEvent.ACTION_UP,
            MotionEvent.ACTION_POINTER_UP -> {
                //抬起手指时删除绘制
                val pointerId = event.getPointerId(event.actionIndex)
                mPaths.remove(pointerId)
                invalidate()
            }
        }
        return true
    }

    override fun onDraw(canvas: Canvas) {
        for (i in 0 until mPaths.size()) {
            canvas.drawPath(mPaths.valueAt(i), mPaint)
        }
    }
}
  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 学习51单片机和C语言编程,可以帮助我们更深入地理解嵌入式系统的原理和工作方式。对于初学者来说,掌握一份适合自己的学习资料非常重要。 要学习51单片机-C语言版,可以阅读《手把手教你学51单片机-C语言版pdf》这本电子书,这本书内容丰富,讲解详细,配合实例编程,非常适合初学者自学。以下是学习本书的几个关键点: 第一,掌握基本的硬件知识,包括单片机的结构和特性,尤其是各种寄存器的作用和配置方法。 第二,了解C语言编程基础,尤其是语法、数据类型、运算符、控制结构、函数等,这是编写单片机程序的基础。 第三,通过实例编程加强对知识的理解和运用能力。例如,可以尝试写一些简单的IO控制、定时器中断、串口通讯等程序。 第四,可以搭配相应的开发板和开发环境行实践学习。例如,可以使用STC89C51开发板和Keil或SDCC开发环境。 总之,《手把手教你学51单片机-C语言版pdf》这本电子书是一个不错的学习资料,但也需要具备一定的基础知识和耐心,可以结合其他资料和实践不断提高自己的能力。 ### 回答2: 学习51单片机-c语言版, 需要基础的C语言编程知识。在学习前,先要熟悉C语言的数据类型、循环、判断及函数等语法结构,并掌握C语言的编写方法。 在学习51单片机-c语言版之前,需要准备好学习环境,如下载并安装Keil软件, 安装并关联好相应的单片机模拟器。Keil软件中有类似于记事本的编辑窗口用来编写C语言代码, 以及编译,调试和下载程序到单片机等功能。 在学习时,可以选择一些简单的例程开始学习,逐步理解其代码逻辑,了解基本的寄存器操作和中断等知识。可以从LED灯等简单的实验开始,逐渐增加难度和功能的复杂度。 同时,可以参考一些权威的学习资料如《单片机原理与应用》、《51单片机学习与应用》等相关书籍,或结合网络资源行学习。在学习过程中,需要勤加练习,多编写代码行实践,同时多与他人交流学习体会和技术问题。通过坚持不断的学习和练习,便可以逐步掌握51单片机-c语言版编程技巧,提高自己的单片机应用开发能力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值