本篇为系列文章
一个跟随手指滑动的图片 – 单点触摸
自定义 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
}
}
多指的中心点,也就是焦点怎么算呢?
举例,两个手指的中心点就是两个两个点的横坐标相加/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
......
}
以上就是计算一个焦点的过程,可以发现,焦点计算出来之后,本质是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()
}
}
}
以上主要体现的是获取焦点替换成虚拟焦点
那么还用处理多指吗?如果不处理的话,在有新的手指按下的时候,会瞬间改变焦点的位置,会导致图片瞬间移动,所以说我们还是需要处理多指的
但是这里的处理就不需要那么复杂了,不管是第几根手指按下还是抬起,都只需要做相同的事情,也是之前单点触摸的逻辑:重置焦点坐标
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
}
我们运行以上代码测试:
我明明处理了多指的抬起啊,为什么还会出现问题呢,其实问题出现在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
}
测试
好了,我们协作类型的到此讲解完毕,我们贴一下全部代码
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
}
}