Dewerro
Dewerro

Reputation: 607

How to replace parts of Text with TextFields in Jetpack Compose?

For example i have this:

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

And i want to replace some words to TextField(in Compose) or to EditText(in XML). Example:

TAKE, O take those lips
That so were forsworn,
And those eyes, the break of day,
Lights that do mislead the morn:
But my bring again,
Bring again—
of love, seal’d in vain,
Seal’d in vain!

– William Shakespeare


Can you advise me the way to realise it? Maybe specific libraries?

Upvotes: 3

Views: 1679

Answers (1)

Phil Dukhov
Phil Dukhov

Reputation: 87774

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:

Upvotes: 4

Related Questions