Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android LinearGradient Preallocate and reuse

I have been updating some old code in an app that has a lot of re-allocations in onDraw that complain with message:

Avoid object allocations during draw/layout operations (preallocate and reuse instead).

So I have got all of it updated properly with no more warning, except one. LinearGradient. It seems there is no method to set values on an instance of the object. And the properties are not public so you can't do linLayout.x = value;

This is my code and it complains with the warning described above (underlines LinearGradient):

myPaintGradient.setShader(new LinearGradient(deviation,6,halfwidth,LinearGradientSize,barColorGreen, barColorRed, android.graphics.Shader.TileMode.CLAMP));
like image 495
Jesse Avatar asked Jul 26 '13 15:07

Jesse


1 Answers

I have just solved the same problem, albeit with a RadialGradient.

If you want to update location data of a shader on each draw call you should pre-allocate the shader (as the linter hints at) and you should also pre-allocate a Matrix. The only functionality a shader instance exposes is getLocalMatrix and setLocalMatrix.

To do what you want here, you will make use of setLocalMatrix, passing in a Matrix instance which you have performed some appropriate transforms on. In your case, I think a simple translation transform will do the trick.

As I touched on earlier, you will need to pre-allocate a Matrix and you will modify this pre-allocated one on each draw loop, before passing it into the shader.setLocalMatrix.

Here is an example of how I did this to update the centerX and centerY of a RadialGradient shader. The use case for this was a user can drag a circle around and I needed to keep the radial gradient centered on the circle.

The code below shows you a full working solution as a custom view which you could copy and paste into your code base, and use in some XML.

The interesting bit is in the onDraw override, where I am doing the matrix transforms and updating the shader:

class MoveableGradientCircle @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    // Struct for all the data which needs pre-allocating
    private data class Circle(
        val radius: Float,
        val centerX: Float,
        val centerY: Float,
        val paint: Paint,
        val shaderMatrix: Matrix
    )
    // Pre-allocate the data
    private var circle: Circle =  Circle(
        radius = 10f,
        centerX = x,
        centerY = y,
        paint = Paint().apply {
            isAntiAlias = true
            style = Paint.Style.FILL_AND_STROKE
            shader = RadialGradient(
                centerX = 0f,
                centerY = 0f,
                radius = 10f,
                startColor = Color.RED,
                edgeColor = Color.GREEN,
                tileMode = Shader.TileMode.CLAMP
            )
        },
        shaderMatrix = Matrix()
    )

    // Setup a touch listener to update the circles x/y positions on user touch location
    init {
        setOnTouchListener { view, event ->
            view.performClick()
            when (event.action) {
                MotionEvent.ACTION_DOWN -> {
                    circle = circle.copy(
                        centerX = event.x,
                        centerY = event.y
                    )
                    invalidate()
                    true
                }
                MotionEvent.ACTION_MOVE -> {
                    circle = circle.copy(
                        centerX = event.x,
                        centerY = event.y
                    )
                    invalidate()
                    true
                }
                MotionEvent.ACTION_UP -> {
                    circle = circle.copy(
                        centerX = event.x,
                        centerY = event.y
                    )
                    invalidate()
                    true
                }
                else -> false
            }
        }
    }

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

        // No need to keep re-allocating shader
        // Instead, we update the pre-allocated matrix
        // In this case, we'll just translate it to the circles current center x,y
        // which happens to be the users touch location
        circle.shaderMatrix.reset()
        circle.shaderMatrix.setTranslate(
            circle.centerX,
            circle.centerY
        )
        // Update the matrix on the shader
        circle.paint.shader.setLocalMatrix(circle.shaderMatrix)
        // Draw the arc with the updated paint
        canvas?.drawArc(
            circle.centerX - circle.radius,
            circle.centerY - circle.radius,
            circle.centerX + circle.radius,
            circle.centerY + circle.radius,
            0f,
            360f,
            true,
            circle.paint
        )
    }
}

I hope this helps you or someone else with the same problem in the future!

like image 156
Thomas Cook Avatar answered Oct 26 '22 00:10

Thomas Cook