0

I need to migrate my old java code that creates a Drawable instance which is a gradient with a starting color in the upper part and a finishing color in the bottom part.

It worked perfectly in java, but after migrating the code to kotlin and compose, getDrawable function returns an empty transparent drawable:

override fun getDrawable(): Drawable {
    val sf: ShaderFactory = object : ShaderFactory() {
        override fun resize(width: Int, height: Int): Shader {
            return LinearGradient(
                startX(width), startY(height), endX(width), endY(height), intArrayOf(
                    color1, color2
                ),
                null, Shader.TileMode.MIRROR
            )
        }
    }
    val p = PaintDrawable()
    p.shape = RectShape()
    p.shaderFactory = sf
    return p
}
   
fun startX(actualWidth: Int): Float {
    when (type) {
        Type.SOLID -> return 0f
        Type.DEGREE_DOWN_UP, Type.DEGREE_UP_DOWN -> return actualWidth / 2.0f
        Type.DEGREE_LEFT_RIGHT -> return 0.0f
        Type.DEGREE_RIGHT_LEFT -> return actualWidth.toFloat()
        else -> {}
    }
    return (-1).toFloat()
}

fun startY(actualHeight: Int): Float {
    when (type) {
        Type.SOLID -> return 0f
        Type.DEGREE_DOWN_UP -> return actualHeight.toFloat()
        Type.DEGREE_LEFT_RIGHT, Type.DEGREE_RIGHT_LEFT -> return actualHeight / 2.0f
        Type.DEGREE_UP_DOWN -> return 0f
        else -> {}
    }
    return (-1).toFloat()
}

fun endX(actualWidth: Int): Float {
    when (type) {
        Type.SOLID -> return actualWidth.toFloat()
        Type.DEGREE_DOWN_UP, Type.DEGREE_UP_DOWN -> return actualWidth / 2.0f
        Type.DEGREE_LEFT_RIGHT -> return actualWidth.toFloat()
        Type.DEGREE_RIGHT_LEFT -> return 0f
        else -> {}
    }
    return (-1).toFloat()
}

fun endY(actualHeight: Int): Float {
    when (type) {
        Type.SOLID -> return actualHeight.toFloat()
        Type.DEGREE_DOWN_UP -> return 0f
        Type.DEGREE_UP_DOWN -> return actualHeight.toFloat()
        Type.DEGREE_LEFT_RIGHT, Type.DEGREE_RIGHT_LEFT -> return actualHeight / 2.0f
        else -> {}
    }
    return (-1).toFloat()
}

The type of the drawable is DEGREE_UP_DOWN

This is how I'm using the drawable:

val bg = GradientShading(Shading.Type.DEGREE_UP_DOWN, topColor, bottomColor)

LazyColumn(
        modifier = modifier.fillMaxWidth()
            .clipToBounds()
            .drawBehind {
                drawIntoCanvas { bg.getDrawable().draw(it.nativeCanvas) }
            },
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Top
    )

What is wrong in the code?

THis is the working java version of the code, which is apparently exactly the same:

@Override
    public Drawable getDrawable() {
        ShapeDrawable.ShaderFactory sf = new ShapeDrawable.ShaderFactory() {
            @Override
            public Shader resize(int width, int height) {
                LinearGradient lg = new LinearGradient(startX(width), startY(height), endX(width), endY(height), new int[] {
                        color1 , color2 },
                        null, Shader.TileMode.MIRROR);
                 return lg;
            }
        };      
        PaintDrawable p = new PaintDrawable();
        p.setShape(new RectShape());
        p.setShaderFactory(sf);
        return (Drawable)p;
    }

