1

I'm trying to create a mentions feature in Jetpack Compose. I found a library which uses an EditText and I of course could use that, but I really want to create this in Jetpack Compose. The only problem is, in some cases it's hard to keep track of the changes to the text. For example if a person moves the cursor to a word and the keyboard shows suggestions and the user clicks it. When using EditText you can use beforeTextChanged and onTextChanged and it tells the start of the change, the length before the change and the length after the change.

So my question is, is there a somewhat equal method for Jetpack Compose TextField or a way to get these values?

Gabriele Mariotti
  • 320,139
  • 94
  • 887
  • 841
Kevin van Mierlo
  • 9,554
  • 5
  • 44
  • 76

2 Answers2

0

You can use the onValueChange property:

var text by remember { mutableStateOf("") }

TextField(
    value = text,
    onValueChange = {
       // text= oldValue
       // it = newValue
    }
)
Gabriele Mariotti
  • 320,139
  • 94
  • 887
  • 841
  • I get that I can use that method, but how do I get the specific changed value. My example in the question is for example the user moves the cursor to the middle of the word, presses the suggestion on the keyboard. Now a whole word has changed. How can I check what specific part has changed now? For example: "Hello there Kevin". If I press "there" and keyboard suggests "you", I should get the index of "there", the length of "there" and new length of "you". Like beforeTextChanged worked. This way I can check what has changed and if my mention is being edited or removed. – Kevin van Mierlo Sep 01 '22 at 09:20
  • @KevinvanMierlo you can only compare the 2 strings to get it – Gabriele Mariotti Sep 01 '22 at 09:50
  • Well the TextField also has a TextFieldValue in which you can also check the cursor and such. I come a long way with checking both. But if the keyboard replaces an entire word it's really hard to check what exactly changed. Which was my question – Kevin van Mierlo Sep 01 '22 at 10:40
0

After trying some possibilities I feel like I found the way to get the diff of which text has changed when typing in a TextField in Jetpack Compose. I will explain it with some code and also provide the full source if you want to use mentions in Jetpack Compose.

First thing we have to do is find the first difference between two strings. I use this code:

// Find first difference between two strings
private fun String.indexOfDifference(otherString: String): Int {
    if (this.contentEquals(otherString)) {
        return -1
    }
    for (i in 0 until min(this.length, otherString.length)) {
        if (this[i] != otherString[i]) {
            return i
        }
    }
    if (this.length != otherString.length) {
        return min(this.length, otherString.length)
    }
    return -1
}

One fault of this code, which for me doesn't really matter is if there are multiple of the same characters in a row. For example if you have a space and add a space before that space, it thinks the change happened at the second space instead of the first.

After knowing the first difference of the text we need to know the length of the diff in the old and new string:

    // Go through the text and find where the texts differentiate
    private fun getDiffRange(indexOfDiffStart: Int, oldText: String, newText: String): Pair<IntRange, IntRange> {
        val newLastIndex = max(0, newText.length)
        val newStartIndex = min(indexOfDiffStart, newLastIndex)

        val oldLastIndex = max(0, oldText.length)
        val oldStartIndex = min(indexOfDiffStart, oldLastIndex)
        var loopIndex = oldStartIndex
        var oldTextIndex = -1
        while(loopIndex <= oldLastIndex) {
            // From where texts differentiates, loop through old text to find at what index the texts will be the same again
            oldTextIndex = newText.indexOf(oldText.substring(loopIndex, oldLastIndex))
            if(oldTextIndex >= 0) {
                break
            }
            loopIndex++
        }
        if(oldTextIndex >= 0) {
            return Pair(first = oldStartIndex .. loopIndex, second = newStartIndex .. max(0, oldTextIndex))
        }
        return Pair(first = oldStartIndex .. oldLastIndex, second = newStartIndex .. newLastIndex)
    }

The method above will loop through the old text until we can find the entire remaining of the old text in the new text. This way we know the location and length of what has changed in the old text and also immediately know the same for the new text.

In the onValueChange method of the TextField you can check for the first difference:

if (oldTextFieldValue.text.contentEquals(newTextFieldValue.text)) {
    // Content stayed the same, probably cursor change
} else {
    val indexOfDiff = oldTextFieldValue.text.indexOfDifference(newTextFieldValue.text)
    if (indexOfDiff >= 0) {
        val (oldDiffRange, newDiffRange) = getDiffRange(indexOfDiff, oldTextFieldValue.text, newTextFieldValue.text)
    }
}

This was the information I needed to handle the mentions. Maybe this already helps somebody that has the same problem. But if you want to see my full mentions implementation you can find it here: https://gist.github.com/kevinvanmierlo/4bd011479c66eed598852ffeacdc0156

Kevin van Mierlo
  • 9,554
  • 5
  • 44
  • 76