Multi touch Android custom controls

Posted by paradigmapc on Thu, 07 Oct 2021 00:05:47 +0200

preface

In the previous blog, we made a picture view that can move with our fingers. It can only support single finger control. If we touch with multiple fingers, it will be all right. We want the effect
There are three types of multi touch
① : baton type or preemptive type
② : Collaborative
③ : non-interference type

1, Baton type or preemptive type

thinking

① : confirm a touch point as a special touch point for controlling the picture
② : when moving, the contact with picture control is the only contact
③ : if a second finger is pressed, control is transferred from the first finger to the second finger
④ : if there are multiple fingers and one of them is lifted, then if the lifted finger has control over the picture, the control will be exchanged

code

1: Action_ When down, when only one finger is pressed, the image control right is assigned to the only finger with index 0

MotionEvent.ACTION_DOWN -> {
    leadingPointerId = event.getPointerId(0)
    downX = event.x
    downY = event.y
    originOffsetX = offsetX
    originOffsetY = offsetY
    }

2: Action_ POINTER_ When down, give control of the picture to the new finger

//Relay, give the control of the picture to the finger just clicked
MotionEvent.ACTION_POINTER_DOWN -> {
    val actionIndex = event.actionIndex
    leadingPointerId = event.getPointerId(actionIndex)
    downX = event.getX(actionIndex)
    downY = event.getY(actionIndex)
    originOffsetX = offsetX
    originOffsetY = offsetY
}

3: A finger is raised

//Relay, if the removed finger is the finger that has control, give control to the other fingers
MotionEvent.ACTION_POINTER_UP -> {
    val isLeadingPointer = event.findPointerIndex(leadingPointerId) == event.actionIndex
    if (isLeadingPointer) {
        val newIndex =
            if (event.findPointerIndex(leadingPointerId) == event.pointerCount - 1) {
                event.pointerCount - 2
            } else {
                event.pointerCount - 1
            }
        leadingPointerId = event.getPointerId(newIndex)
        downX = event.getX(newIndex)
        downY = event.getY(newIndex)
        originOffsetX = offsetX
        originOffsetY = offsetY
    }
}

1: If the raised finger has no control over the picture, it will not be processed
2: If the raised finger has control of the picture
① : if the raised finger is the last one, give control to the penultimate one
② : otherwise, give it to the penultimate one
3: The values downX, originOffsetY are updated only during relay

Complete code

package com.lbj23.customview.customview

import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import com.lbj23.customview.R
import com.lbj23.customview.dp
import com.lbj23.customview.getAvatar

class MultiTouchView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
    private val image = getAvatar(resources, R.drawable.bg6, 85.dp.toInt())
    private var offsetX = 0f
    private var offsetY = 0f
    private var originOffsetX = 0f
    private var originOffsetY = 0f
    private var downX = 0f
    private var downY = 0f
    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
    private var leadingPointerId = 0
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        canvas.drawBitmap(image, offsetX, offsetY, paint)
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.actionMasked) {
            MotionEvent.ACTION_DOWN -> {
                leadingPointerId = event.getPointerId(0)
                downX = event.x
                downY = event.y
                originOffsetX = offsetX
                originOffsetY = offsetY
            }
            //Relay, if the removed finger is the finger that has control, give control to the other fingers
            MotionEvent.ACTION_POINTER_UP -> {
                val isLeadingPointer = event.findPointerIndex(leadingPointerId) == event.actionIndex
                if (isLeadingPointer) {
                    val newIndex =
                        if (event.findPointerIndex(leadingPointerId) == event.pointerCount - 1) {
                            event.pointerCount - 2
                        } else {
                            event.pointerCount - 1
                        }
                    leadingPointerId = event.getPointerId(newIndex)
                    downX = event.getX(newIndex)
                    downY = event.getY(newIndex)
                    originOffsetX = offsetX
                    originOffsetY = offsetY
                }
            }
            //Relay, give the control of the picture to the finger just clicked
            MotionEvent.ACTION_POINTER_DOWN -> {
                val actionIndex = event.actionIndex
                leadingPointerId = event.getPointerId(actionIndex)
                downX = event.getX(actionIndex)
                downY = event.getY(actionIndex)
                originOffsetX = offsetX
                originOffsetY = offsetY
            }
            MotionEvent.ACTION_MOVE -> {
                val index = event.findPointerIndex(leadingPointerId)
                offsetX = event.getX(index) - downX + originOffsetX
                offsetY = event.getY(index) - downY + originOffsetY
                invalidate()
            }
        }
        return true
    }
}

Effect display


Obviously, only one finger has control over the picture

2, Collaborative

thinking

① : when one finger, the picture is controlled by only one finger
② : when multiple fingers are pressed on the screen at the same time, the geometric center of multiple fingers is used as a virtual finger to control the picture

code

1: In onTouchEvent, before judging various events, first find the geometric center and find the X and Y coordinates of the virtual finger with picture control

        var sumX = 0f
        var sumY = 0f
        val isPointerUP = event.actionMasked == MotionEvent.ACTION_POINTER_UP
        for (index in 0 until event.pointerCount) {
            //If you are lifting, you need to do the operation in advance. At this time, the pointerCount still does not subtract the number of points raised, so update the position in advance
            if (!(isPointerUP && index == event.actionIndex)) {
                sumX += event.getX(index)
                sumY += event.getY(index)
            }
        }
        var pointerCount = event.pointerCount
        if (isPointerUP) {
            pointerCount--
        }
        //Set a geometric center
        val focusX = sumX / pointerCount
        val focusY = sumY / pointerCount

① : use sumX and sumY to record the total coordinates by traversing all fingers
② : divide sumX and sumY by the number of fingers at the same time to get the position of the geometric center
③ : if event.actionMasked is MotionEvent.ACTION_POINTER_UP, because all fingers are traversed at this time, the raised finger is still there. There will be problems when using this geometric center in subsequent actions, so if it is MotionEvent.ACTION_POINTER_UP, then peel the raised finger from the calculation of the geometric center

2: If you click and lift, update the click position and the original position

MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_UP, MotionEvent.ACTION_POINTER_DOWN -> {
    downX = focusX
    downY = focusY
    originOffsetX = offsetX
    originOffsetY = offsetY
}

3: If it is moving, change the image offset and use the position of the geometric center

MotionEvent.ACTION_MOVE -> {
    offsetX = focusX - downX + originOffsetX
    offsetY = focusY - downY + originOffsetY
    invalidate()
}

Effect display


It is observed that:
① : when two fingers slide, the picture drag speed is faster
② : one finger is fixed and the other finger slides. The picture drag speed is slow, which is half the speed of the moving finger

Complete code

package com.lbj23.customview.customview

import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import com.lbj23.customview.R
import com.lbj23.customview.dp
import com.lbj23.customview.getAvatar

class MultiTouchView2(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
    private val image = getAvatar(resources, R.drawable.bg6, 150.dp.toInt())
    private var offsetX = 0f
    private var offsetY = 0f
    private var originOffsetX = 0f
    private var originOffsetY = 0f
    private var downX = 0f
    private var downY = 0f
    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        canvas.drawBitmap(image, offsetX, offsetY, paint)
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        var sumX = 0f
        var sumY = 0f
        val isPointerUP = event.actionMasked == MotionEvent.ACTION_POINTER_UP
        for (index in 0 until event.pointerCount) {
            //If you are lifting, you need to do the operation in advance. At this time, the pointerCount still does not subtract the number of points raised, so update the position in advance
            if (!(isPointerUP && index == event.actionIndex)) {
                sumX += event.getX(index)
                sumY += event.getY(index)
            }
        }
        var pointerCount = event.pointerCount
        if (isPointerUP) {
            pointerCount--
        }
        //Set a geometric center
        val focusX = sumX / pointerCount
        val focusY = sumY / pointerCount
        when (event.actionMasked) {
            //If you click and lift, update the click position and the original position
            MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_UP, MotionEvent.ACTION_POINTER_DOWN -> {
                downX = focusX
                downY = focusY
                originOffsetX = offsetX
                originOffsetY = offsetY
            }
            //If it is moving, change the position and use the geometric center
            MotionEvent.ACTION_MOVE -> {
                offsetX = focusX - downX + originOffsetX
                offsetY = focusY - downY + originOffsetY
                invalidate()
            }
        }
        return true
    }
}

4: Each fighting his own way, each painting his own way

This type requires us to treat each finger separately
Click Details

Topics: Android kotlin