单点触控实现
实现一个触摸移动图片的View
- onTouchEvent监听move和down事件
- 移动时重新绘制
class MultiTouchView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
private val bitmap = getAvatar(resources,200.dp.toInt())
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private var originalX = 0f
private var originalY = 0f
private var offsetX = 0f
private var offsetY = 0f
private var downX = 0f
private var downY = 0f
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawBitmap(bitmap,offsetX,offsetY,paint)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.actionMasked) {
ACTION_DOWN ->{
downX = event.x
downY = event.y
originalX = offsetX
originalY = offsetY
}
ACTION_MOVE -> {
offsetX = event.x - downX + originalX
offsetY = event.y - downY + originalY
invalidate()
}
}
return true
}
}
现在我们先用一个手指移动图片不松开,然后另外一个手指点击图片,松开第一个手指,发现图片发生了位移,图片会移动到我们第二个手指的触摸位置,这是因为我们还没有处理多点触控事件,下面我们要处理多点触控事件,先来看下概念
触摸事件的结构
- 触摸事件是按序列来分组的,每一组事件必然以 ACTION_DOWN 开头,以 ACTION_UP 或 ACTION_CANCEL 结束。
- ACTION_POINTER_DOWN 和 ACTION_POINTER_UP 和 ACTION_MOVE 一样, 只是事件序列中的组成部分,并不会单独分出新的事件序列
- 触摸事件序列是针对 View 的,而不是针对 pointer 的。「某个 pointer 的事 件」这种说法是不正确的。
- 在一个触摸事件里,每个 Pointer 除了 x 和 y 之外,还有 index 和 id。
- 「移动的那个手指」这个概念是伪概念,「寻找移动的那个手指」这个需求是个 伪需求。
多点触控事件序列
ACTION_DOWN p(x, y, index, id)
ACTION MOVE p(x, y, index, id)
ACTION_MOVE p(x, y, index, id)
ACTION_MOVE p(x, y, index, id)
ACTION_POINTER_DOWN p(x, y, index, id) p(x, y, index, id)
ACTION_MOVE p(x, y, index, id)p(x, y, index, id)
ACTION_MOVE p(x, y, index, id)p(x, y, index, id)
ACTION POINTER_DOWN
ACTION MOVE
ACTION POINTER_UP
ACTION MOVE
ACTION POINTER_UP
ACTION MOVE
ACTION_UP
MotionEvent.getActionMasked()
- ACTION_DOWN 第一个手指按下(之前没有任何手指触摸到 View)
- ACTION_UP 最后一个手指抬起(抬起之后没有任何手指触摸到 View,这个手指未必是 ACTION_DOWN 的那个手指)
- ACTION_MOVE 有手指发生移动
- ACTION_POINTER_DOWN 额外手指按下(按下之前已经有别的手指触摸到View)
- ACTION_POINTER_UP 有手指抬起,但不是最后一个(抬起之后,仍然还有别的手指在触摸着 View)
MotionEvent.getActionIndex(index)
获取「非第一根按下手指」或「非最后一根抬起手指」的 index
多点触控的三种类型
- 接力型
同一时刻只有一个 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 对它们进行跟踪。
接力型
- 比如图片多点触控,同一时间只有一个手指的触摸事件生效
- 除了ACTION_DOWN和ACTION_MOVE,还要处理ACTION_POINTER_DOWN和ACTION_POINTER_UP事件
- 完整代码如下:
class MultiTouchView2(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
private val bitmap = getAvatar(resources,200.dp.toInt())
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private var originalX = 0f
private var originalY = 0f
private var offsetX = 0f
private var offsetY = 0f
private var downX = 0f
private var downY = 0f
private var trackingPointerId = 0//唯一手指id
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawBitmap(bitmap,offsetX,offsetY,paint)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.actionMasked) {
ACTION_DOWN -> {
trackingPointerId = event.getPointerId(0)
downX = event.x
downY = event.y
originalX = offsetX
originalY = offsetY
}
ACTION_POINTER_DOWN -> {//多指触控 按下
val actionIndex = event.actionIndex
trackingPointerId = event.getPointerId(actionIndex)
downX = event.getX(actionIndex)
downY = event.getY(actionIndex)
originalX = offsetX
originalY = offsetY
}
ACTION_POINTER_UP -> {//多指触控 抬起
val actionIndex = event.actionIndex
val pointerId = event.getPointerId(actionIndex)
if (pointerId == trackingPointerId){
var newIndex = -1 ;
if (actionIndex == event.pointerCount - 1 ){//表示这是最后一个触控的手指
newIndex = event.pointerCount - 2
}else{
newIndex = event.pointerCount - 1
}
trackingPointerId = event.getPointerId(newIndex)
downX = event.getX(newIndex)
downY = event.getY(newIndex)
originalX = offsetX
originalY = offsetY
}
}
ACTION_MOVE -> {
val index = event.findPointerIndex(trackingPointerId)
offsetX = event.getX(index) - downX + originalX
offsetY = event.getY(index) - downY + originalY
invalidate()
}
}
return true
}
}
协作型
- 比如图片两个多点触摸
- 一个手指移动,另外一个不移动,半速移动
- 两个手指同一方向移动,全速移动
- 两个手指反向移动,移动快的方向差值半速移动
- 使用所有 pointer 的坐标来共同更新焦点坐标
- 完整代码如下:
/** 多点触控移动图片 协作型
* @author dongshuhuan
* date 2020/12/22
* version
*/
class MultiTouchView3(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
private val bitmap = getAvatar(resources,200.dp.toInt())
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private var originalX = 0f
private var originalY = 0f
private var offsetX = 0f
private var offsetY = 0f
private var downX = 0f
private var downY = 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 == ACTION_POINTER_UP
for (i in 0 until pointerCount){
if (!(isPointerUp && i == event.actionIndex)){
sumX += event.getX(i)
sumY += event.getY(i)
}
}
if (isPointerUp){
pointerCount--
}
focusX = sumX/pointerCount
focusY = sumY/pointerCount
when (event.actionMasked) {
ACTION_DOWN,ACTION_POINTER_DOWN, ACTION_POINTER_UP -> {
downX = focusX
downY = focusY
originalX = offsetX
originalY = offsetY
}
ACTION_MOVE -> {
offsetX = focusX - downX + originalX
offsetY = focusY - downY + originalY
invalidate()
}
}
return true
}
}
各自为战型
- 比如涂鸦画板
单点涂鸦绘制
- 监听ACTION_DOWN和ACTION_MOVE
- 通过path.moveTo和path.lineTo记录path变化
- 然后invalidate()生效
class MultiTouchView4(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
private val paint= Paint(Paint.ANTI_ALIAS_FLAG)
private var path = Path()
init {
paint.style = Paint.Style.STROKE
paint.strokeWidth = 4.dp
paint.strokeCap = Paint.Cap.ROUND
paint.strokeJoin = Paint.Join.ROUND
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawPath(path,paint)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
path.moveTo(event.x,event.y)
invalidate()
}
MotionEvent.ACTION_MOVE -> {
path.lineTo(event.x,event.y)
invalidate()
}
MotionEvent.ACTION_UP -> {
path.reset()
invalidate()
}
}
return true
}
}
多点涂鸦绘制
- 首先用SparseArray记录每个手指的轨迹
- 然后在onTouch中记录按下移动抬起等事件
- 最后在onDraw中重绘
- 效果如图
-
- 完整代码如下
/** 多点触控 互不干扰型
* 例:涂鸦画板
* @author dongshuhuan
* date 2020/12/22
* version
*/
class MultiTouchView4(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
private val paint= Paint(Paint.ANTI_ALIAS_FLAG)
private var paths = SparseArray<Path>()
init {
paint.style = Paint.Style.STROKE
paint.strokeWidth = 4.dp
paint.strokeCap = Paint.Cap.ROUND
paint.strokeJoin = Paint.Join.ROUND
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
for (i in 0 until paths.size()){
val path = paths.valueAt(i)
canvas.drawPath(path,paint)
}
}
override fun onTouchEvent(event: MotionEvent): Boolean {
//需要捕获异常,否则会报出java.lang.IllegalArgumentException: pointerIndex out of range异常,这是一个谷歌官方bug,至今未修复
try {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN,MotionEvent.ACTION_POINTER_DOWN -> {
val actionIndex = event.actionIndex
val path = Path()
path.moveTo(event.getX(actionIndex),event.getY(actionIndex))
paths.append(event.getPointerId(actionIndex),path)
invalidate()
}
MotionEvent.ACTION_MOVE -> {
for (i in 0 until paths.size()){
val pointerId = event.getPointerId(i)
val path = paths.get(pointerId)
path.lineTo(event.getX(i),event.getY(i))
}
invalidate()
}
MotionEvent.ACTION_UP,MotionEvent.ACTION_POINTER_UP -> {
val actionIndex = event.actionIndex
val pointerId = event.getPointerId(actionIndex)
paths.remove(pointerId)
invalidate()
}
}
}catch (e:IllegalArgumentException){
e.printStackTrace()
}
return true
}
}