0

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()
    }
}
Aymen Ben Salah
  • 489
  • 4
  • 13
Drics
  • 41
  • 1
  • 10

1 Answers1

0

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)

cactustictacs
  • 17,935
  • 2
  • 14
  • 25
  • Your assumption turned out to be correct and after initializing the gradient after `textWidth`, the gradient began to be displayed more, but still it does not take up all the space, but only reaches the middle of the element, and then it is filled with a solid color – Drics Jun 09 '22 at 12:21
  • @Drics then the width isn't getting calculated properly - when you define your gradient's position, you're saying where the gradient starts and ends on the canvas, using its coordinates. The line extends past those start and end points, and beyond those points it'll fill with the start and end colour respectively. So your end point is in the wrong place. (And when you use that paint, you're creating a "window" into that filled canvas). I don't know what you're trying to do exactly with these calculations, but if you just want to fill your rectangle, define your gradient with the same x coords – cactustictacs Jun 09 '22 at 14:19
  • Thank you very much for answering my question. But could you help me solve a related problem? My coordinates are based on `Rect()` whose coordinates are updated only in the `draw` method and when I try to set `rect.set(bounds)` from any other location, then the coordinates are 0, but I need them when initializing Drawable. How to fix it? – Drics Jun 09 '22 at 15:52
  • @Drics a typical way to do it is override `onSizeChanged` (which gets called when the `View` is resized, including when it's first laid out). That's a good place to do all your setup and calculation for things that depend on the view's dimensions. So for example, you'd want to update your gradient in there, since it depends on specific coordinates. You could also store a `Rect` and update it whenever the view size changes, if that's useful to you! – cactustictacs Jun 09 '22 at 16:53
  • Thanks for the idea, but I don't observe the `onSizeChanged` method in Drawable – Drics Jun 09 '22 at 17:28
  • @Drics oh sorry, I was thinking it was a custom view, not a custom drawable. I've never actually worked with them before, but the example code (https://developer.android.com/guide/topics/graphics/drawables#Custom) shows them getting the dimensions/bounds inside `draw()` every time it's called. So I guess do all your initialisation there - maybe store a current width/height/Rect and update it in `draw` and then tell all your other stuff to update, and have it reference those current values. You could also check if those values have actually changed, and avoid the update calls if they haven't – cactustictacs Jun 09 '22 at 18:12
  • Yes, I did get the values in `draw`, but I can't figure out how to tell other elements about the update. Thus, my gradient uses the values created during initialization (that is, those that are equal to 0) and after updating these parameters in the `draw` method it keeps using 0 – Drics Jun 09 '22 at 18:20
  • 1
    Thank you very much for your help. I solved this problem using `Delegates.observable()`. I definitely wouldn't have done it without you – Drics Jun 09 '22 at 19:31
  • @Drics oh no worries! I added some code to my answer to give you an idea about how you'd generally structure this stuff and make some things initialise "later". Lots of ways to make that happen (and delegates like `observable` and `lazy` can be real useful!) – cactustictacs Jun 09 '22 at 19:42