-1

We have the red dot in (100; 100) coordinates. If we click to red dot after dragging - it will save it's (100; 100) coordinates. But if we scale in or out it will have coordinates completely different from (100; 100).

How to calculate x and y correctly after scaling?

class CanvasView(context: Context, attrs: AttributeSet?) : View(context, attrs) {

    companion object {
        private const val INVALID_POINTER_ID = -1
    }

    private var posX: Float = 0f
    private var posY: Float = 0f

    private var lastTouchX: Float = 0f
    private var lastTouchY: Float = 0f
    private var activePointerId = INVALID_POINTER_ID

    private val scaleDetector: ScaleGestureDetector
    private var scaleFactor = 1f

    private var prevMotionType = MotionEvent.ACTION_DOWN
    private var prevX = 0f
    private var prevY = 0f

    private val paint: Paint = Paint()

    constructor(mContext: Context) : this(mContext, null)

    init {
        scaleDetector = ScaleGestureDetector(context, ScaleListener())

        paint.strokeWidth = 1f
        paint.color = Color.RED
    }

    public override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        canvas.save()
        canvas.scale(scaleFactor, scaleFactor, pivotX, pivotY)
        canvas.translate(posX, posY)
        canvas.drawCircle(100f, 100f, 10f, paint)
        canvas.restore()
    }

    override fun onTouchEvent(ev: MotionEvent): Boolean {
        scaleDetector.onTouchEvent(ev)

        val action = ev.action
        when (action and MotionEvent.ACTION_MASK) {
            MotionEvent.ACTION_DOWN -> {
                val x = ev.x
                val y = ev.y

                lastTouchX = x
                lastTouchY = y
                activePointerId = ev.getPointerId(0)

                calculateIfClicked(ev)
            }

            MotionEvent.ACTION_MOVE -> {
                val pointerIndex = ev.findPointerIndex(activePointerId)
                val x = ev.getX(pointerIndex)
                val y = ev.getY(pointerIndex)

                if (!scaleDetector.isInProgress) {
                    val dx = x - lastTouchX
                    val dy = y - lastTouchY

                    posX += dx / scaleFactor
                    posY += dy / scaleFactor

                    invalidate()
                }

                lastTouchX = x
                lastTouchY = y

                calculateIfClicked(ev)
            }

            MotionEvent.ACTION_UP -> {
                activePointerId = INVALID_POINTER_ID

                calculateIfClicked(ev)
            }

            MotionEvent.ACTION_CANCEL -> {
                activePointerId = INVALID_POINTER_ID
            }

            MotionEvent.ACTION_POINTER_UP -> {
                val pointerIndex =
                    ev.action and MotionEvent.ACTION_POINTER_INDEX_MASK shr MotionEvent.ACTION_POINTER_INDEX_SHIFT
                val pointerId = ev.getPointerId(pointerIndex)
                if (pointerId == activePointerId) {
                    val newPointerIndex = if (pointerIndex == 0) 1 else 0
                    lastTouchX = ev.getX(newPointerIndex)
                    lastTouchY = ev.getY(newPointerIndex)
                    activePointerId = ev.getPointerId(newPointerIndex)
                }
            }
        }

        return true
    }

    private fun calculateIfClicked(ev: MotionEvent) {
        when (ev.action) {
            MotionEvent.ACTION_DOWN -> {
                prevMotionType = MotionEvent.ACTION_DOWN
                prevX = ev.x
                prevY = ev.y
            }

            MotionEvent.ACTION_MOVE -> prevMotionType = MotionEvent.ACTION_MOVE

            MotionEvent.ACTION_UP -> {
                val delta = Math.max(
                    Math.abs(Math.abs(ev.x) - Math.abs(prevX)),
                    Math.abs(Math.abs(ev.y) - Math.abs(prevY))
                )

                if (prevMotionType == MotionEvent.ACTION_DOWN ||
                    (prevMotionType == MotionEvent.ACTION_MOVE && delta < 5)
                ) {
                    val x = ev.x - posX * scaleFactor
                    val y = ev.y - posY * scaleFactor

                    Log.d("abcd", "x: $x, y: $y")
                }
            }
        }
    }

    private inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {
        override fun onScale(detector: ScaleGestureDetector): Boolean {
            scaleFactor *= detector.scaleFactor
            scaleFactor = Math.max(0.3f, Math.min(scaleFactor, 10.0f))

            invalidate()
            return true
        }
    }
}

x and y coordinates are wrong after scaling. They keep their position, but it's not that expected.

