How to draw several pins at zoomable and draggable image and avoid freezes when zooming and dragging?

  android, drawing, imageview

I want to add to the image several pins at once. This image can zoom and scroll. But when I’m adding many pins (10 and more), the image starting to freezing when I zooming or scrolling it.
I’m adding new pin in "onLongPress":

data class CoordinatesEntity(var x: Float, var y: Float)

class ScaleImageView : AppCompatImageView, View.OnTouchListener {
...

private val pinsCoordinates = ArrayList<CoordinatesEntity>()
private var invertedPinsCoordinates = ArrayList<CoordinatesEntity>()
private val pinCountersMap = HashMap<String, String>()

...

mDetector = GestureDetector(savedContext,
                object : GestureDetector.SimpleOnGestureListener() {
                    override fun onDoubleTap(e: MotionEvent): Boolean {
                        ...
                    }

                    override fun onLongPress(e: MotionEvent) {
                        super.onLongPress(e)
                        pinsCoordinates.add(CoordinatesEntity(e.x, e.y))

                        val touchCoords = floatArrayOf(e.x, e.y)
                        val matrixInverse = Matrix()
                        mMatrix!!.invert(matrixInverse) // XY to UV mapping matrix.
                        matrixInverse.mapPoints(touchCoords) // Touch point in bitmap-U,V coords.

                        val entity = CoordinatesEntity(touchCoords[0], touchCoords[1])
                        invertedPinsCoordinates.add(entity)

                        pinCountersMap["${entity.x}${entity.y}"] = (++pinCounter).toString()

                        invalidate()
                    }
                })

...
}

and drawing all of my pins in "onDraw". My pin – it’s image plus text, that I adding programmatically to bitmap with pin:

class ScaleImageView : AppCompatImageView, View.OnTouchListener {

...

private fun getPinForCoordinates(coordinates: CoordinatesEntity): Bitmap {
    val bm: Bitmap = BitmapFactory.decodeResource(
            context.resources, R.drawable.ic_pin_raster_no_middle_32
    ).copy(Bitmap.Config.ARGB_8888, true)
    val paint = Paint()
    paint.style = Paint.Style.FILL
    paint.color = Color.parseColor("#3874A4")
    paint.textSize = 20f

    val text = pinCountersMap["${coordinates.x}${coordinates.y}"] ?: ""
    val canvas = Canvas(bm)
    val xStart = when (text.length) {
        1 -> bm.width / 2f - 6
        2 -> bm.width / 2f - 12
        else -> bm.width / 2f - 36
    }
    canvas.drawText(text, xStart, bm.height / 2f, paint)

    return BitmapDrawable(context.resources, bm).bitmap
}

override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        invertedPinsCoordinates.forEach {
            val marker = getPinForCoordinates(it)
            val matrixMarker = Matrix()
            matrixMarker.setTranslate(it.x, it.y)
            matrixMarker.postConcat(mMatrix)
            canvas.drawBitmap(marker, matrixMarker, null)
        }
    }

...
}

I think it’s freezing because of the loop inside of "onDraw". But I don’t know how to draw several pins at once and keep them zoomable and scrollable with image. Please help!

My whole class:

import android.annotation.SuppressLint
import android.content.Context
import android.graphics.*
import android.graphics.drawable.BitmapDrawable
import android.util.AttributeSet
import android.util.Log
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import androidx.appcompat.widget.AppCompatImageView
import com.signalsense.signalsenseapp.R
import com.signalsense.signalsenseapp.interactors.image_interactor.ImageInteractor
import com.signalsense.signalsenseapp.interactors.image_interactor.ImageInteractorImpl
import com.signalsense.signalsenseapp.mvp.models.entities.CoordinatesEntity
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sqrt

class ScaleImageView : AppCompatImageView, View.OnTouchListener {
    private val dirtyRect = RectF()
    private val savedContext: Context
    private val maxScale = 2f
    private val matrixValues = FloatArray(9)
    private var lastTouchX = 0f
    private var lastTouchY = 0f
    private var paint = Paint()
    var tag = "ScaleImageView"