float startX(int actualWidth) {         
        switch (this.type) {
        case SOLID:
            return 0;
        case DEGREE_DOWN_UP:
        case DEGREE_UP_DOWN:
            return actualWidth/2.0f;
        case DEGREE_LEFT_RIGHT:
            return 0.0f;
        case DEGREE_RIGHT_LEFT:
            return actualWidth;
        }
        
        return -1;
    }
    
    float startY(int actualHeight){     
        switch (this.type) {
        case SOLID:
            return 0;
        case DEGREE_DOWN_UP:
            return actualHeight;
        case DEGREE_LEFT_RIGHT:
        case DEGREE_RIGHT_LEFT:
            return actualHeight/2.0f;
        case DEGREE_UP_DOWN:
            return 0;
        }
        
        return -1;      
    }
    
    float endX(int actualWidth){        
        switch (this.type) {
        case SOLID:
            return actualWidth;
        case DEGREE_DOWN_UP:
        case DEGREE_UP_DOWN:
            return actualWidth/2.0f;
        case DEGREE_LEFT_RIGHT:
            return actualWidth;
        case DEGREE_RIGHT_LEFT:
            return 0;
        }       
        return -1;          
    }
    
    float endY(int actualHeight){       
        switch (this.type) {
        case SOLID:
            return actualHeight;
        case DEGREE_DOWN_UP:
            return 0;
        case DEGREE_UP_DOWN:
            return actualHeight;
        case DEGREE_LEFT_RIGHT:
        case DEGREE_RIGHT_LEFT:
            return actualHeight/2.0f;
        }       
        return -1;      
    }
9
  • Are you setting the Drawable's bounds somewhere? They're all zeroes by default. Btw, this one kinda works by accident, since they're ignoring the bounds in their draw. GradientDrawable relies on the bounds, though, to calculate its gradient. Commented Oct 23, 2024 at 21:33
  • PaintDrawable, I meant, relies on the bounds for the shader calculations. Commented Oct 23, 2024 at 21:38
  • Btw, at this point, it's probably the same amount of work to just translate it into proper Compose gradients and ditch the Drawable. Commented Oct 23, 2024 at 21:46
  • "after migrating the code to kotlin and compose" -- there does not appear to be any Compose-related code here. You might want to post how you are using this code and your Java version for comparison. Commented Oct 23, 2024 at 21:50
  • As @MikeM. rightly noted, I didn't set bounds for the drawable, so it is my bad that your Drawable is not working. I've modified my previous answer (you are intrested in drawIntoCanvas block), please check if it works now. Commented Oct 24, 2024 at 6:59

1 Answer 1

1

Assign nativeCanvas.clipBounds to drawable.bounds before calling drawable.draw like so:

val drawable = remember { GradientShading(Color.BLUE, Color.MAGENTA).getDrawable() }

LazyColumn(modifier = Modifier
    .fillMaxWidth()
    .clipToBounds()
    .drawBehind {
        drawIntoCanvas { canvas ->
            drawable.bounds = canvas.nativeCanvas.clipBounds
            drawable.draw(canvas.nativeCanvas)
        }
    }
) {
    items(10) { Text("Item $it") }
}

My GradientShading class reconstruction for DEGREE_UP_DOWN (getDrawable method is the same):

private class GradientShading(val color1: Int, val color2: Int) {
    fun getDrawable(): Drawable {
        val sf: ShaderFactory = object : ShaderFactory() {
            override fun resize(width: Int, height: Int): Shader {
                return LinearGradient(
                    startX(width), startY(height), endX(width), endY(height), intArrayOf(
                        color1, color2
                    ),
                    null, Shader.TileMode.MIRROR
                )
            }
        }
        val p = PaintDrawable()
        p.shape = RectShape()
        p.shaderFactory = sf
        return p
    }

    fun startX(actualWidth: Int): Float {
        return actualWidth / 2.0f
    }

    fun startY(actualHeight: Int): Float {
        return 0f
    }

    fun endX(actualWidth: Int): Float {
        return actualWidth / 2.0f
    }

    fun endY(actualHeight: Int): Float {
        return actualHeight.toFloat()
    }
}

Result:

screenshot

Sign up to request clarification or add additional context in comments.

3 Comments

Wonderful Jan, thanks to you I found my error. I was calling bg.getDrawable().bounds and bg.getDrawable().draw in the drawIntoCanvas lambda, and for some reason it was generating the empty drawable. After extracting the getDrawable() call outside the lambda like you, and calling it only one time, it started working.
Just one question more. Why you put the drawable generation inside a " = remember { } " block? If you wanna keep it between recompositions for some reason, shouldn't be inside a remember mutablestate of?
1) I don't see why shouldn't we save the Drawable, I reckon it is cheaper that creating new ShaderFactory and PaintDrawable every recomposition. 2) We need a State when there is a composable that needs to be updated when the State changes. In this case LazyColumn updates don't depend on the Drawable. On the opposite, we update its bounds when LazyColumn updates or just needs to redraw. @NullPointerException

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.