badadin
  • 507
  • 1
  • 5
  • 18
  • why do you want to `canvas.scale` and `canvas.translate`? cannot you simply `canvas.drawCircle(canterX, canterY, scaledRadius, paint)`? – pskink Nov 10 '18 at 07:16
  • Because the red dot is just an example. I'm going to draw multiple objects, not only one red dot. And moving objects instead of viewport will break everything! I need to save object coordinates to correctly handle click events on them after drag/scale. – badadin Nov 10 '18 at 08:00
  • then use `Matrix` API and `Canvas#concat` method – pskink Nov 10 '18 at 08:06
  • I thought about it, but I did not find an example with drag/zoom/click. If you show me an example, I will very appreciate. – badadin Nov 10 '18 at 08:11
  • And what's wrong with canvas.scale and canvas.translate? Why do you recommend not tot use them? – badadin Nov 10 '18 at 08:28
  • Add the code directly into the question – Zoe Nov 10 '18 at 13:06
  • see https://stackoverflow.com/a/21657145/2252830 – pskink Nov 10 '18 at 14:28
  • **Look at this answer. It worked for me.** https://stackoverflow.com/a/26400401/8207343 – HYA Nov 10 '18 at 19:23
  • so do you know why `Matrix` API is better in such cases? – pskink Nov 11 '18 at 05:22
  • @pskink, no i don't know. So tell me please, why Matrix API is better in such cases. – badadin Nov 11 '18 at 08:27
  • did you see the code I posted? did you run it? – pskink Nov 11 '18 at 08:31
  • Yes, I've ran it, but in this example we can drag and scale drawables like single objects, but I need to drag and scale viewport, not objects. – badadin Nov 11 '18 at 08:35
  • it is exactly the same: instead of 3 layers just use one layer that draws multiple circles, rectangles, whatever – pskink Nov 11 '18 at 09:14
  • Thanks! Even so I used matrix in my code, but I've tooken solution from https://stackoverflow.com/a/42665104/6367262 – badadin Nov 11 '18 at 09:24
  • no, this is wrong, you dont have to use those `values[Matrix.*]` constants and that scaling math, just use `Matrix#mapPoints()` method - see https://pastebin.com/raw/9y3ymVpB – pskink Nov 11 '18 at 10:19
  • sure, you're welcome – pskink Nov 11 '18 at 11:39
  • @pskink, I finally got to my PC and tried to build your example. What is MatrixGestureDetector? And where to get it? – badadin Nov 11 '18 at 19:14
  • see the first link I gave you – pskink Nov 11 '18 at 19:23

1 Answers1

0

At first I decided to use this solution and it worked as expected. But then I used solution that pskink advised me to use. It simpler, shorter and rather more correct then my previous choice. Example:

interface OnMatrixChangeListener {
    fun onChange(theMatrix: Matrix)
}

class ChartView2(context: Context, attrs: AttributeSet?) : View(context, attrs), OnMatrixChangeListener {

    private var theMatrix = Matrix()
    private var detector = MatrixGestureDetector(theMatrix, this)
    private var paint = Paint()
    private var colors = intArrayOf(Color.RED, Color.GREEN, Color.BLUE)
    private var colorNames = arrayOf("RED", "GREEN", "BLUE")
    private var centers = arrayOf(PointF(100f, 100f), PointF(400f, 100f), PointF(250f, 360f))

    var inverse = Matrix()
    override fun onTouchEvent(event: MotionEvent): Boolean {
        if (event.action == MotionEvent.ACTION_DOWN) {
            theMatrix.invert(inverse)
            val pts = floatArrayOf(event.x, event.y)
            inverse.mapPoints(pts)
            for (i in colors.indices) {
                if (Math.hypot((pts[0] - centers[i].x).toDouble(), (pts[1] - centers[i].y).toDouble()) < 100)
                    Log.d("abcd", colorNames[i] + " circle clicked")
            }
        }
        detector.onTouchEvent(event)
        return true
    }

    override fun onDraw(canvas: Canvas) {
        canvas.concat(theMatrix)
        for (i in colors.indices) {
            paint.color = colors[i]
            canvas.drawCircle(centers[i].x, centers[i].y, 100f, paint)
        }
    }

    override fun onChange(theMatrix: Matrix) {
        invalidate()
    }
}

internal class MatrixGestureDetector(private val mMatrix: Matrix, listener: OnMatrixChangeListener) {

    private var ptpIdx = 0
    private val mTempMatrix = Matrix()
    private val mListener: OnMatrixChangeListener?
    private val mSrc = FloatArray(4)
    private val mDst = FloatArray(4)
    private var mCount: Int = 0

    init {
        this.mListener = listener
    }

    fun onTouchEvent(event: MotionEvent) {
        if (event.pointerCount > 2) {
            return
        }

        val action = event.actionMasked
        val index = event.actionIndex

        when (action) {
            MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> {
                val idx = index * 2
                mSrc[idx] = event.getX(index)
                mSrc[idx + 1] = event.getY(index)
                mCount++
                ptpIdx = 0
            }

            MotionEvent.ACTION_MOVE -> {
                for (i in 0 until mCount) {
                    val idx = ptpIdx + i * 2
                    mDst[idx] = event.getX(i)
                    mDst[idx + 1] = event.getY(i)
                }
                mTempMatrix.setPolyToPoly(mSrc, ptpIdx, mDst, ptpIdx, mCount)
                mMatrix.postConcat(mTempMatrix)
                mListener?.onChange(mMatrix)
                System.arraycopy(mDst, 0, mSrc, 0, mDst.size)
            }

            MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> {
                if (event.getPointerId(index) == 0) ptpIdx = 2
                mCount--
            }
        }
    }
}
badadin
  • 507
  • 1
  • 5
  • 18