    // display width height.
    private var mWidth = 0
    private var mHeight = 0
    private var mIntrinsicWidth = 0
    private var mIntrinsicHeight = 0
    private var mScale = 0f
    private var mMinScale = 1f
    private var mPrevDistance = 0f
    private var isScaling = false
    private var mPrevMoveX = 0
    private var mPrevMoveY = 0
    private var mDetector: GestureDetector? = null
    private val pinsCoordinates = ArrayList<CoordinatesEntity>()
    private var invertedPinsCoordinates = ArrayList<CoordinatesEntity>()
    private var pinCounter = 0
    private val pinCountersMap = HashMap<String, String>()

    constructor(context: Context, attr: AttributeSet?) : super(context, attr) {
        savedContext = context
        initialize()
    }

    constructor(context: Context) : super(context) {
        savedContext = context
        initialize()
    }

    private fun resetDirtyRect(eventX: Float, eventY: Float) {
        dirtyRect.left = min(lastTouchX, eventX)
        dirtyRect.right = max(lastTouchX, eventX)
        dirtyRect.top = min(lastTouchY, eventY)
        dirtyRect.bottom = max(lastTouchY, eventY)
    }

    override fun setImageBitmap(bm: Bitmap) {
        super.setImageBitmap(bm)
        initialize()
    }

    override fun setImageResource(resId: Int) {
        super.setImageResource(resId)
        initialize()
    }

    private fun initialize() {
        this.scaleType = ScaleType.MATRIX
        mMatrix = Matrix()
        val d = drawable
        paint.isAntiAlias = true
        paint.color = Color.RED
        paint.style = Paint.Style.STROKE
        paint.strokeJoin = Paint.Join.ROUND
        paint.strokeWidth = STROKE_WIDTH
        if (d != null) {
            mIntrinsicWidth = d.intrinsicWidth
            mIntrinsicHeight = d.intrinsicHeight
            setOnTouchListener(this)
        }
        mDetector = GestureDetector(savedContext,
                object : GestureDetector.SimpleOnGestureListener() {
                    override fun onDoubleTap(e: MotionEvent): Boolean {
                        maxZoomTo(e.x.toInt(), e.y.toInt())
                        cutting()
                        return super.onDoubleTap(e)
                    }

                    override fun onLongPress(e: MotionEvent) {
                        super.onLongPress(e)
                        pinsCoordinates.add(CoordinatesEntity(e.x, e.y))

                        val touchCoords = floatArrayOf(e.x, e.y)
                        val matrixInverse = Matrix()
                        mMatrix!!.invert(matrixInverse) // XY to UV mapping matrix.
                        matrixInverse.mapPoints(touchCoords) // Touch point in bitmap-U,V coords.

                        val entity = CoordinatesEntity(touchCoords[0], touchCoords[1])
                        invertedPinsCoordinates.add(entity)

                        pinCountersMap["${entity.x}${entity.y}"] = (++pinCounter).toString()

                        invalidate()
                    }
                })
    }

    override fun setFrame(l: Int, t: Int, r: Int, b: Int): Boolean {
        mWidth = r - l
        mHeight = b - t
        mMatrix!!.reset()
        val rNorm = r - l
        mScale = rNorm.toFloat() / mIntrinsicWidth.toFloat()
        var paddingHeight = 0
        var paddingWidth = 0
        // scaling vertical
        if (mScale * mIntrinsicHeight > mHeight) {
            mScale = mHeight.toFloat() / mIntrinsicHeight.toFloat()
            mMatrix!!.postScale(mScale, mScale)
            paddingWidth = (r - mWidth) / 2
            paddingHeight = 0
            // scaling horizontal
        } else {
            mMatrix!!.postScale(mScale, mScale)
            paddingHeight = (b - mHeight) / 2
            paddingWidth = 0
        }
        mMatrix!!.postTranslate(paddingWidth.toFloat(), paddingHeight.toFloat())
        imageMatrix = mMatrix
        mMinScale = mScale
        Log.i(tag, "MinScale: $mMinScale")
        zoomTo(mScale, mWidth / 2, mHeight / 2)
        cutting()
        return super.setFrame(l, t, r, b)
    }

    private fun getValue(matrix: Matrix?, whichValue: Int): Float {
        matrix!!.getValues(matrixValues)
        return matrixValues[whichValue]
    }

    private val scale: Float
        get() = getValue(mMatrix, Matrix.MSCALE_X)
    private val translateX: Float
        get() = getValue(mMatrix, Matrix.MTRANS_X)
    private val translateY: Float
        get() = getValue(mMatrix, Matrix.MTRANS_Y)

    private fun maxZoomTo(x: Int, y: Int) {
        if (mMinScale != getCalculatedScale() && getCalculatedScale() - mMinScale > 0.1f) {
            // threshold 0.1f
            val scale = mMinScale / getCalculatedScale()
            zoomTo(scale, x, y)
        } else {
            val scale = maxScale / getCalculatedScale()
            zoomTo(scale, x, y)
        }
    }

    private fun zoomTo(scale: Float, x: Int, y: Int) {
        if (getCalculatedScale() * scale < mMinScale) {
            return
        }
        if (scale >= 1 && getCalculatedScale() * scale > maxScale) {
            return
        }
        Log.i(tag, "Scale: $scale, multiplied: ${scale * scale}")
        mMatrix!!.postScale(scale, scale)
        // move to center
        mMatrix!!.postTranslate(-(mWidth * scale - mWidth) / 2,
                -(mHeight * scale - mHeight) / 2)

        // move x and y distance
        mMatrix!!.postTranslate(-(x - mWidth / 2) * scale, 0f)
        mMatrix!!.postTranslate(0f, -(y - mHeight / 2) * scale)
        imageMatrix = mMatrix
    }

    private fun cutting() {
        val width = (mIntrinsicWidth * getCalculatedScale()).toInt()
        val height = (mIntrinsicHeight * getCalculatedScale()).toInt()
        imageWidth = width
        imageHeight = height
        if (translateX < -(width - mWidth)) {
            mMatrix!!.postTranslate(-(translateX + width - mWidth), 0f)
        }
        if (translateX > 0) {
            mMatrix!!.postTranslate(-translateX, 0f)
        }
        if (translateY < -(height - mHeight)) {
            mMatrix!!.postTranslate(0f, -(translateY + height - mHeight))
        }
        if (translateY > 0) {
            mMatrix!!.postTranslate(0f, -translateY)
        }
        if (width < mWidth) {
            mMatrix!!.postTranslate(((mWidth - width) / 2).toFloat(), 0f)
        }
        if (height < mHeight) {
            mMatrix!!.postTranslate(0f, ((mHeight - height) / 2).toFloat())
        }
        imageMatrix = mMatrix
    }

    private fun distance(x0: Float, x1: Float, y0: Float, y1: Float): Float {
        val x = x0 - x1
        val y = y0 - y1
        return sqrt((x * x + y * y).toDouble()).toFloat()
    }

    private fun dispDistance(): Float {
        return sqrt((mWidth * mWidth + mHeight * mHeight).toDouble()).toFloat()
    }

    fun clear() {
        path.reset()
        invalidate()
    }

    fun save() {
        val returnedBitmap = Bitmap.createBitmap(
                width,
                height,
                Bitmap.Config.ARGB_8888)
        val canvas = Canvas(returnedBitmap)
        draw(canvas)
        setImageBitmap(returnedBitmap)
    }

