I faced the exact same problem recently and I wrote my own custom algorithm to detect the diff from the TextWatcher output.
Algorithm -
We store 4 things -
- Old selection size
- Old text
- Old text sequence before the cursor/selection.
- Old text sequence after the cursor/selection.
Above 4 things are updated during the beforeTextChanged()
callback.
Now during the onTextChanged()
callback, we compute following two things -
- New text sequence before the cursor/selection.
- New text sequence after the cursor/selection.
Now following cases are possible -
Case 1
New text sequence before the cursor/selection == Old text sequence before the cursor/selection
AND New text sequence after the cursor/selection isASuffixOf Old text sequence after the cursor/selection
This is a delete forward case. The number of deleted characters can be calculated by the oldText length minus the newText length.
Example -
Old text = Hello wo|rld (|
represents the cursor)
Old text sequence before the cursor/selection = Hello wo
Old text sequence after the cursor/selection = rld
Old selection size = 0
New text = Hello wo|ld (|
represents the cursor)
New text sequence before the cursor/selection = Hello wo
New text sequence after the cursor/selection = ld
Clearly, this is a case of delete in forward direction by 1 character.
Case 2
New text sequence after the cursor/selection == Old text sequence after the cursor/selection
AND New text sequence before the cursor/selection isAPrefixOf Old text sequence before the cursor/selection
This is a delete backward case. The number of deleted characters can be calculated by the oldText length minus the newText length.
Example -
Old text = Hello wo|rld (|
represents the cursor)
Old text sequence before the cursor/selection = Hello wo
Old text sequence after the cursor/selection = rld
Old selection size = 0
New text = Hello w|rld (|
represents the cursor)
New text sequence before the cursor/selection = Hello w
New text sequence after the cursor/selection = rld
Clearly, this is a case of delete in backward direction by 1 character.
Case 3
New text sequence after the cursor/selection == Old text sequence after the cursor/selection
AND Old text sequence before the cursor/selection isAPrefixOf New text sequence before the cursor/selection
This is an insert case. The exact insertion string can be calculated by removing the old text sequence from cursor + old text sequence after cursor
from the new text string.
Example -
Old text = Hello wo|rld (|
represents the cursor)
Old text sequence before the cursor/selection = Hello wo
Old text sequence after the cursor/selection = rld
Old selection size = 0
New text = Hello wo123|rld (|
represents the cursor)
New text sequence before the cursor/selection = Hello wo123
New text sequence after the cursor/selection = rld
Clearly, this is a case of insert and inserted string is 123
.
Case 4
If none of the above cases are satisfied, then we can say that it is a replace case. And the replace data is already provided by TextWatcher in the onTextChanged
callback.
Here is the code for above algorithm -
class MyTextWatcher : android.text.TextWatcher {
var oldSelectionSize = 0
var oldText: String = ""
var oldSequenceBeforeCursor: String = ""
var oldSequenceAfterCursor: String = ""
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
oldSelectionSize = editText.selectionEnd - editText.selectionStart
oldText = s.toString()
oldSequenceBeforeCursor = s?.subSequence(0, editText.selectionStart).toString()
oldSequenceAfterCursor = s?.subSequence(editText.selectionEnd, s.length).toString()
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
s?.toString()?.let { newText ->
val newSequenceBeforeCursor = newText.subSequence(0, selectionStart).toString()
val newSequenceAfterCursor = newText.subSequence(selectionEnd, newText.length)
.toString()
if (newSequenceBeforeCursor == oldSequenceBeforeCursor &&
oldSequenceAfterCursor.endsWith(newSequenceAfterCursor))
// handle delete forward
// number of characters to delete ==>
// if(oldSelectionSize > 0) then deleted number of characters = oldSelectionSize
// else number of characters to delete = oldText.length - newText.length
else if (newSequenceAfterCursor == oldSequenceAfterCursor &&
oldSequenceBeforeCursor.startsWith(newSequenceBeforeCursor))
// handle delete backward
// number of characters to delete ==>
// if(oldSelectionSize > 0) then deleted number of characters = oldSelectionSize
// else number of characters to delete = oldText.length - newText.length
else if (newSequenceAfterCursor == oldSequenceAfterCursor &&
newSequenceBeforeCursor.startsWith(oldSequenceBeforeCursor))
// handle insert
// inserted string = (newText - oldSequenceBeforeCursor) - oldSequenceAfterCursor
else
// handle replace
// replace info already provided in `onTextChanged()` arguments.
}
}
override fun afterTextChanged(s: Editable?) {
}
}