当前位置:   article > 正文

Android 手把手进阶自定义View(十五)- 多指触摸_@suppresslint("clickableviewaccessibility")

@suppresslint("clickableviewaccessibility")

一、基础准备


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,第一个手指在上面滑动一段距离后,此时第二个手指也放上来了,那么此时由第二个手指控制滑动,第二个手指松开后,又交由第一个手机控制滑动。简单地说就是同一时刻只有最新地那个手指能起作用地。我们先看一下实现代码:

  1. class MultiTouchView1(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
  2. companion object {
  3. val IMAGE_WIDTH = Utils.dp2px(200)
  4. }
  5. private var mPaint = Paint(Paint.ANTI_ALIAS_FLAG)
  6. private var mBitmap: Bitmap
  7. //手指按下时的坐标
  8. private var mDownPoint = PointF()
  9. //canvas偏移坐标
  10. private var mCanvasOffsetPoint = PointF()
  11. //canvas上一次偏移坐标
  12. private var mCanvasLastOffsetPoint = PointF()
  13. //当前正在监控的手指
  14. private var mTrackingPointerId = 0
  15. init {
  16. mBitmap = Utils.decodeBitmap(resources, R.drawable.avatar, IMAGE_WIDTH.toInt())
  17. }
  18. @SuppressLint("ClickableViewAccessibility")
  19. override fun onTouchEvent(event: MotionEvent): Boolean {
  20. when (event.actionMasked) {
  21. MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> {
  22. setTrackingPointer(event.actionIndex, event)//分析1
  23. }
  24. MotionEvent.ACTION_MOVE -> {
  25. val index = event.findPointerIndex(mTrackingPointerId)
  26. mCanvasOffsetPoint.x = mCanvasLastOffsetPoint.x + event.getX(index) - mDownPoint.x
  27. mCanvasOffsetPoint.y = mCanvasLastOffsetPoint.y + event.getY(index) - mDownPoint.y
  28. invalidate()
  29. }
  30. MotionEvent.ACTION_POINTER_UP -> {
  31. //抬起手指时切换监控的手指
  32. //当前抬起的手指index
  33. val actionIndex = event.actionIndex
  34. //当前抬起的手指id
  35. val pointerId = event.getPointerId(actionIndex)
  36. //看抬起的手指是否是当前正在监控的手指
  37. if (pointerId == mTrackingPointerId) {
  38. //判断抬起的手指是否是最后一个,并将当前追踪点设置为最后一个index对应的point
  39. val newIndex =
  40. if (actionIndex == event.pointerCount - 1) event.pointerCount - 2 else event.pointerCount - 1//分析2
  41. setTrackingPointer(newIndex, event)
  42. }
  43. }
  44. }
  45. return true
  46. }
  47. /**
  48. * 设置当前追踪的 Pointer
  49. */
  50. private fun setTrackingPointer(newPointIndex: Int, event: MotionEvent) {
  51. mTrackingPointerId = event.getPointerId(newPointIndex)
  52. mDownPoint.x = event.getX(newPointIndex)
  53. mDownPoint.y = event.getY(newPointIndex)
  54. mCanvasLastOffsetPoint.x = mCanvasOffsetPoint.x
  55. mCanvasLastOffsetPoint.y = mCanvasOffsetPoint.y//分析3
  56. }
  57. override fun onDraw(canvas: Canvas) {
  58. canvas.drawBitmap(mBitmap, mCanvasOffsetPoint.x, mCanvasOffsetPoint.y, mPaint)
  59. }
  60. }

接下来我们来分析一下:

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

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

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

 

