-
-
Save andrei-fedorov/75aa50e02b3ad7ceda274a1b3957e9ec to your computer and use it in GitHub Desktop.
Text Highlight in Jetpack Compose
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import androidx.compose.animation.core.animateOffsetAsState | |
| import androidx.compose.animation.core.animateSizeAsState | |
| import androidx.compose.foundation.layout.Arrangement | |
| import androidx.compose.foundation.layout.Column | |
| import androidx.compose.foundation.layout.fillMaxSize | |
| import androidx.compose.foundation.layout.padding | |
| import androidx.compose.material3.Button | |
| import androidx.compose.material3.MaterialTheme | |
| import androidx.compose.material3.Text | |
| import androidx.compose.runtime.Composable | |
| import androidx.compose.runtime.getValue | |
| import androidx.compose.runtime.mutableIntStateOf | |
| import androidx.compose.runtime.mutableStateOf | |
| import androidx.compose.runtime.remember | |
| import androidx.compose.runtime.setValue | |
| import androidx.compose.ui.Alignment | |
| import androidx.compose.ui.Modifier | |
| import androidx.compose.ui.draw.drawBehind | |
| import androidx.compose.ui.geometry.CornerRadius | |
| import androidx.compose.ui.geometry.Offset | |
| import androidx.compose.ui.geometry.Rect | |
| import androidx.compose.ui.geometry.Size | |
| import androidx.compose.ui.graphics.Color | |
| import androidx.compose.ui.text.TextLayoutResult | |
| import androidx.compose.ui.unit.dp | |
| @Composable | |
| fun TextHighlight(modifier: Modifier = Modifier) { | |
| Column( | |
| modifier = modifier.fillMaxSize(), | |
| verticalArrangement = Arrangement.Center, | |
| horizontalAlignment = Alignment.CenterHorizontally | |
| ) { | |
| val text = """ | |
| Text is a central piece of any UI, and Jetpack Compose makes it easier to display or write text. Compose leverages composition of its building blocks, meaning you don’t need to overwrite properties and methods or extend big classes to have a specific composable design and logic working the way you want. | |
| As its base, Compose provides a BasicText and BasicTextField, which are the barebones to display text and handle user input. At a higher level, Compose provides Text and TextField, which are composables following Material Design guidelines. It’s recommended to use them as they have the right look and feel for users on Android, and includes other options to simplify their customization without having to write a lot of code. | |
| """.trimIndent() | |
| var wordRects by remember { mutableStateOf(emptyList<Pair<String, Rect>>()) } | |
| var counter: Int by remember { mutableIntStateOf(0) } | |
| val currentRectTopLeft by animateOffsetAsState( | |
| targetValue = if (wordRects.isEmpty()) { | |
| Offset.Zero | |
| } else { | |
| wordRects[counter].second.topLeft | |
| }, | |
| label = "currentRectTopLeft" | |
| ) | |
| val currentRectSize by animateSizeAsState( | |
| targetValue = if (wordRects.isEmpty()) { | |
| Size.Zero | |
| } else { | |
| wordRects[counter].second.size | |
| }, | |
| label = "currentRectSize" | |
| ) | |
| Text( | |
| text = text, | |
| style = MaterialTheme.typography.titleMedium, | |
| modifier = Modifier | |
| .padding(24.dp) | |
| .drawBehind { | |
| drawRoundRect( | |
| Color(0xFFffd31c), | |
| currentRectTopLeft, | |
| currentRectSize, | |
| CornerRadius(20f, 20f) | |
| ) | |
| }, | |
| onTextLayout = { textLayoutResult -> | |
| wordRects = extractWordRects(text, textLayoutResult) | |
| } | |
| ) | |
| Button(onClick = { counter++ }) { | |
| Text(text = "Next word") | |
| } | |
| Button(onClick = { counter = 0 }) { | |
| Text(text = "Reset") | |
| } | |
| } | |
| } | |
| fun extractWordRects(text: String, layoutResult: TextLayoutResult): List<Pair<String, Rect>> { | |
| val words = text.split("\\s+".toRegex()) // Split text into words | |
| val wordRects = mutableListOf<Pair<String, Rect>>() | |
| var startIndex = 0 | |
| for (word in words) { | |
| while (word.first() != text[startIndex]) { | |
| // A little bit hacky, but we need to shift the index if the first character don't match | |
| // the character at given index. This ensures that the newline character is skipped | |
| // and we actually calculate the word range properly. | |
| startIndex++ | |
| } | |
| val wordRange = startIndex until (startIndex + word.length) | |
| val rect = layoutResult.getBoundingBoxForRange(wordRange) | |
| wordRects.add(word to rect) | |
| startIndex += word.length + 1 // Move past the word and the following space | |
| } | |
| return wordRects | |
| } | |
| fun TextLayoutResult.getBoundingBoxForRange(range: IntRange): Rect { | |
| val start = range.first | |
| val end = range.last + 1 // Include last character | |
| val startBoundingBox = getBoundingBox(start) | |
| val endBoundingBox = getBoundingBox(end - 1) | |
| return Rect( | |
| startBoundingBox.left, | |
| startBoundingBox.top, | |
| endBoundingBox.right, | |
| endBoundingBox.bottom | |
| ).inflate(10f) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment