When trying to make a gradient from the background, the background is filled with one color

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 Paints, 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

This Answer collected from stackoverflow, is licensed under cc by-sa 2.5 , cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply

(*) Required, Your email will not be published