Brushing up on Compose Text coloring
Imagine your designer asks you to implement the sketch below:
Building this screen in Jetpack Compose should be straightforward, except for the Text
’s gradient color.
Let’s explore some general strategies you could have used to implement a gradient in Compose before version 1.2.0.
A first approach could be to use Compose’s Canvas
, drawing directly onto the native android.graphics.Canvas
:
A more Compose-idiomatic approach would be to use the drawWithCache
modifier on the text, together with a Brush
:
Here, the strategy is drawing a rectangle with gradient colors on top of the text and then blending it using SrcAtop
to make sure that only text is visible and the rest of the rectangle is cut. This approach, however, draws over emojis (as you can see above), and inline content.
Both solutions require deeper knowledge of drawing APIs, Canvas
and Paint
. Starting with Compose 1.2.0 we have a better solution!
Brush API
Compose 1.2.0 adds Brush
API to TextStyle
and SpanStyle
to provide a way to draw text with complex coloring, with gradient being only the beginning.
All usages of
Brush
inTextStyle
are experimental, so make sure you add theExperimentalTextApi
annotation where necessary.
There are 2 main components you’ll be working with:
Brush
: It provides access to default brushes, most of them implementations ofShaderBrush
(likeLinearGradient
,RadialGradient
, etc).ShaderBrush
: When the default brushes are not enough, you can extend this class to implement your own custom brush.
To implement the design above, we define the list of gradient colors and use a linearGradient
brush on the TextStyle
.
That’s it! It is really that easy. Notably, with this solution the brush won’t draw over emojis, as they are skipped by the underlying shader.
Default brushes
Brush
offers various predefined brush styles in its API. We already used linearGradient
, and you also have the following:
Additionally SolidColor
brush paints with a single given color:
Check out the Brush
documentation for the complete API description.
Custom brushes
There are instances where you might need to know exactly the brush’s size or drawing area and perform some calculations with it- like decreasing the size of your brush to achieve a particular tiling effect. Below, see how we are able to achieve this using custom brushes.
Repeating a color pattern
Imagine we want to achieve a certain color pattern to be repeated three times. An easy way to do this would be to reduce the brush size to a third of the drawing area and then repeat the sequence.
To access the brush size, you can create your own Brush
, by extending abstract class ShaderBrush
and overriding createShader()
method.
The gradient pattern is repeated with a strategy given by tileMode
parameter.
The tileMode
parameter determines the behavior for how the shader fills a region outside its bounds. Because of how the repetition is calculated, it’s clearer to notice the effect when:
- Your brush is forced to be smaller than the text layout (like in this case).
- Your drawing coordinates are smaller than the available drawing area.
- Using a radial gradient brush.
You can use the following tile modes:
repeated
(just used above) restarts the colors at the edges, repeating the sequence.mirror
mirrors the colors at the edges of the pattern from last to first.clamp
will complete the drawing area by painting with the the color on the edge of the gradient:decal
is supported starting Android S (API 31) and above, it draws the pattern given by the brush size and completes the rest of the drawing area with black. You can check if thistileMode
is supported by using theisSupported
method. If it isn’t, it will fallback totileMode
clamp
.
Image pattern as text coloring
Let’s say that we need to use the colors of an image as text color. For example, using the Jetpack Compose logo, we would want this result:
To implement this, we use a method to create a ShaderBrush
, pass in a native BitmapShader
and set the Bitmap
we want to use.
We’re using the remember function to save the ShaderBrush
across recompositions, because creating a shader can be costly and every call to ShaderBrush would cause a new allocation for BitmapShader as well.
Brush integrations
Brush can be used together with all elements that receive styling components TextStyle
and AnnotatedString
.
For example, you can configure a Brush
as style for your TextField
:
Make sure to use the remember
function to persist the brush across recompositions, when the TextField
state changes on each new typed character.
Additionally, some performance optimizations are done under the hood. For example, converting a brush to a shader can be an expensive operation but AndroidTextPaint
optimizes this process for brushes that don’t change between compositions (like in this case).
To add a gradient to only selected parts of your text or paragraph, you can build an AnnotatedString
and set a Brush
style to only specific spans of your text, like this:
Opacity with Brush
Compose version 1.3.0-alpha01 introduces an alpha optional parameter to TextStyle
/ SpanStyle
, which allows you to modify the opacity of the whole Text when using a color gradient to implement something like this:
To achieve this, we’ll use the same brush for both parts of a text and we will change the alpha of the text in its corresponding span.
For more examples of Brush and TextStyle
/ SpanStyle
, take a look at the BrushDemo
examples in AOSP.
And for more inspiration of what can be achieved with brush in Compose, check out this demo by Halil Özercan (you can find the code here).
Recap
We hope that by having new exciting Compose-idiomatic APIs that seamlessly integrate with APIs you’re already familiar with, will help you create beautiful visuals for your most creative use cases.
If you find any bugs while using Brush
API, please let us know by filing a bug on our issue tracker. To learn more about Canvas
in Compose in general, you can check out our documentation on Graphics in Compose.
We can’t wait to see what you build with Brush
. Tag me on Twitter @astamatok so I can see your beautiful creations!
Last but not least, if you want to learn how to animate colors painted with brush 🖌️ head over to the second part of this blog post!
Happy Composing! 👋
This post was written with the collaboration of Halil Özercan on the Jetpack Compose Text team. Thanks to Rebecca Franks, Florina Muntenescu and Nick Butcher on the DevRel team for their thorough reviews.