How can I draw text where the color transparency d...
# compose
j
How can I draw text where the color transparency depends on the y value? Text() doesn't take a Brush so I can't use a vertical gradient
s
If you have a known background color, I suppose one workaround would be to overlay a box with a gradient. But probably not the sleekest solution.
j
The color under the text changes so I need actual transparency. I'm open to hacky solutions :)
s
Yeah, that makes it a bit more difficult, I guess. I don't have a great solution at hand. :\
c
Canvas+Android native text paint?
j
I think that should work @Chachako. I'm gonna try stuff with drawWithContent + drawRect with a gradient and different blend modes first and I'll go with your solution if I can't figure it out
c
Ok, I also look forward to your solution
j
Ended up doing something similar to this thread. Might be a better way
Copy code
@Composable
fun Digit(digit: Int) {
    val textDelegate = TextDelegate(
        text = AnnotatedString(digit.toString()),
        style = MaterialTheme.typography.h1.copy(
            color = MaterialTheme.colors.onBackground,
            textAlign = TextAlign.Center
        ),
        density = LocalDensity.current,
        resourceLoader = LocalFontLoader.current
    ).layout(Constraints(), LayoutDirection.Ltr)
    Canvas(
        modifier = Modifier
            .fillMaxSize()
            .alpha(0.99f)
    ) {
        TextDelegate.paint(drawContext.canvas, textDelegate)
        drawRect(
            brush = Brush.verticalGradient(listOf(Color.Transparent, Color.White)),
            blendMode = BlendMode.SrcIn
        )
    }
}
s
TextDelegate works for this, though it's an internal API that doesn't provide stability gurantees. You can instead write this using
Paragraph
to do the text layout.
Copy code
@Composable
fun ParaFadingText(text: String, modifier: Modifier = Modifier) {
    val density = LocalDensity.current
    val style = MaterialTheme.typography.h2
    val resourceLoader = LocalFontLoader.current
    BoxWithConstraints(modifier) {
        val paragraph = remember(text, density, style, resourceLoader) {
            Paragraph(
                text = text,
                style = style,
                density = density,
                resourceLoader = resourceLoader,
                width = constraints.maxWidth.toFloat()
            )
        }
        with(density) {
            Canvas(modifier.size(paragraph.width.toDp(), paragraph.height.toDp())) {
                drawIntoCanvas { canvas ->
                    paragraph.paint(canvas)
                    drawRect( // or some other drawing commands here
                        Brush.verticalGradient(listOf(Color.Transparent, Color.White)),
                        blendMode = BlendMode.SrcAtop
                    )
                }
            }
        }
    }
}
There's an open bug to make some of this integration a bit shorter, FYI https://issuetracker.google.com/issues/159949181
Also - make sure you
remember
your
Paragraph
objects and
TextLayoutResult
objects! They're quite expensive (they do full text layout, which involves quite a bit of work)
n
@Sean McQuillan [G] for this particular instance i think just opening up the Text composable to accept a brush instead of just a color would solve this case
j
Thanks guys! I'm going to stay with
TextDelegate
for now since this is just for the challenge and I'm a bit short on time but I have remembered the
TextLayoutResult
for performance and I'll use Paragraph next time. And yes, being able to pass a Brush to the Text composable would have been ideal for this case.
a
Hi @Sean McQuillan [G], I’ve followed the approach stated here. Use
TextDelegate
to get the exact size that the canvas needs to be:
Copy code
@InternalFoundationTextApi
@Composable
fun GradientText(
    text: String,
    brush: Brush,
    modifier: Modifier = Modifier,
    maxLines: Int = Int.MAX_VALUE,
    softWrap: Boolean = true,
    overflow: TextOverflow = TextOverflow.Clip,
    style: TextStyle = LocalTextStyle.current,
) {
    val density = LocalDensity.current
    val resourceLoader = LocalFontLoader.current

    BoxWithConstraints(
        modifier = modifier,
    ) {
        val textDelegate =
            remember(text, style, maxLines, softWrap, overflow, density, resourceLoader) {
                TextDelegate(
                    text = AnnotatedString(text),
                    style = style,
                    maxLines = maxLines,
                    softWrap = softWrap,
                    overflow = overflow,
                    density = density,
                    resourceLoader = resourceLoader,
                ).layout(constraints, LayoutDirection.Ltr)
            }

        with(density) {
            Canvas(
                modifier = Modifier.size(
                    width = textDelegate.size.width.toDp(),
                    height = textDelegate.size.height.toDp()
                )
            ) {
                TextDelegate.paint(drawContext.canvas, textDelegate)
                drawRect(
                    brush = brush,
                    blendMode = BlendMode.SrcIn
                )
            }
        }
    }
}
Works great if the text isn’t drawn on top of a surface.
Copy code
@InternalFoundationTextApi
@Preview
@Composable
fun GradientTextPreview() {
    DfTheme {
//        Surface {
            GradientText(
                text = "Streaming",
                brush = horizontalGradient(
                    colors = listOf(
                        MaterialTheme.colors.secondaryVariant,
                        MaterialTheme.colors.secondary,
                    )
                ),
                modifier = Modifier,
                style = MaterialTheme.typography.h6,
            )
//        }
    }
}
but once rendered on a surface, the brush just blends with the surface as well:
Copy code
@InternalFoundationTextApi
@Preview
@Composable
fun GradientTextPreview() {
    DfTheme {
        Surface {
            GradientText(
                text = "Streaming",
                brush = horizontalGradient(
                    colors = listOf(
                        MaterialTheme.colors.secondaryVariant,
                        MaterialTheme.colors.secondary,
                    )
                ),
                modifier = Modifier,
                style = MaterialTheme.typography.h6,
            )
        }
    }
}
Do you have any advise to work around this? Thanks in advance.
s
Can you file a bug with that repro? I'm not seeing anything obvious
a
hi @Sean McQuillan [G], thanks for the reply. I can’t file it with the same repo but i will extract the code and file it. I will try to file it tomorrow.
s
Thank you!
a
Hi @Sean McQuillan [G], I filled them here: https://issuetracker.google.com/issues/187766726 I am not really sure under where I should file it. I filed it under Foundation. Hope that was the right category.
Here’s the link to the repo as well: https://github.com/qrezet/GradientTextBug