Issue
I'm creating an Android application and I needed to create a Drawable with a gradient background and text inside, but for some reason I don't have a gradient, and the entire background is filled with solid color
Class code:
class TestDrawable(textSize: Int = 16) : Drawable() {
private val rect = RectF()
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)
private val textWidth: Int
private val text: String
private var backgroundGradient: LinearGradient = LinearGradient(
0f, 0f, intrinsicWidth.toFloat(), 0f,
intArrayOf(-0xb73320, -0xafa523, -0x41bf40, -0x457d5),
floatArrayOf(0.06f, 0.34f, 0.73f, 1f),
Shader.TileMode.CLAMP
)
override fun draw(canvas: Canvas) {
rect.set(bounds)
canvas.drawRoundRect(rect,
AndroidUtilities.dp(2f).toFloat(),
AndroidUtilities.dp(2f).toFloat(), paint)
canvas.drawText(
text,
rect.left + AndroidUtilities.dp(5f),
rect.top + AndroidUtilities.dp(12f),
textPaint
)
}
override fun getIntrinsicWidth(): Int {
return textWidth + AndroidUtilities.dp((5 * 2).toFloat())
}
override fun getIntrinsicHeight(): Int {
return AndroidUtilities.dp(16f)
}
init {
textPaint.textSize = AndroidUtilities.dp(textSize.toFloat()).toFloat()
textPaint.typeface = AndroidUtilities.getTypeface("fonts/rmedium.ttf")
textPaint.color = -0x1000000
//paint.style = Paint.Style.FILL
paint.color = -0x1
paint.shader = backgroundGradient
backgroundGradient.setLocalMatrix(Matrix())
text = "plus".uppercase()
textWidth = ceil(textPaint.measureText(text).toDouble()).toInt()
}
}
Solution
You're initialising backgroundGradient
when you declare the variable, and that sets the gradient width with a call to getIntrinsicWidth
, which itself relies on textWidth
having been initialised. But that initialisation happens in the init
block, which is below backgroundGradient
, so it hasn't run yet.
I haven't tested it but I'm guessing textWidth
is still zero (they behave like Java objects/primitives in this situation) so you're getting a very tiny gradient and the rest of your background is just the end colour. Try initialising your gradient in init
, after textWidth
has been set
This is the kind of thing I'm talking about in the comments - you get your metrics in draw()
, so that's when you should initialise/update your stuff that depends on those metrics:
// keep a record of the previous bounds values for comparison
private var previousBounds: Rect? = null
override fun draw(canvas: Canvas) {
// check if the dimensions have changed - if so, update everything
if (bounds != previousBounds) {
updateStuff(bounds)
previousBounds = bounds
}
// draw stuff
canvas.drawRoundRect(bounds,
AndroidUtilities.dp(2f).toFloat(),
AndroidUtilities.dp(2f).toFloat(), paint
)
canvas.drawText(
text,
rect.left + AndroidUtilities.dp(5f),
rect.top + AndroidUtilities.dp(12f),
textPaint
)
}
private fun updateStuff(area: Rect) {
// update all your stuff that changes when the dimensions change
paint.shader = LinearGradient(
0f, 0f, area.width, 0f,
intArrayOf(-0xb73320, -0xafa523, -0x41bf40, -0x457d5),
floatArrayOf(0.06f, 0.34f, 0.73f, 1f),
Shader.TileMode.CLAMP
)
}
So the basic idea here is there's stuff you can initialise during construction - basic Paint
s, colours etc. Then there's some stuff that you can only initialise during draw
, when you finally have the drawable's dimensions. If you split those out, you can initialise/update the stuff that needs it directly from the draw
function, when you have the info needed.
For example, you don't actually need to set a gradient shader on your paint during construction - you just need it before you try to draw anything. That's simple enough - set it inside the draw
call. By keeping a copy of the most recent set of dimensions, you can compare and see if anything's changed, and avoid unnecessarily recreating the same LinearGradient
every time (I don't know how often draw
is called, but it's a good habit either way). By making it null at the start, the comparison fails so it always updates the first time draw
is called (i.e. it initialises)
I don't know if you're still using that getIntrinsicWidth
call, but if you are, since it relies on textSize
being set, just make sure that's set before you reference it. And since it looks like it doesn't change after being set during init, and the draw
call (and any updates it triggers) comes later, it's all good. If any of that stuff does need to update, just put it in the update function, and make sure things come after anything they rely on
(I haven't tested this code, it's just to give you the general idea)
Answered By - cactustictacs
Answer Checked By - David Goodson (JavaFixing Volunteer)