3

I have an Android programming question. Using the code below I want to validate a string match. It validates fine but LogCat is showing that the TextWatcher methods are firing twice per a keystroke and I can't figure out why. I would like for the firing to only occur once per keystroke.

Do you know why it's doing this?

I thought it might be because I change the color of the text but after commenting it out it didn't make a difference.

LogCat Output

03-31 03:37:25.269: I/BeforeText(676): Hit 
03-31 03:37:25.269: I/OnText(676): Hit
03-31 03:37:25.269: I/AfterText(676): Hit
03-31 03:37:25.274: I/InvalidText(676): Incorrect Text.
03-31 03:37:25.274: I/Text Value(676): a
03-31 03:37:25.404: I/BeforeText(676): Hit
03-31 03:37:25.404: I/OnText(676): Hit
03-31 03:37:25.404: I/AfterText(676): Hit
03-31 03:37:25.404: I/InvalidText(676): Incorrect Text.
03-31 03:37:25.404: I/Text Value(676): a

Activity Code

public void onCreate(Bundle savedInstanceState) {

     //...omitted

    //Create Answer Field
    textField = (EditText)this.findViewById(R.id.textField);

    //Add validation to TextField
    textField.addTextChangedListener(new TextWatcher(){
        public void afterTextChanged(Editable s){

            Log.i("AfterText","Hit");

            if(textField.getText().toString().trim().equalsIgnoreCase("hello")){
                Log.i("ValidText", "Text matched.");

                answerField.setTextColor(Color.GREEN);

            }
            else{
                Log.i("InvalidText", "Incorrect text.");
                Log.i("Text Value", textField.getText().toString());

                textField.setTextColor(Color.RED);

            }
        }

        public void beforeTextChanged(CharSequence s, int start, int count, int after){
            //Do nothing
            Log.i("BeforeText", "Hit");
        }

        public void onTextChanged(CharSequence s, int start, int before, int count){
            //Do nothing
            Log.i("OnText","Hit");

        }
    });
}
Romano Zumbé
  • 7,893
  • 4
  • 33
  • 55
AmarettoSlim
  • 75
  • 2
  • 6
  • Check by comparing the char sequence like `s.toString().trim().equalsIgnoreCase("hello")`. – Hamza Waqas Mar 31 '12 at 04:33
  • I have the same problem and I think I found where it comes from but... I still don't have the solution. In my case it seems that when you hit whitespace a new span is created and this seems to fire two events. If you try to change the edit text from 'a b' to 'a ' you will see the next error log: E/SpannableStringBuilder(24004): SPAN_EXCLUSIVE_EXCLUSIVE spans cannot have a zero length – Carlos Verdes Jan 27 '14 at 21:15

5 Answers5

3

As your Question is for TextWatcher methods are firing twice per a keystroke. You have use TextWather for Make watch on EditText for Validate String and set Color .

You can refer Document for TextWatcher in developer site here. http://developer.android.com/reference/android/text/TextWatcher.html.

As when you make press keystore it will make change in EditText text that way TextWatcher method onTextChanged call ,when you press any key for EditText method beforeTextChanged this will call when we start edit EditText.

One More thing that is when you enter one character in EditText ,it will call all this three method of Textwather.Just there sequence for Call are different.and also refre this SO Question Android TextWatcher.afterTextChanged vs TextWatcher.onTextChanged

So there is nothing wrong will call twice for Text Change in EditText.

Hope you get understand.

Community
  • 1
  • 1
Herry
  • 7,037
  • 7
  • 50
  • 80
  • Hi Herry, I have the same problem and no, I don't understand why the method is called twice. In my case only occurs when I hit whitespaces or & or similar weird characters. For example, if the text is 'a' and I press white space the method is called twice, one telling me the new string is 'a' and the second telling me that the text is 'a '. My problem is that the first method fires an onchange logic that makes me enter into an infinite loop. Could you please explain why the methods are called twice? – Carlos Verdes Jan 27 '14 at 20:59
  • When I say "the method" I mean afterTextChanged method. – Carlos Verdes Jan 27 '14 at 21:00
  • @CarlosVerdes When you have 'a' in your Edittext and now you are adding white space in EditText then it will call method in this sequence First beforeTextChange,onTextChanged and at last afterTextChanged. – Herry Jan 28 '14 at 06:49
  • @CarlosVerdes It will not call afterTextChanged method two Times for just adding white space in Edittext . – Herry Jan 28 '14 at 06:50
  • @Herry... believe me, it does call twice, one says the new val is 'a' and the second says the new val is 'a '. I think that the problem is with swipe which detects whitespace to show you suggestions. If you have swipe just try to do a simple test inserting and deleting whitespaces and you will see the weird behaviour (I did it in Nexus 5). Also check in my answer the url, it talks about this. – Carlos Verdes Jan 28 '14 at 08:40
