Single touch implementation

Implement a View that touches and moves pictures

  • OnTouchEvent listens for move and Down events
  • Redraw as you move
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 } }Copy the code

Now we use one finger to move first pictures don’t loosen, then another finger click on the image, loosen the first finger and found that the image displacement happens, images will be moved to our second finger touch location, this is because we have no dealing with a multi-touch events, we have to deal with multi-touch events below, first look at the concept

Touch the structure of the event

  • Touch events are grouped in sequence, and each group of events must begin with ACTION_DOWN and end with ACTION_UP or ACTION_CANCEL.
  • Like ACTION_POINTER_DOWN and ACTION_POINTER_UP and ACTION_MOVE, ACTION_POINTER_DOWN is only a part of the event sequence and does not separate into a new event sequence
  • The sequence of touch events is for views, not for Pointers. It is not true to say “something pointer happened”.
  • In a touch event, each Pointer has an index and an ID in addition to x and y.
  • The concept of “the moving finger” is a pseudo concept, and the requirement of “find the moving finger” is a pseudo requirement.

Multi-touch event sequence

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
Copy the code

MotionEvent.getActionMasked()

  • ACTION_DOWN First finger pressed (no fingers touched the View before)
  • The last ACTION_UP finger is lifted (after lifting, no finger touches the View, this finger may not be the same as the ACTION_DOWN finger)
  • ACTION_MOVE A finger moves
  • ACTION_POINTER_DOWN Extra finger press (another finger has touched the View before pressing)
  • ACTION_POINTER_UP has fingers raised, but not the last one (there are still other fingers touching the View after lifting)

MotionEvent.getActionIndex(index)

Gets the index of non-first pressed finger or non-last lifted finger

Three types of multi-touch

  • The relay type

Only one pointer is active at a time, the latest pointer. Typical: ListView, RecyclerView. Implementation: Record the latest pointer in ACTION_POINTER_DOWN and ACTION_POINTER_UP, and use this pointer to determine the position in subsequent ACTION_MOVE events.

  • Cooperative/collaborative

All pointer touches to the View work together. Typical :ScaleGestureDetector and the onScroll() method of GestureDetector. Implementation: Use the coordinates of all Pointers in each DOWN, POINTER_DOWN, POINTER_UP, and UP events to update the focus coordinates, and use the coordinates of all Pointers in a MOVE event to determine the position.

  • Each to his own

Each pointer does different things without affecting each other. Typical: A palette that supports multiple brushes should be used. Implementation: Record the ID of each pointer in each DOWN and POINTER_DOWN event, and track them in the MOVE event with the ID.

The relay type

  • For example, for image multi-touch, only one finger’s touch events work at a time
  • In addition to ACTION_DOWN and ACTION_MOVE, ACTION_POINTER_DOWN and ACTION_POINTER_UP events are also handled
  • The complete code is as follows:
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 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 = Y originalX = offsetX originalY = offsetY} ACTION_POINTER_DOWN -> {// Multi-touch val actionIndex = event.actionIndex  trackingPointerId = event.getPointerId(actionIndex) downX = event.getX(actionIndex) downY = event.getY(actionIndex) OriginalX = offsetX originalY = offsetY} ACTION_POINTER_UP -> {actionIndex = event.actionIndex val pointerId = event.getPointerId(actionIndex) if (pointerId == trackingPointerId){ var newIndex = -1 ; If (actionIndex == event.pointercount-1){// Indicates that this is the last finger to touch 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 } }Copy the code

collaborative

  • Two multitouch images, for example
    • One finger moves, the other finger doesn’t move, half speed
    • Move both fingers in the same direction, full speed
    • The two fingers move in opposite directions, and the difference in the direction of fast movement is half speed
  • Update the focus coordinates together with the coordinates of all Pointers
  • The complete code is as follows:
* @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 } }Copy the code

Each to his own

  • Like doodle board

Single point graffiti drawing

  • Listen for ACTION_DOWN and ACTION_MOVE
  • Path changes are recorded using path.moveTo and path.lineTo
  • Then invalidate() takes effect
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 } }Copy the code

Multipoint graffiti drawing

  • Start by recording the trajectory of each finger with SparseArray
  • Then record events such as press move lift in onTouch
  • Finally redraw in onDraw
  • The effect is shown in figure
  • The complete code is as follows
/** Multitouch non-interference * example: Doodlepad * @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) }} to override fun onTouchEvent (event: MotionEvent) : Boolean {/ / need to catch exceptions, otherwise you will quote us the Java. Lang. IllegalArgumentException: PointerIndex out of range exception, which is an official Google 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 } }Copy the code