8

I need to detect text changes in an EditText. I've tried TextWatcher, but it doesn't work in a way I would expect it to. Take the onTextChanged method:

public void onTextChanged( CharSequence s, int start, int before, int count )

Say I have the text "John" in already in the EditText. If press another key, "e", s will be "Johne", start will be 0, before will be 4, and count will be 5. The way I would expect this method to work would be the difference between what the EditText previously was, and what it's about to become.

So I would expect:

s = "Johne"
start = 4 // inserting character at index = 4
before = 0 // adding a character, so there was nothing there before
count = 1 // inserting one character

I need to be able to detect individual changes every time a key is pressed. So if I have text "John", I need to know "e" was added at index 4. If I backspace "e", I need to know "e" was removed from index 4. If I put the cursor after "J" and backspace, I need to know "J" was removed from index 0. If I put a "G" where "J" was, I want to know "G" replaced "J" at index 0.

How can I achieve this? I can't think of a reliable way to do this.

Jason Robinson
  • 31,005
  • 19
  • 77
  • 131

4 Answers4

12

Use a textwatcher and do the diff yourself. store the previous text inside the watcher, and then compare the previous text to whatever sequence you get onTextChanged. Since onTextChanged is fired after every character, you know your previous text and the given text will differ by at most one letter, which should make it simple to figure out what letter was added or removed where. ie:

new TextWatcher(){ 
    String previousText = theEditText.getText();

    @Override 
    onTextChanged(CharSequence s, int start, int before, int count){
        compare(s, previousText); //compare and do whatever you need to do
        previousText = s;
    }

    ...
}
Sam Judd
  • 7,317
  • 1
  • 38
  • 38
  • It's a suggestion I've thought about, and certainly viable, but I was hoping for an easy "here you go!" method, which may not exist. The only problem with your approach is that the text can differ by more than 1 letter (selection range, pasting from clipboard). – Jason Robinson Feb 27 '12 at 16:14
  • 1
    True but at that point indices have little meaning (what happens if you paste a word before the current text for example?) and even then you can still do the diff yourself. I don't think there are any built in solutions for what you're trying to do. – Sam Judd Feb 27 '12 at 22:44
  • Fair enough. I ended up doing what you said and essentially implemented my own TextWatcher and passed in values I was expecting. It's not 100% fool proof but it's pretty close. – Jason Robinson Feb 28 '12 at 17:01
1

The best approach you can follow to identify text changes.

var previousText = ""


override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
    previousText = s.toString()
}

override fun onTextChanged(newText: CharSequence?, start: Int, before: Int, count: Int) {

    val position = start + before ;

    if(newText!!.length > previousText.length){ //Character Added
        Log.i("Added Character", " ${newText[position]} ")
        Log.i("At Position", " $position ")
    } else {  //Character Removed
        Log.i("Removed Character", " ${previousText[position-1]} ")
        Log.i("From Position", " ${position-1} ")
    }
}

override fun afterTextChanged(finalText: Editable?) { }
Saurabh Padwekar
  • 3,888
  • 1
  • 31
  • 37
0

You need to store and update the previous CharSequence every time the text is changed. You can do so by implementing the TextWatcher.

Example:

final CharSequence[] previousText = {""};
editText.addTextChangedListener(new TextWatcher()
{
    @Override
    public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2)
    {
    }

    @Override public void onTextChanged(CharSequence charSequence, int i, int i1, int i2)
    {
        if(i1 > 0)
        {
            System.out.println("Removed Chars Positions & Text:");
            for(int index = 0; index < i1; index++)
            {
                System.out.print((i + index) + " : " + previousText[0].charAt(i + index)+", ");
            }
        }
        if(i2 > 0)
        {
            System.out.println("Inserted Chars Positions & Text:");
            for(int index = 0; index < i2; index++)
            {
                System.out.print((index + i) + " : " + charSequence.charAt(i + index)+", ");
            }
            System.out.print("\n");
        }
        previousText[0] = charSequence.toString();//update reference
    }

    @Override public void afterTextChanged(Editable editable)
    {
    }
});
Menelaos Kotsollaris
  • 5,776
  • 9
  • 54
  • 68
0

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 -

  1. Old selection size
  2. Old text
  3. Old text sequence before the cursor/selection.
  4. 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 -

  1. New text sequence before the cursor/selection.
  2. 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?) {
        }
    }

Amol Jindal
  • 106
  • 4