First of all, I highlighted the words to be replaced by * so that they can be easily found using a regular expression.
val string = """
TAKE, O take those lips *away*
That so sweetly were forsworn,
And those eyes, the break of day,
Lights that do mislead the morn:
But my *kisses* bring again,
Bring again—
*Seals* of love, *but* seal’d in vain,
Seal’d in vain!
– William Shakespeare
""".trimIndent()
val matches = remember(string) { Regex("\\*\\w+\\*").findAll(string) }
Using the onTextLayout argument in Compose Text, you can get a lot of information about the text to be rendered, including the positions of each character. And the indexes of the characters you need to replace are already defined by a regular expression.
All you have to do is place the text fields at the appropriate positions.
I use BasicTextField because it doesn't have the extra padding that TextField has, so the size is easy to match with Text. I set its background to white so that the original text doesn't shine through. If you have an unusual background, a gradient for example, you can also make the text transparent with annotated text as shown in the documentation, then the BasicTextField can be left transparent.
The SubcomposeLayout is a great tool for creating such layouts without waiting a next recomposition to use onTextLayout result.
val textLayoutState = remember { mutableStateOf<TextLayoutResult?>(null) }
val textFieldTexts = remember(matches.count()) { List(matches.count()) { "" }.toMutableStateList() }
val style = MaterialTheme.typography.body1
SubcomposeLayout { constraints ->
val text = subcompose("text") {
Text(
text = string,
style = style,
onTextLayout = {
textLayoutState.value = it
},
)
}[0].measure(constraints)
val textLayout = textLayoutState.value ?: run {
// shouldn't happen as textLayoutState is updated during sub-composition
return@SubcomposeLayout layout(0, 0) {}
}
val wordsBounds = matches.map {
// I expect all selected words to be on a single line
// otherwise path bounds will take both lines
textLayout
.getPathForRange(it.range.first, it.range.last + 1)
.getBounds()
}
val textFields = wordsBounds.mapIndexed { i, wordBounds ->
subcompose("textField$i") {
BasicTextField(
value = textFieldTexts[i],
onValueChange = {
textFieldTexts[i] = it
},
onTextLayout = {
println("${it.size}")
},
textStyle = style,
modifier = Modifier
.border(1.dp, Color.LightGray)
.background(Color.White)
)
}[0].measure(Constraints(
maxWidth = floor(wordBounds.width).toInt(),
)) to wordBounds.topLeft
}
layout(text.width, text.height) {
text.place(0, 0)
textFields.forEach {
val (placeable, position) = it
placeable.place(floor(position.x).toInt(), floor(position.y).toInt())
}
}
}
Result:
