Search code examples
android-custom-viewkotlin-coroutines

Why did Canvas draw in Coroutine background thread draw in weird position?


I have a simple customView that draw a single line as below.

class ViewCustom @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0)
    : View(context, attrs, defStyleAttr) {

    private val strokePaint = Paint()
        .apply { color = Color.BLACK}
        .apply { strokeWidth = 6f }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
            canvas.drawLine(
                0f, 0f,
                width.toFloat(), height.toFloat(),
                strokePaint
            )
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)

        val desiredWidth = suggestedMinimumWidth + paddingLeft + paddingRight
        val desiredHeight = suggestedMinimumHeight + paddingTop + paddingBottom

        setMeasuredDimension(resolveSize(desiredWidth, widthMeasureSpec),
            resolveSize(desiredHeight, heightMeasureSpec))
    }
}

It works fine, draw the diagonal line end to end.

However, if I add a coroutine thread around it

class ViewCustom @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0)
    : View(context, attrs, defStyleAttr) {

    private val treeDrawing by lazy {
        TreeDrawingCanvasCustom()
    }

    private val strokePaint = Paint()
        .apply { color = Color.BLACK}
        .apply { strokeWidth = 6f }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        CoroutineScope(Dispatchers.Default).launch { // <-- add coroutine
            canvas.drawLine(
                0f, 0f,
                width.toFloat(), height.toFloat(),
                strokePaint
            )
        }
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)

        val desiredWidth = suggestedMinimumWidth + paddingLeft + paddingRight
        val desiredHeight = suggestedMinimumHeight + paddingTop + paddingBottom

        setMeasuredDimension(resolveSize(desiredWidth, widthMeasureSpec),
            resolveSize(desiredHeight, heightMeasureSpec))
    }
}

It still draws, but at a strange location as shown in the diagram below. I have checked that the height and width value is still the same with or without the coroutine.

Why is it behaving as such?

enter image description here


Solution

  • Just found out the reason is that there's only one canvas shared across all drawings. When the onDraw is on the background thread, there's no guarantee others are also manipulating the main thread in drawing the canvas, hence such concurrent drawing will cause the drawing coordinate to be misaligned from its original coordinate.

    In the image diagram case below, the drawing is on the Caption area, is because that happens concurrently between the Text Caption drawing, hence it is drawn on the Text Caption area.