A Dive into Text Styling with Annotated String in Jetpack Compose
Photo by Clay Banks on Unsplash
Introduction
Text plays a crucial role in user interfaces as a means of communication. Jetpack Compose introduces the AnnotatedString API, a dynamic tool that transcends ordinary text rendering. In this comprehensive guide, we will delve deep into the realm of AnnotatedString in Jetpack Compose.
Through meticulous explanations, a multitude of coding snippets, and real-world examples, you’ll emerge as a master of AnnotatedString
, understanding its nuances and recognizing why and when it’s indispensable for your UI endeavors.
Key Concepts
1. Understanding AnnotatedString
AnnotatedString isn’t just a text container; it’s a gateway to dynamic styling. It lets you associate styles with specific text ranges, enabling the creation of text that’s both visually captivating and functionally interactive.
2. Annotations and Styles
AnnotatedString introduces dynamic text styling by allowing you to associate specific styles with defined text ranges. Annotations act as markers, specifying these ranges, while style objects are used to adorn the annotated portions. In our journey, we’ll explore two key style categories — SpanStyle
and ParagraphStyle
, each offering unique styling capabilities for creating visually compelling and interactive text.
3. Building AnnotatedString
With the buildAnnotatedString
DSL, you wield the power to craft an AnnotatedString
. Through styles and annotations, you dictate how your text appears to users.
Basic Text Styling
1. Applying Font Styles
The SpanStyle
lets you tweak font properties like family, size, weight, and more for specific segments of text.
Font Styles Example
val boldText = buildAnnotatedString { withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { append("Bold Text") } append("\n\n") append("Regular Text") }
2. Colorful Text
The SpanStyle
span allows you to apply colors to the text.
Colorful Text Example
val colorfulText = buildAnnotatedString { withStyle(style = SpanStyle(color = Color.Red)) { append("Vibrant Text") } append("\n\n") append("Regular Text") }
3. Background Highlights
Mark / highlight specific text using SpanStyle
.
Text Highlighting Example
val highlightedText = buildAnnotatedString { withStyle(style = SpanStyle(background = Color.Yellow)) { append("Highlighted Text") } append("\n\n") append("Regular Text") }
Create Clickable Text
1. Clickable Links
Let’s explore how we can add a clickable link inside a text string
Clickable Links Example
val annotatedLinkString: AnnotatedString = buildAnnotatedString { val str = "Let's open google!" val startIndex = str.indexOf("google") val endIndex = startIndex + 6 append(str) addStyle( style = SpanStyle( color = Color.Red, textDecoration = TextDecoration.Underline ), start = startIndex, end = endIndex ) addUrlAnnotation( UrlAnnotation("https://google.com"), start = startIndex, end = endIndex ) } val uriHandler = LocalUriHandler.current ClickableText( modifier = Modifier.padding(20.dp).fillMaxWidth(), text = annotatedLinkString, onClick = { annotatedLinkString .getUrlAnnotations(it, it) .firstOrNull()?.let { annotation -> uriHandler.openUri(annotation.item.url) } } )
Key Points —
AnnotatedString
is used to define the text with a clickable link.- We identify the range of the link within the text and apply styling (red color and underline) using
addStyle
. - A URL annotation (“https://google.com“) is added to the link range using
addUrlAnnotation
. - The
uriHandler
is obtained to open URLs. - The
ClickableText
composable displays the annotated string. - When the text is clicked, the
onClick
lambda checks for URL annotations and opens the URL usinguriHandler
.
2. Expandable Text
Let’s see how we can create a toggleable text composable
Expandable Text Example
// Define tags for expanded and minified text val tagExpanded = "expanded_text" val tagMinified = "minified_text" // Maintain the state of text toggling (expanded or minified) val textToggleState = remember { mutableStateOf(Pair(tagMinified, "Read more ...")) } // Create the annotated string for expanded and minified text val expandedTextString: AnnotatedString = buildAnnotatedString { val toggleString = textToggleState.value.second // Define the base article snippet with a toggle string val snippet = "In a groundbreaking discovery, scientists " + "have identified a new species of marine life " + "in the deep sea. $toggleString" // Find the start and end indices of the toggle string val startIndex = snippet.indexOf(toggleString) val endIndex = startIndex + toggleString.length // Apply styling to the entire snippet (font size and semi-bold) withStyle(style = SpanStyle(fontSize = 24.sp)) { withStyle(style = SpanStyle(fontWeight = FontWeight.SemiBold)) { append(snippet) } // If the text is expanded, add more article content if(textToggleState.value.first == tagExpanded) { append( "\n\nThis new species, tentatively named " + "'DeepSea Marvel,' was found at a depth " + "of 4,000 meters beneath the ocean's surface." ) } } // Apply styling to the specified range (magenta color and underline) addStyle( style = SpanStyle( color = Color.Magenta, textDecoration = TextDecoration.Underline ), start = startIndex, end = endIndex ) // Add annotations based on the text state (expanded or minified) if(textToggleState.value.first == tagExpanded) { addStringAnnotation( tagMinified, "Read again ...", start = startIndex, end = endIndex ) } else { addStringAnnotation( tagExpanded, "Show less ...", start = startIndex, end = endIndex ) } } // Create a clickable text composable to display the article text ClickableText( modifier = Modifier.padding(16.dp).fillMaxWidth(), text = expandedTextString, onClick = { // Toggle between expanded and minified text based on annotations expandedTextString .getStringAnnotations(it, it) .firstOrNull()?.let { annotation -> if(annotation.tag == tagExpanded) { textToggleState.value = Pair(tagExpanded, annotation.item) } else { textToggleState.value = Pair(tagMinified, annotation.item) } } } )
Key Points —
- Text Toggling State — The code maintains the state of text toggling using
textToggleState
. This state variable tracks whether the text is currently in the expanded or minified state. - AnnotatedString — An
AnnotatedString
namedexpandedTextString
is used to represent the text. It contains styling information and annotations for managing the expandable behavior. - Toggle String — The
toggleString
provides context for users to expand or minimize the article content. It dynamically changes based on the text’s state. - Styling — Styling is applied to the text using
SpanStyle
. The text is set to a larger font size and semi-bold for the headline. Additionally, it uses magenta color and underlining to highlight thetoggleString
. - Annotations — Annotations are added to the
AnnotatedString
to indicate whether to show more or less text based on the state. Tags liketagExpanded
andtagMinified
help manage these annotations. When the text is in the expanded state (i.e., when users click “Read more …”), the code adds an annotation with the tagtagMinified
and the text “Read again …” to the same range as the toggle string. This annotation indicates that readers can click to see the content again if they choose to minimize it. - ClickableText Composable — The
ClickableText
composable displays the article text. When readers click on the text, it checks for annotations and toggles between the expanded and minified states.
Paragraph Text Styling
Paragraph styles in Jetpack Compose allow you to control the visual appearance of text at the paragraph level, providing flexibility in text layout. The following function exemplifies the application of withStyle
to craft a paragraph style for the provided text
parameter, subsequently justifying the text with a paragraph lineHeight
set to 30.sp
.
Job Offers
Paragraph Style Example
@Composable fun LegalLetterText(text: String) { val customLineSpacingText = buildAnnotatedString { withStyle( style = ParagraphStyle( lineHeight = 30.sp, textAlign = TextAlign.Justify, ) ) { append(text) } } Text( text = customLineSpacingText, modifier = Modifier.fillMaxWidth().padding(16.dp), ) }
More Example Ideas Anyone? 💡
Photo by Caroline Roose on Unsplash
Annotated strings offer a versatile toolset for transforming plain text into visually engaging and context-rich content. Here are some creative and practical ideas for leveraging annotated strings —
- Stylized Poetry — Use annotated strings to bring your poems to life, applying different styles and colors to emphasize emotions or themes.
- Musical Notations — Create musical scores or tutorials with annotated strings, visually representing notes, chords, and sheet music symbols.
- Code Commentary — Enhance your code documentation by using annotated strings to provide explanations, warnings, or code annotations.
- Syntax Learning — Build interactive learning modules for programming languages, highlighting syntax elements and providing explanations.
- Historical Letters — Recreate historical letters, diaries, or documents with annotated strings, preserving their original format and style.
- Abstract Art Text — Craft visually striking abstract art pieces using annotated strings, where text itself becomes an art form.
- Sci-Fi Terminal — Simulate a futuristic or sci-fi terminal interface with styled text, making your app feel like it’s from a distant future.
- ASCII Banner — Generate eye-catching ASCII art banners or headers using annotated strings, ideal for headers or branding.
- Emulator Text — Replicate the appearance of vintage computer or game console screens with annotated strings for retro-themed apps.
- Code Editor Text — Create a custom code editor with syntax highlighting and annotations for a developer-friendly experience.
- Blog Post — Format blog post content dynamically with annotated strings, allowing for rich text formatting, links, and more.
- News Feed — Display news articles with custom formatting, emphasizing headlines, images, and metadata using annotated strings.
Example Code Snippets
1. Stylized Poetry
Stylized Poetry Example
val stylizedPoetry = buildAnnotatedString { withStyle(style = SpanStyle(fontSize = 24.sp)) { append("The ") withStyle(style = SpanStyle(fontWeight = FontWeight.Bold, color = Color.Red)) { append("rose") } append(" is ") withStyle(style = SpanStyle(fontStyle = FontStyle.Italic, color = Color.Green)) { append("red") } append("\n") withStyle(style = SpanStyle(textDecoration = TextDecoration.LineThrough, color = Color.Blue)) { append("The ") } withStyle(style = SpanStyle(fontStyle = FontStyle.Italic, color = Color.Red)) { append("violets") } append(" are blue") } }
2. Musical Notations
Musical Notations Example
val musicalNotation = buildAnnotatedString { withStyle(style = SpanStyle(fontSize = 24.sp, color = Color.Black)) { append("Sheet Music") } append("\n\n") withStyle(style = SpanStyle(fontSize = 36.sp, color = Color.Black)) { append("♪") } append(" ") withStyle(style = SpanStyle(fontSize = 20.sp, color = Color.Gray)) { append("Piano Sonata in C Minor") } append("\n") withStyle(style = SpanStyle(fontSize = 24.sp, color = Color.Black)) { append("pp") } append(" ") withStyle(style = SpanStyle(fontSize = 18.sp, color = Color.Gray)) { append("Softly") } append(" ") withStyle(style = SpanStyle(fontSize = 28.sp, color = Color.Black)) { append("f") } append(" ") withStyle(style = SpanStyle(fontSize = 18.sp, color = Color.Gray)) { append("Loudly") } }
3. Abstract Art Text
Abstract Art Text Example
val abstractArtText = buildAnnotatedString { withStyle(style = SpanStyle(fontSize = 42.sp, brush = Brush.horizontalGradient(listOf(Color.Red, Color.Blue, Color.Green)))) { append("Colors in Motion") } append("\n\n") append("Vivid shades of ") withStyle(style = SpanStyle(brush = Brush.verticalGradient(listOf(Color.Magenta, Color.Yellow)))) { append("passion") } append(" blend with the ") withStyle(style = SpanStyle(brush = Brush.horizontalGradient(listOf(Color.Blue, Color.Green)))) { append("calmness") } append(" of an eternal ") withStyle(style = SpanStyle(brush = Brush.radialGradient(listOf(Color.Red, Color.Magenta)))) { append("sunrise") } append(". A symphony of ") withStyle(style = SpanStyle(brush = Brush.linearGradient(listOf(Color.Blue, Color.Cyan)))) { append("colors") } append(" dances in the ") withStyle(style = SpanStyle(brush = Brush.sweepGradient(listOf(Color.Green, Color.Blue, Color.Magenta)))) { append("sky") } append(" like a dream.")
Sample Screenshots
Here are some more sample screenshots for some of the examples listed.
Terminal Text, Blog Post and Code Examples
Note — You can find all the code snippets for examples shown in this article in the gist provided. If there’s a specific one you would like to develop and need help, feel free to reach out!
Why Choose AnnotatedString?
Photo by Markus Winkler on Unsplash
1. Interactive Content
AnnotatedString enables you to create interactive text elements within your UI. Whether it’s clickable links, expandable sections, or user-triggered actions, AnnotatedString adds an extra layer of engagement to your app.
2. Rich Text Formatting
AnnotatedString goes beyond plain text. You can apply a myriad of formatting options, such as font styles, colors, highlights, and custom paragraph layouts. This flexibility allows you to bring your text to life and align it with your app’s design.
3. Contextual Styling
AnnotatedString excels at contextual styling. You can emphasize specific words or phrases within a text block, making it perfect for code commentary, highlighting keywords, or providing visual cues in your content.
4. Accessibility
With AnnotatedString, you can ensure that your text remains accessible to all users. Implement annotations to provide additional context for screen readers or other assistive technologies, enhancing the inclusivity of your app.
Conclusion
Whether it’s fine-tuning fonts, creating clickable links, or orchestrating visual symphonies, AnnotatedString empowers you to create UIs that leave an impression.
As you continue your journey with Jetpack Compose, remember this little gem — AnnotatedString
. It’s your partner in the quest for delightful, user-friendly interfaces. So, embrace it, explore it, and let your UIs tell stories that captivate, inform, and make people smile.
It’s not about just styling text; it’s about crafting experiences that users remember!
Closing Remarks
If you liked what you read, please feel free to leave your valuable feedback or appreciation. I am always looking to learn, collaborate and grow with fellow developers.
If you have any questions feel free to message me!
Here is the link to the gist containing all the previously mentioned code snippets and code for some of the examples I mentioned at the end.
Follow me on Medium for more articles — Medium Profile
Connect with me on LinkedIn for collaboration — LinkedIn Profile
Also, you’re welcome to follow me on Twitter for more updates and insights — Twitter Profile
Happy Composing!
This article was previously published on proandroiddev.com