Not a Phase ā Text with Compose and Canvas
Not a Phase ā Text with Compose and Canvas ź“ė Ø
Iāve continued my journey with Compose and Canvas! After exploring drawing and animating shapes, I wanted to learn more about text. Bi-visibility Day was coming, so I drew a small animation to publish on Instagram. The final animation looks like this:
In this blog post, we will look at how to add text to Canvas and position and animate it. Weāre also utilizing custom Google Fonts in the drawing.
If youāre interested in reading the first two posts, here are the links:
- Paint the Stars ā Drawing with Compose and Canvas
- Floating in Space ā Animations with Compose and Canvas
Before We Start
Before we start drawing, I want to say a few words about the design. It has the moon in the waning crescent phase, with a dashed line to complete it to the full moon shape. The text says, āNot a phaseā.
Now, if youāre familiar with the discrimination and stereotypes bisexuals face, you probably already know what all of this means. But for those who are not, one of the stereotypes is that bisexuality is ājust a phase on the way to being straight/gayā.
But itās not ā itās an (umbrella) term for people who feel attraction towards their own and other genders. And even if a bi person is in a monogamous relationship with a person from one gender, it doesnāt make them straight/gay. Theyāre still bi.
So yeah, weāre here. We exist.
Now, letās get to the coding part.
Drawing the Text
Measuring
Drawing text on Canvas is a two-step process: First, measure the text and then draw it. To start with measuring, weāll need aĀ TextMeasurer
, and with Compose-code, we have this neat remember-function we can use:
val textMeasurer = rememberTextMeasurer()
For measuring,Ā TextMeasurer
Ā has a functionĀ measure
, which takes in the text as eitherĀ AnnotatedString
Ā orĀ String
, and a bunch of other (mainly) optional parameters that affect the size of the text. Things likeĀ density
,Ā layoutDirection
,Ā style
,Ā fontFamilyResolver
, and others.
We will divide the text into two strings, as we want to animate and position them a bit differently. As both of our texts are just simple strings with one style, we can use theĀ String
-version for both. The first version of the āNotā-text looks like this:
val notText =
textMeasurer.measure(
text = "Not",
style =
MaterialTheme.typography.titleSmall.copy(
brush = Brush.linearGradient(
colors = Colors.biFlag
),
),
)
For theĀ measure
-function, we pass in the text and then styles. We want to use the theme typography here for straightforwardness, so we copy the small title styles and add a brush to have a linear gradient as the text color. Here, weāre using the bi-flag colors pink, purple, and blue.
The second text is pretty similar:
val phaseText =
textMeasurer.measure(
text = "a phase",
style =
MaterialTheme.typography.titleLarge.copy(
brush =
Brush.linearGradient(
colors = Colors.biFlag,
),
fontSize = 30.sp,
),
)
For this text, weāre utilizing the large title styles from the theme. In addition to gradient colors, weāre setting the font size to 30Ā sp
Ā to make the text bigger.
Alright, now we have everything we need from the measuring step. Next up is drawing the texts on canvas.
Drawing
Compose Canvas has a method calledĀ drawText
Ā for drawing text. It takes in aĀ TextLayoutResult
, which is the type thatĀ measure
Ā function returns. In addition, it takes other parameters meant for styling and positioning the text on Canvas.
For theĀ notText
Ā we defined in the previous subsection, theĀ drawText
Ā would look like this:
drawText(
textLayoutResult = notText,
topLeft =
Offset(
size.width * 0.25f,
size.height * 0.6f,
),
)
We pass in the text layout result, and then we define theĀ topLeft
Ā offset to position the text correctly.
The other text is a bit different. We want to position it relative to theĀ notText
, so we useĀ notText
Ā for calculating the correct position:
drawText(
textLayoutResult = phaseText,
topLeft =
Offset(
x = size.width * 0.35f,
y = (size.height * 0.6f + notText.size.height * 0.7f),
),
)
So here, we define the y-offset to be the same as for theĀ notText
, and then we add 70% of the height of theĀ notText
. This could be the whole height, but I wanted to keep less break between the texts.
There is just one thing left for the drawing ā using custom fonts. Letās talk about that next.
Adding Fonts
For this animation, I wanted to have custom fonts. After playing around with Google Fonts, I decided that the two fonts Iām using are Poppins and Damion.
Android documentation has a page about adding fonts to your project:Ā Work with fonts. However, I accidentally found that Android Studio lets you add Google Fonts as XML files straightforwardly. Hereās how it happens:
- Go to Resource Manager and select the āFontā-tab.
- Click the ā+ā button to add new resource.
- Select āMore Fontsā¦ā.
- Find the Google Font you want to use, select weights, and press OK.
- Let Android Studio add everything needed, like the certification for fonts.
However, previews donāt work correctly if you do it this way and donāt import the ttf-files for fonts. So, if you rely on previews when developing, importing those files should resolve the issue.
After the font is available, the next thing to do is to use it in the styles. Hereās the code for the font families weāre going to use:
val PoppinsFontFamily =
FontFamily(
Font(R.font.poppins\_bold, FontWeight.Bold),
)
val DamionFontFamily =
FontFamily(
Font(R.font.damion, FontWeight.Normal),
)
Then we add the font families to both texts ā Damion for the āNotā text and Poppins to the āa phaseā-text:
val notText =
textMeasurer.measure(
text = "Not",
style =
MaterialTheme.typography.titleSmall.copy(
...
fontFamily = DamionFontFamily
),
)
And
val phaseText =
textMeasurer.measure(
text = "a phase",
style =
MaterialTheme.typography.titleLarge.copy(
...
fontFamily = PoppinsFontFamily
),
)
Animating the Text
The last step weāll need to take is animating the text. We will do that by animating colors and floats. To set things up, letās defineĀ infiniteTransition
, which weāre going to use later:
val infiniteTransition = rememberInfiniteTransition(
label = "infinite"
)
We also want to show the color animation first on the ānotā-text and only after that on the āa phaseā-text. One way to accomplish that is to define a helper float, based on which we use to animate the words. Weāll get back to the implementation later.
Weāll define a variable calledĀ animationPosition
, an infinitely transitioning float from 0f to 4f, which restarts from 0 when it reaches 4. These values could be anything, but after testing, I found that these values worked best when combined with other things in this drawing.
The code forĀ animationPosition
Ā could look like this:
val animationPosition by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 4f,
animationSpec =
infiniteRepeatable(
tween(
durationMillis = 10000,
easing = EaseIn,
),
RepeatMode.Restart,
),
label = "animationPosition",
)
In addition, we will define a helper function for animating the colors. Letās call itĀ biColorsAnimated
, define it to take in a Boolean parameterĀ animated
, and return a list of colors:
@Composable
fun biColorsAnimated(animated: Boolean): List<Color> {
// ....
}
Inside the function, we define our animated colors. We first create a list with the colors, and then map through it. For each color, we returnĀ animateColorAsState
ās value, which has the typeĀ Color
, and finally, we return the list of colors:
val colors = listOf(
biFlag.pink,
biFlag.purple,
biFlag.blue
)
return colors.map {
animateColorAsState(
targetValue = if (animated) it else white,
animationSpec =
tween(
durationMillis = 1000,
easing = EaseInBounce,
),
label = it.toString()
).value
}
This way, we have the bi flagās colors as animated values and can use them with our text.
Finally, we get to tie everything together. For both of the texts, we change the brush gradientās color parameter to use this new function:
val notText =
textMeasurer.measure(
text = "Not",
style =
MaterialTheme.typography.titleSmall.copy(
brush =
Brush.linearGradient(
colors = biColorsAnimated(
animated = animationPosition in 0.5f..1.5f
),
),
// ...
),
)
val phaseText =
textMeasurer.measure(
text = "a phase",
style =
MaterialTheme.typography.titleLarge.copy(
brush =
Brush.linearGradient(
colors = biColorsAnimated(
animated = animationPosition in 2f..3.5f
),
),
// ...
),
)
We use theĀ animationPosition
Ā value to define if the colors for that text are animated. For the first text, we change the colors from white to the bi flag colors if theĀ animationPosition
Ā is between 0.5f and 1.5f, and for the second, if the value is between 2f and 3.5f.
These changes get us the animation you can see at the beginning of this blog post. You can findĀ the complete code in this code snippet.
Wrapping Up
In this blog post, weāve looked into adding text to Canvas, using custom Google Fonts, and animating colors. There was a lot to cover, but the end result is pretty nice!
I hope youāve enjoyed this blog post and learned something. If you want to share your learnings, post on the social media of your choosing, or let me know in the comments!
Links in the Blog Post
Info
This article is previously published on proandroiddev.com (proandroiddev
)