    @Suppress("DEPRECATION")
    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {
        if (mDetector!!.onTouchEvent(event)) {
            return true
        }
        val touchCount = event.pointerCount
        when (event.action) {
            MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_1_DOWN, MotionEvent.ACTION_POINTER_2_DOWN -> {
                if (touchCount >= 2) {
                    val distance = distance(event.getX(0), event.getX(1),
                            event.getY(0), event.getY(1))
                    mPrevDistance = distance
                    isScaling = true
                } else {
                    mPrevMoveX = event.x.toInt()
                    mPrevMoveY = event.y.toInt()
                }
                if (touchCount >= 2 && isScaling) {
                    val dist = distance(event.getX(0), event.getX(1),
                            event.getY(0), event.getY(1))
                    var scale = (dist - mPrevDistance) / dispDistance()
                    mPrevDistance = dist
                    scale += 1f
                    scale *= scale
                    zoomTo(scale, mWidth / 2, mHeight / 2)
                    cutting()
                } else if (!isScaling) {
                    val distanceX = mPrevMoveX - event.x.toInt()
                    val distanceY = mPrevMoveY - event.y.toInt()
                    mPrevMoveX = event.x.toInt()
                    mPrevMoveY = event.y.toInt()
                    mMatrix!!.postTranslate(-distanceX.toFloat(), -distanceY.toFloat())
                    cutting()
                }
            }
            MotionEvent.ACTION_MOVE -> if (touchCount >= 2 && isScaling) {
                val dist = distance(event.getX(0), event.getX(1),
                        event.getY(0), event.getY(1))
                var scale = (dist - mPrevDistance) / dispDistance()
                mPrevDistance = dist
                scale += 1f
                scale *= scale
                zoomTo(scale, mWidth / 2, mHeight / 2)
                cutting()
            } else if (!isScaling) {
                val distanceX = mPrevMoveX - event.x.toInt()
                val distanceY = mPrevMoveY - event.y.toInt()
                mPrevMoveX = event.x.toInt()
                mPrevMoveY = event.y.toInt()
                mMatrix!!.postTranslate(-distanceX.toFloat(), -distanceY.toFloat())
                cutting()
            }
            MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP, MotionEvent.ACTION_POINTER_2_UP -> if (event.pointerCount <= 1) {
                isScaling = false
            }
        }
        return true
    }

    private fun getCalculatedScale() = getValue(mMatrix, Matrix.MSCALE_X)

    private fun getPinForCoordinates(coordinates: CoordinatesEntity): Bitmap {
        val bm: Bitmap = BitmapFactory.decodeResource(
                context.resources, R.drawable.ic_pin_raster_no_middle_32
        ).copy(Bitmap.Config.ARGB_8888, true)
        val paint = Paint()
        paint.style = Paint.Style.FILL
        paint.color = Color.parseColor("#3874A4")
        paint.textSize = 20f

        val text = pinCountersMap["${coordinates.x}${coordinates.y}"] ?: ""
        val canvas = Canvas(bm)
        val xStart = when (text.length) {
            1 -> bm.width / 2f - 6
            2 -> bm.width / 2f - 12
            else -> bm.width / 2f - 36
        }
        canvas.drawText(text, xStart, bm.height / 2f, paint)

        return BitmapDrawable(context.resources, bm).bitmap
    }

    @SuppressLint("DrawAllocation")
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        invertedPinsCoordinates.forEach {
            val marker = getPinForCoordinates(it)
            val matrixMarker = Matrix()
            matrixMarker.setTranslate(it.x, it.y)
            matrixMarker.postConcat(mMatrix)
            canvas.drawBitmap(marker, matrixMarker, null)
        }
    }

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouch(v: View, event: MotionEvent): Boolean {
        return super.onTouchEvent(event)
    }

    companion object {
        const val STROKE_WIDTH = 10f
        const val HALF_STROKE_WIDTH = STROKE_WIDTH / 2
        var path = Path()
        var imageHeight = 0
        var imageWidth = 0
        private var mMatrix: Matrix? = null
    }
}

Source: Android Questions

LEAVE A COMMENT