三、配合型 / 协作型


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

  1. /**
  2. * 多指触控:协作型
  3. * 忽略个体,只看整体的焦点
  4. */
  5. class MultiTouchView2(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
  6. companion object {
  7. val IMAGE_WIDTH = Utils.dp2px(200)
  8. }
  9. private var mPaint = Paint(Paint.ANTI_ALIAS_FLAG)
  10. private var mBitmap: Bitmap
  11. //手指按下时的坐标
  12. private var mDownPoint = PointF()
  13. //canvas偏移坐标
  14. private var mCanvasOffsetPoint = PointF()
  15. //canvas上一次偏移坐标
  16. private var mCanvasLastOffsetPoint = PointF()
  17. //所有pointer的焦点(中心点)
  18. private var mPointerFocusPoint = PointF()
  19. //pointer数量
  20. private var mPointerCount = 0
  21. init {
  22. mBitmap = Utils.decodeBitmap(resources, R.drawable.avatar, IMAGE_WIDTH.toInt())
  23. }
  24. @SuppressLint("ClickableViewAccessibility")
  25. override fun onTouchEvent(event: MotionEvent): Boolean {
  26. //pointer数量
  27. mPointerCount = event.pointerCount
  28. //所有pointer的x、y总和
  29. var sumX = 0f
  30. var sumY = 0f
  31. //是否是pointer_up事件
  32. val isPointerUp = event.actionMasked == MotionEvent.ACTION_POINTER_UP
  33. for (i in 0 until mPointerCount) {
  34. //抬起的那个pointer不用计算
  35. if (!(isPointerUp && i == event.actionIndex)) {
  36. sumX += event.getX(i)
  37. sumY += event.getY(i)
  38. }
  39. }
  40. if (isPointerUp) {
  41. //如果是pointer_up抬起事件,则pointer总数量-1
  42. mPointerCount -= 1
  43. }
  44. //计算焦点
  45. mPointerFocusPoint.x = sumX / mPointerCount
  46. mPointerFocusPoint.y = sumY / mPointerCount
  47. when (event.actionMasked) {
  48. MotionEvent.ACTION_DOWN,
  49. MotionEvent.ACTION_POINTER_DOWN,
  50. MotionEvent.ACTION_POINTER_UP -> {
  51. mDownPoint.x = mPointerFocusPoint.x
  52. mDownPoint.y = mPointerFocusPoint.y
  53. mCanvasLastOffsetPoint.x = mCanvasOffsetPoint.x
  54. mCanvasLastOffsetPoint.y = mCanvasOffsetPoint.y
  55. }
  56. MotionEvent.ACTION_MOVE -> {
  57. mCanvasOffsetPoint.x = mCanvasLastOffsetPoint.x + mPointerFocusPoint.x - mDownPoint.x
  58. mCanvasOffsetPoint.y = mCanvasLastOffsetPoint.y + mPointerFocusPoint.y - mDownPoint.y
  59. invalidate()
  60. }
  61. }
  62. return true
  63. }
  64. override fun onDraw(canvas: Canvas) {
  65. canvas.drawBitmap(mBitmap, mCanvasOffsetPoint.x, mCanvasOffsetPoint.y, mPaint)
  66. }
  67. }

 

四、各自为战型


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

  1. /**
  2. * 多指触控:各自为战型
  3. */
  4. class MultiTouchView3(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
  5. private var mPaint = Paint(Paint.ANTI_ALIAS_FLAG)
  6. private var mPaths = SparseArray<Path>()
  7. init {
  8. mPaint.style = Paint.Style.STROKE
  9. mPaint.strokeWidth = Utils.dp2px(4)
  10. mPaint.strokeCap = Paint.Cap.ROUND
  11. mPaint.strokeJoin = Paint.Join.ROUND
  12. }
  13. @SuppressLint("ClickableViewAccessibility")
  14. override fun onTouchEvent(event: MotionEvent): Boolean {
  15. when (event.actionMasked) {
  16. MotionEvent.ACTION_DOWN,
  17. MotionEvent.ACTION_POINTER_DOWN -> {
  18. val actionIndex = event.actionIndex
  19. val pointerId = event.getPointerId(actionIndex)
  20. val path = Path()
  21. path.moveTo(event.getX(actionIndex), event.getY(actionIndex))
  22. mPaths.append(pointerId, path)
  23. }
  24. MotionEvent.ACTION_MOVE -> {
  25. for (i in 0 until event.pointerCount) {
  26. val path = mPaths.get(event.getPointerId(i))
  27. path.lineTo(event.getX(i), event.getY(i))
  28. }
  29. invalidate()
  30. }
  31. MotionEvent.ACTION_UP,
  32. MotionEvent.ACTION_POINTER_UP -> {
  33. //抬起手指时删除绘制
  34. val pointerId = event.getPointerId(event.actionIndex)
  35. mPaths.remove(pointerId)
  36. invalidate()
  37. }
  38. }
  39. return true
  40. }
  41. override fun onDraw(canvas: Canvas) {
  42. for (i in 0 until mPaths.size()) {
  43. canvas.drawPath(mPaths.valueAt(i), mPaint)
  44. }
  45. }
  46. }
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/253665
推荐阅读
相关标签
  

闽ICP备14008679号