Exploring Jetpack Compose: Text

Within Android Studio 4.0 Canary 1 we can start exploring Jetpack compose, a new way to build the UI for your android applications in a declarative manner. To get started with jetpack compose, there is a great tutorial on the official developer site. In this series of articles I want to dive into each of the components that are available, exploring how we can utilise each of them within our applications.


In this article we’re going to start with the Text component, something that we’re likely to use in most of our applications. When it comes to text, we have a Composable Text component located in the androidx.ui.core package. This component provides us with a collection of attributes to control the appearance of our text.

@Composable
fun Text(
    text: String,
    modifier: Modifier = Modifier.None,
    style: TextStyle? = null,
    paragraphStyle: ParagraphStyle? = null,
    softWrap: Boolean = DefaultSoftWrap,
    overflow: TextOverflow = DefaultOverflow,
    maxLines: Int? = DefaultMaxLines,
    selectionColor: Color = DefaultSelectionColor
)

Using the Text component out of the box does not require too much from our side if we just want to display some text on screen. For example, let’s take some text without any customisation:

We can display this within our preview / on the device by declaring a new Text component and providing a value for the text attribute:

Text(text = "This is some text")

When it comes to the Text component, we can see in the constructor that there are many more attributes that we can assign values for. Let’s take a quick dive into each of these to learn how we can customise the appearance of our Text component:

softWrap – declares whether text should be broken at soft line breaks. This defaults to true, which means that by default text will break onto the next line when the width of the container is reach. To see what this looks like, let’s create a container with a constrained width:

Container(width = Dp(80f)) {
    Text(
        text = "HelloHelloHello"
    )
}

As you can see, our text wraps onto a new line when it reaches the end of the container. However, if we set softWrap = false then our container will act as though there was no limit to the horizontal space for the component. This would give us something that looks like so:

overflow – declares how overflowing text should be displayed within the component. We can declare overflowed text to be displayed in one of three ways:

  • Clip – states that overflowed text should be clipped when it reaches the constraints of its parent. This is the default option used for overflow
  • Fade – overflowed text will be faded out instead of suddenly clipped off at the constraint. The location of the fade will depend on where the overflow has occurred
  • Ellipsis – overflowed text will use ellipsis () when the dimension constraints are met

maxLines – declare the maximum number of lines that can be displayed for the text component. This is a nullable value and this is what the attribute defaults to if not set:

Container(width = Dp(100f)) {
    Text(
        text = "HelloHelloHello",
        maxLines = 1
    )
}

paragraphStyle – an androidx.ui.text.ParagraphStyle reference used to declare the different attributes for styling paragraphs. We’ll cover this in depth in the next section below

textStyle – an androidx.ui.text.TextStyle reference used to declare the different attributes used for styling the text. We’ll cover this in depth in the next section below

selectionColor – an androidx.ui.graphics.Color reference used for the styling of the component when the text is selected


Styling text

When it comes to defining the style attribute for the text component, we need to use an instance of the TextStyle class. As we can see below, there are a collection of different properties that we can set here:

data class TextStyle(
    val color: Color? = null,
    val fontSize: Sp? = null,
    val fontSizeScale: Float? = null,
    val fontWeight: FontWeight? = null,
    val fontStyle: FontStyle? = null,
    val fontSynthesis: FontSynthesis? = null,
    var fontFamily: FontFamily? = null,
    val fontFeatureSettings: String? = null,
    val letterSpacing: Em? = null,
    val baselineShift: BaselineShift? = null,
    val textGeometricTransform: TextGeometricTransform? = null,
    val localeList: LocaleList? = null,
    val background: Color? = null,
    val decoration: TextDecoration? = null,
    val shadow: Shadow? = null
)

You may have noticed that each of these is nullable, with a default value of null – this means that you only need to provide the attributes that you want to style. Let’s take a look at each of these TextStyle attributes and the required data when it comes to setting them:

  • color – an androidx.ui.graphics.Color reference used for the color of the text
  • fontSize – size of the font, using an androidx.ui.core.Sp reference
  • fontSizeScale – value used to scale the given fontSize by. This will essentially multiply the declared fontSize by this scale
  • fontWeight – weight used for the text. For this we must use on the weights defined in the companion object for the class:
val W100 = FontWeight(100)
val W200 = FontWeight(200)
val W300 = FontWeight(300)
val W400 = FontWeight(400)
val W500 = FontWeight(500)
val W600 = FontWeight(600)
val W700 = FontWeight(700)
val W800 = FontWeight(800)
val W900 = FontWeight(900)
val Normal = W400
val Bold = W700
  • fontStyle – style to be used for the text. This can be either Normal or Italic.
  • fontSynthesis – this allows us to state whether or not the system should try to ‘fake’ a bold / italic style for a given font when the font family does not contain the requested style. This can be set to one of:
    • None – if the value does not exist for the given font family, then the default style will be applied to the font
    • Weight – only bold fonts are synthesised when the font family does not contain the bold style. Italic style fonts will not be synthesised
    • Style – only italic fonts are synthesised when the font family does not contain the italic style. Bold style fonts will not be synthesised
    • All – Both bold and italic styles will be synthesised if they do not exist in the desired font family
  • fontFamily – the font family to be used for the text within the component. There are a couple of options for how we can set this:

– Using one of the provided font families within the companion object:

val SansSerif = FontFamily("sans-serif")
val Serif = FontFamily("serif")
val Monospace = FontFamily("monospace")
val Cursive = FontFamily("cursive")

– Providing our own font using one of the provided constructors:

constructor(genericFamily: String) : this(genericFamily, listOf())

constructor(font: Font) : this(null, listOf(font))

constructor(fonts: List<Font>) : this(null, fonts)

constructor(vararg fonts: Font) : this(null, fonts.asList())
  • fontFeatureSettings – used to provide any advanced typography settings for the font, as per the attributes used within CSS
  • letterSpacing – declares the spacing used between characters using a androidx.ui.core.Em reference.
  • baselineShift – declares the vertical shift used from the baseline of the text, a reference can be created by either:
    • Using the BaselineShift constructor, passing in a float reference for the value of the shift
    • Using one of the predefined values from the companion object of the class
val Superscript = BaselineShift(0.5f)
val Subscript = BaselineShift(-0.5f)
  • textGeometricTransform – declare a geometric transformation to be applied to the text. A reference can be created by passing the horizontal scale to be applied to the text, along with the skew to be applied for the horizontal direction
data class TextGeometricTransform(
    val scaleX: Float? = null,
    val skewX: Float? = null
)
  • localeList – the locales uses to select locale specific text, given the text used in the component. A LocalList reference can be created by
    • Using the current locale reference from the companion object of the class, LocaleList.current
    • Create a reference using one of the provided constructors
constructor(languageTags: String) :
            this(languageTags.split(",").map { it.trim() }.map { Locale(it) })

constructor(vararg locales: Locale) : this(locales.toList())
  • background – an androidx.ui.graphics.Color reference used for the background color of the text component
  • decoration – an androidx.ui.text.style.TextDecoration reference that is used to paint a horizontal line somewhere on the text. There are two ways in which we can provide this:

– Using one of the provided Text Decorations within the companion object:

  • TextDecoration.None
  • TextDecoration.Underline
  • TextDecoration.LineThrough

– Combining multiple Text Decorations to create a single decoration using the combine function within the companion object:

fun combine(decorations: List<TextDecoration>): TextDecoration
  • shadow – an androidx.ui.graphics.Shadow reference that declares the shadow to be drawn on the text
data class Shadow(
    val color: Color = Color(0xFF000000),
    val offset: Offset = Offset.zero,
    val blurRadius: Px = 0.px
)

As we can see from this, the TextStyle class offers a huge amount of flexibility with the things that we can style our text with. Ss previously mentioned, not all of these are required when creating our instance but are there should be need to customise our text to a certain level.


Paragraph styling

When it comes to providing styles for the paragraphs of the text component, we need to use an instance of the ParagraphStyle class. As we can see below, there are several properties that we can set here:

data class ParagraphStyle constructor(
    val textAlign: TextAlign? = null,
    val textDirectionAlgorithm: TextDirectionAlgorithm? = null,
    val lineHeight: Sp? = null,
    val textIndent: TextIndent? = null
)

You may have noticed that each of these is nullable, with a default value of null – this means that you only need to provide the attributes that you want to style. Let’s take a look at each of these ParagraphStyle attributes and the required data when it comes to setting them:

textAlign – used to declare how the displayed text should be aligned in the component. This can be one of either LEFT, START, RIGHT, END, CENTER or JUSTIFY.

textDirectionAlgorithm – an androidx.ui.text.style.TextDirectionAlgorithm reference used to declare how the text direction shouljd be evaluated, given the textual content. With this being an enum, we can use one of the declared values:

  • ContentOrLtr – using the Unicode Bidirectional Algorithm, if a strong directional character is detected within the text then the first occurrence will decide the text direction. Otherwise, left-to-right will be used
  • ContentOrRtl – using the Unicode Bidirectional Algorithm, if a strong directional character is detected within the text then the first occurrence will decide the text direction. Otherwise, right-to-left will be used
  • ForceLtr – forces the text to display using a left-to-right format
  • ForceRtl – forces the text to display using a right-to-left format

lineHeight – an androidx.ui.core.Sp reference that declares the line height for the displayed text

textIndent – an androidx.ui.text.style.TextIndent reference that states the properties to be used for displaying an indent for the text

data class TextIndent(
    val firstLine: Sp = 0.sp,
    val restLine: Sp = 0.sp
)

With this attributes we can customise the appearance of the properties surrounding the paragraphs of our text.


As we saw above, the Composable Text class can instantiated to display this component within our UI. If we take a look at the source for how this component is constructed, we can see that is just called another function used to create a Composable Text instance:

@Composable
fun Text(
    text: String,
    modifier: Modifier = Modifier.None,
    style: TextStyle? = null,
    paragraphStyle: ParagraphStyle? = null,
    softWrap: Boolean = DefaultSoftWrap,
    overflow: TextOverflow = DefaultOverflow,
    maxLines: Int? = DefaultMaxLines,
    selectionColor: Color = DefaultSelectionColor
) {
    Text(
        text = AnnotatedString(text),
        modifier = modifier,
        style = style,
        paragraphStyle = paragraphStyle,
        softWrap = softWrap,
        overflow = overflow,
        maxLines = maxLines,
        selectionColor = selectionColor
    )
}

We can see above that this second function used to create a Text component is pretty much the same, with the difference of an androidx.ui.text.AnnotatedString reference instead of a String for the text attribute.

@Composable
fun Text(
    text: AnnotatedString,
    modifier: Modifier = Modifier.None,
    style: TextStyle? = null,
    paragraphStyle: ParagraphStyle? = null,
    softWrap: Boolean = DefaultSoftWrap,
    overflow: TextOverflow = DefaultOverflow,
    @SuppressLint("AutoBoxing")
    maxLines: Int? = DefaultMaxLines,
    selectionColor: Color = DefaultSelectionColor
) 

If we take a look at this AnnotatedString class we can see that it takes a text reference for instantiation. The original Text Composable function that we called utilised this in-order to reuse the second Text Composable function above, via the use of text = AnnotatedString(text).

data class AnnotatedString(
    val text: String,
    val textStyles: List<Item<TextStyle>> = listOf(),
    val paragraphStyles: List<Item<ParagraphStyle>> = listOf()
) 

In some cases we may want to call this second Text component function and provide our own instance of the AnnotatedString class – this class is used when we want to provide styled text that utilises spans / multiple text styles. When it comes to utilising this class we need to make use of the Builder that it provides. As an example with minimal styling, this might look something like so:

val annotatedString = AnnotatedString.Builder("Hello")
    .apply {
        addStyle(TextStyle(color = Color.Red), 0, 2)
    }
Text(text = annotatedString.toAnnotatedString())

When displaying this in the preview, this renders the below result:

When we create a new Builder for the AnnotatedString class, we have two constructors which can be used to start building from:

constructor(text: String)

constructor(text: AnnotatedString)

Once we have our Builder instantiated, we can append onto our AnnotatedString using three different functions. Each of these allows us to append content onto the current state of our AnnotatedString, providing it is in the format that satisfies one of the available constructors:

fun append(text: String) { ... }
fun append(char: Char) { ... }
fun append(text: AnnotatedString) { ... }

Let’s take the example we have above and append some text to it:

annotatedString.append(", some more text!")
Text(text = annotatedString.toAnnotatedString())

When it comes to adding a style to our AnnotatedText, we saw the usage of the addStyle above, passing in a reference to a TextStyle instance, along with the start and end indexes for where the style should be applied in our text. Alongside this, we also have an almost identical addStyle function that takes a ParagraphStyle instance for where we want to style the properties provided by that class:

fun addStyle(style: TextStyle, start: Int, end: Int) { ... }

fun addStyle(style: ParagraphStyle, start: Int, end: Int) { ... }

With the AnnotatedText class there may be cases where we want to add and remove styles across wider ranges of content, without having to provide start and end indexes constantly. In these situations we can utilise the push and pop functions that are provided by the Builder.

pushStyle(style: TextStyle): Int – pushes a TextStyle onto the AnnotatedText. The style will be applied to any text that is appended after the style has been pushed. When pushing a TextStyle, the index for the style will be returned.

pushStyle(style: ParagraphStyle): Int – pushes a ParagraphStyle onto the AnnotatedText. This function works the same as the above.

popStyle() – removes the previously added style from the AnnotatedText

popStyle(index: Int) – removes the style at the specified index from the AnnotatedText

So for example, we could write the following code:

val style = TextStyle(color = Color.Red)
val annotatedString = AnnotatedString.Builder("Hello")
annotatedString.pushStyle(style)
annotatedString.append(", some more text!")
annotatedString.popStyle()
annotatedString.append(" And some more.")
Text(text = annotatedString.toAnnotatedString())

Which would give us the following result in our preview window:

Finally, once we’re done building our AnnotatedText we can use the toAnnotatedString() function to take our text and the attached styles and return us an instance of the AnnotatedText class.


Throughout this article we’ve dived into all the different parts of the Text component and how they can be used to style our text just as we would pre-Compose. Whilst we’ve only looked at some introductory usage of these properties, now that we know what is available here and how it can be used, this gives us to knowledge we need for what Text has to offer.

Whilst Jetpack Compose is still in preview, some of these details are likely to change. But in the meantime if you have any thoughts or questions, please feel free to reach out!

[twitter-follow screen_name=’hitherejoe’ show_count=’yes’]

Leave a Reply

Your email address will not be published. Required fields are marked *