Home > OS >  How to update paint shader in a custom view?
How to update paint shader in a custom view?

Time:02-26

I'm working in a custom view that draws a circle based on the touch position. Later I want to change the circle size and color based on the position but right now I'm worried because in the onDraw method, when I create a RadialGradient and add it to the paint.shader, Android Studio shows me a DrawAllocation warning. I want to know if there is a way to avoid this warning or if there is a better approach to achieve this, the fact is that I want to draw gradients with dynamic values, in this case, the only dynamic value I need is the center of the RadialGradient but later I will draw more circles with more gradients and I don't want to have performance issues. Hope you can help me

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

    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)

    private var touch = PointF(0.0f, 0.0f)
    private val size = SIZE_DP * resources.displayMetrics.density
    private var centerX = 0f
    private var centerY = 0f

    override fun dispatchTouchEvent(event: MotionEvent): Boolean {
        touch = PointF(event.x, event.y)
        return true
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        centerX = w * 0.5f
        centerY = h * 0.5f
        touch = PointF(centerX, centerY)
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        // Here is where the warning shows
        // I would like to have only one RadialGradient object and only change the 
        // center coordinates here something like gradient.centerX = touch.x
        paint.shader = RadialGradient(
            touch.x, touch.y, SIZE_DP * 4,
            Color.TRANSPARENT, Color.BLACK, Shader.TileMode.CLAMP
        )
        canvas.drawCircle(
            touch.x, touch.y,
            size, paint
        )
        postInvalidate()
    }

    companion object {
        const val SIZE_DP = 100.0f
    }
}

CodePudding user response:

I haven't used the gradients before, but as far as I know, when you define one (with its coordinates) you're effectively "painting" a gradient on the canvas, and when you draw a circle somewhere on that canvas, you're basically peeking through to the pre-defined gradient behind. It's not centred on the circle you're drawing.

So in that case, you have to create a new RadialGradient each time you want to draw a new circle somewhere else. The warning you're getting is basically because onDraw can potentially be called every frame (every 16ms or faster on some devices), and allocating stuff in there is a quick way to pile up the memory usage. But it's up to you - if you know it's only going to be called occasionally, you can just suppress the warning (put the cursor on the warning and pick one of the quick fixes)


One suggestion though - you're calling postInvalidate() (you could just use invalidate()) in onDraw, which ensures it will get drawn again next frame. You don't actually need to do that as far as I can tell - you only need to redraw your view when the circle changes, which is when you get another touch event, right?

So instead, you could call invalidate() from your dispatchTouchEvent method. Every time you get a new event, tell the view to update itself. You're updating the current value for touch in there - you could also create your RadialGradient in there too, and set it on paint. That way, your onDraw just needs to draw a circle using the current paint and touch. And you don't need to supress any warnings since the allocations aren't happening in there.

Since you update touch in onSizeChanged too, best put all your updates in a function, update the state in one place:

fun updateCirclePosition(x: Int, y: Int) {
    touch = PointF(x, y)
    paint.shader = RadialGradient(
        x, y, SIZE_DP * 4,
        Color.TRANSPARENT, Color.BLACK, Shader.TileMode.CLAMP
    )
}

and call that from your functions that update touch. That way paint is consistent with it. You could even get smarter with it:

override fun dispatchTouchEvent(event: MotionEvent): Boolean {
    // no redrawing / reallocating unless necessary
    // you might need to check the type of event though (e.g. lifted finger)
    if (event.x != touch.x || event.y != touch.y) {
        updateCirclePosition(event.x, event.y)
        invalidate()
    }
    return true
}

you could even stick the invalidate in updateCirclePosition since, really, updating that position means the view contents are invalid and it needs redrawing

  • Related