1

I don't think if this would help you but in my case it seems to be the swipe keyboard.

The point is, when the swipe tries to make a suggestion the onchange methods seems to be called twice (one from EditText and another from swipe keyboard).

I don't know if this is feasible in your code, but just try this on your EditText to see what happen:

android:inputType="textNoSuggestions"

You can check the next site talking about this issue:

http://support.swiftkey.net/forums/116693-2-bug-reports/suggestions/2994580-span-exclusive-exclusive-spans-cannot-have-a-zero-

I will edit this answer if I found out a solution.


EDIT


My conclusion is that I can't avoid the intermediate events and I can't make the difference between a real change event from the user or a swipe change event.

Depending on your real problem you should try a different workaround. One workaround could be to save the information updated in a ConcurrentHashMap and schedule a task 100 milliseconds after the event is received. At the same time you should setup a "lastModified" date.

In the scheduled task you should ask "has elapsed 100 millisecond from last update?", if the answer is no --> do nothing (this means that another event has arrived so another updating task will be executed with the last value).

Acompany all with a ReentrantLock to reach atomicity.

Something like this:

   private final ReentrantLock lock = new ReentrantLock();
    private final ScheduledExecutorService scheduleExecutor = new ScheduledThreadPoolExecutor(1);
    private Date lastModification = null;
    private static final long UPDATE_WINDOW_IN_MILLISECONDS = 100;

protected class UpdateBusinessLogicTask implements Runnable {
    private final String newVal;

    public UpdateBusinessLogicTask(String newVal) {
        super();
        this.newVal = newVal;
    }

    @Override
    public void run() {

        //check current date
        Date now = new Date();
        //get lock to achieve atomicity 
        lock.lock();

        // calculate elapsed time
        long elapsedTime = lastModification.getTime() - now.getTime();

        // free the lock --> you could free the lock after business logic, depends on your scenario
        lock.unlock();

        //if the time is less than the updating window (100 milliseconds)
        if (elapsedTime < (UPDATE_WINDOW_IN_MILLISECONDS - 1)) {
            // DO NOTHING!!
            Log.d("TEST", "Dismiss update "+newVal);
        } else {
            Log.d("TEST", "Updating business with newVal: "+newVal);
            // TODO implement business logic
        }

    }
};

protected void onCreate(Bundle savedInstanceState) {

    EditText view = findViewById(R.id.nameEditText);

    TextWatcher tw = new TextWatcher() {

        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
            Log.d("TEST", "New text event received, new val: " + s.toString());

            lock.lock();
            lastModification = new Date();

            scheduleExecutor.schedule(new UpdateBusinessLogicTask(s.toString()), UPDATE_WINDOW_IN_MILLISECONDS, TimeUnit.MILLISECONDS);
            lock.unlock();
        }

        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
        }

        @Override
        public void afterTextChanged(Editable s) {
        }
    };

}
Carlos Verdes
  • 3,037
  • 22
  • 20
1

Maybe you have two textWatchers, the textField.addTextChangedListener method is called twice.

abentan
  • 518
  • 5
  • 6
0

You may use a boolean. Set it when the user changes the text , reset it when your custom code fires. The event still fires twice but your code is only called once. Best regards.

pouzzler
  • 1,800
  • 2
  • 20
  • 32
-1

Use this, it might help you:

myInput.addTextChangedListener(new TextWatcher() {
    boolean mToggle = false;

    public void onTextChanged(CharSequence cs, int s, int b, int c) {}

    public void afterTextChanged(Editable editable) {
        if (mToggle)  
            Toast.makeText(getBaseContext(), "HIT KEY", Toast.LENGTH_LONG).show();
        mToggle = !mToggle;
    }
});
George Brighton
  • 5,131
  • 9
  • 27
  • 36
Mr_Hmp
  • 2,474
  • 2
  • 35
  • 53