Skip to content

Instantly share code, notes, and snippets.

@andrei-fedorov
Forked from hipczor/TextHighlight.kt
Created December 9, 2024 21:19
Show Gist options
  • Select an option

  • Save andrei-fedorov/75aa50e02b3ad7ceda274a1b3957e9ec to your computer and use it in GitHub Desktop.

Select an option

Save andrei-fedorov/75aa50e02b3ad7ceda274a1b3957e9ec to your computer and use it in GitHub Desktop.
Text Highlight in Jetpack Compose
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