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));
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!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With