Android TextWatcher Tutorial

View more Tutorials:

1- Android TextWatcher

As you know,  TextEdit allows the user to enter and modify text. TextEdit uses the TextWatcher ​​​​​​​interface to see changes that have occurred to its text, or to modify the textual content.
editText.addTextChangedListener(TextWatcher watcher)editText.addTextChangedListener(TextWatcher watcher)
Methods of the  TextWatcher interface are:
  • void afterTextChanged(Editable s)
  • void beforeTextChanged(CharSequence s, int start, int count, int after)
  • void onTextChanged(CharSequence s, int start, int before, int count)
TextWatcher ​​​​​​​can be used to ensure that the user enters text that matches a given pattern.

2- TextWatcher Methods

3- Example: Date Pattern

In this example, we are going to use the  TextWatcher to make sure that the user enters a valid date in the format​​​​​​​ of  DD/MM/YYYY.
DateFormatTextWatcher.java
package org.o7planning.textwatcherdateexample;

import android.text.Editable;
import android.text.TextWatcher;
import android.widget.EditText;

import java.util.Calendar;

public class DateFormatTextWatcher implements TextWatcher  {

    private static final String DDMMYYYY = "DDMMYYYY";
    private static final String SEPARATOR = "/";

    private final Calendar calendar = Calendar.getInstance();

    private String currentText = "";

    private EditText editText;

    public DateFormatTextWatcher(EditText editText)  {
        this.editText = editText;
    }

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

    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
        if (!s.toString().equals(this.currentText)) {
            // Remove all non-digit.
            String newTextClean = s.toString().replaceAll("[^\\d.]|\\.", "");
            String currentTextClean = this.currentText.replaceAll("[^\\d.]|\\.", "");

            int newTextLength = newTextClean.length();

            // Cursor Position Index.
            int selectionIndex = newTextLength;
            for (int i = 2; i <= newTextLength && i < 6; i += 2) {
                selectionIndex++;
            }
            // Fix for pressing delete next to a forward slash
            if (newTextClean.equals(currentTextClean))  {
                selectionIndex--;
            }

            if (newTextClean.length() < 8) {
                newTextClean = newTextClean + this.DDMMYYYY.substring(newTextClean.length());
            } else {
                // This part makes sure that when we finish entering numbers
                // the date is correct, fixing it otherwise
                int day  = Integer.parseInt(newTextClean.substring(0,2));
                int month  = Integer.parseInt(newTextClean.substring(2,4));
                int year = Integer.parseInt(newTextClean.substring(4,8));

                month = month < 1 ? 1 : month > 12 ? 12 : month;
                this.calendar.set(Calendar.MONTH, month-1);

                year = (year < 1900)? 1900:(year > 2100)? 2100 : year;
                this.calendar.set(Calendar.YEAR, year);

                // ^ first set year for the line below to work correctly
                // with leap years - otherwise, date e.g. 29/02/2012
                // would be automatically corrected to 28/02/2012

                day = (day > this.calendar.getActualMaximum(Calendar.DATE))? this.calendar.getActualMaximum(Calendar.DATE):day;

                newTextClean = String.format("%02d%02d%02d",day, month, year);
            }
            // "%s/%s/%s"
            String format = "%s" + SEPARATOR + "%s" + SEPARATOR +"%s";
            newTextClean = String.format(format, newTextClean.substring(0, 2),
                    newTextClean.substring(2, 4),
                    newTextClean.substring(4, 8));

            selectionIndex = selectionIndex < 0 ? 0 : selectionIndex;
            this.currentText = newTextClean;

            this.editText.setText(this.currentText);
            this.editText.setSelection(selectionIndex < this.currentText.length() ? selectionIndex : this.currentText.length());
        }
    }

    @Override
    public void afterTextChanged(Editable s) {

    }
}
​​​​​​​Here is the application interface:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/textView71"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginLeft="16dp"
        android:layout_marginTop="32dp"
        android:layout_marginEnd="16dp"
        android:layout_marginRight="16dp"
        android:text="Birthday:"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/editText_birthDay"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginLeft="16dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="16dp"
        android:layout_marginRight="16dp"
        android:ems="10"
        android:hint="DD/MM/YYYY"
        android:inputType="date"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView71" />

</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity.java
package org.o7planning.textwatcherdateexample;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.text.TextWatcher;
import android.widget.EditText;

public class MainActivity extends AppCompatActivity {

    private EditText editText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        this.editText = (EditText) this.findViewById(R.id.editText_birthDay);

        // Create TextWatcher:
        TextWatcher textWatcher = new DateFormatTextWatcher(this.editText);
        this.editText.addTextChangedListener(textWatcher);

    }
}

4- Example: Number

NumberTextWatcher.java
package org.o7planning.textwatchernumberexample;

import android.text.Editable;
import android.text.TextWatcher;
import android.text.method.DigitsKeyListener;
import android.util.Log;
import android.widget.EditText;

import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.ParseException;
import java.util.Locale;


public class NumberTextWatcher implements TextWatcher {

    private static final String LOG_TAG = "AndroidExample";

    private final int numDecimals;
    private String groupingSeparator;
    private String decimalSeparator;
    private boolean nonUsFormat;

    private DecimalFormat decimalFormatDec;
    private DecimalFormat decimalFormatInt;

    private boolean hasFractionalPart;

    private EditText editText;
    private String value;

    public NumberTextWatcher(EditText editText, Locale locale, int numDecimals) {
        this.editText = editText;
        this.numDecimals = numDecimals;
        this.hasFractionalPart = false;

        this.editText.setKeyListener(DigitsKeyListener.getInstance("0123456789.,"));

        DecimalFormatSymbols symbols = new DecimalFormatSymbols(locale);

        char gs = symbols.getGroupingSeparator();
        char ds = symbols.getDecimalSeparator();
        this.groupingSeparator = String.valueOf(gs);
        this.decimalSeparator = String.valueOf(ds);

        String patternInt = "#,###";
        this.decimalFormatInt = new DecimalFormat(patternInt, symbols);

        String patternDec = patternInt + "." + replicate('#', this.numDecimals);
        this.decimalFormatDec = new DecimalFormat(patternDec, symbols);
        this.decimalFormatDec.setDecimalSeparatorAlwaysShown(true);
        this.decimalFormatDec.setRoundingMode(RoundingMode.DOWN);

        this.nonUsFormat = !this.decimalSeparator.equals(".");
        this.value = null;
    }


    @Override
    public void afterTextChanged(Editable s) {
        Log.d(LOG_TAG, "afterTextChanged");
        this.editText.removeTextChangedListener(this);

        try {
            int initLeng = this.editText.getText().length();

            String v = this.value.replace(this.groupingSeparator, "");

            Number n = this.decimalFormatDec.parse(v);

            int selectionStart = this.editText.getSelectionStart();
            if (this.hasFractionalPart) {
                int decPos = v.indexOf(this.decimalSeparator) + 1;
                int decLen = v.length() - decPos;
                if (decLen > this.numDecimals) {
                    v = v.substring(0, decPos + this.numDecimals);
                }
                int trz = countTrailingZeros(v);

                StringBuilder fmt = new StringBuilder(this.decimalFormatDec.format(n));
                while (trz-- > 0) {
                    fmt.append("0");
                }
                this.editText.setText(fmt.toString());
            } else {
                this.editText.setText(this.decimalFormatInt.format(n));
            }

            int endLeng = this.editText.getText().length();
            int selection = (selectionStart + (endLeng - initLeng));
            if (selection > 0 && selection <= this.editText.getText().length()) {
                this.editText.setSelection(selection);
            } else {
                // Place cursor at the end?
                this.editText.setSelection(this.editText.getText().length() - 1);
            }
        } catch (NumberFormatException | ParseException nfe) {
            // Do nothing?
        }
        this.editText.addTextChangedListener(this);
    }

    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
        Log.d(LOG_TAG, "beforeTextChanged");
        this.value = this.editText.getText().toString();
    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
        Log.d(LOG_TAG, "onTextChanged");

        String newValue = s.toString();
        String change = newValue.substring(start, start + count);
        String prefix = this.value.substring(0, start);
        String suffix = this.value.substring(start + before);

        if (".".equals(change) && this.nonUsFormat) {
            change = this.decimalSeparator;
        }

        this.value = prefix + change + suffix;
        this.hasFractionalPart = this.value.contains(this.decimalSeparator);

        Log.d(LOG_TAG, "VALUE: " + this.value);
    }

    private int countTrailingZeros(String str) {
        int count = 0;

        for (int i = str.length() - 1; i >= 0; i--) {
            char ch = str.charAt(i);
            if ('0' == ch) {
                count++;
            } else {
                break;
            }
        }
        return count;
    }

    private String replicate(char ch, int n) {
        return new String(new char[n]).replace("\0", "" + ch);
    }

}
​​​​​​​Here is the application interface:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/textView71"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginLeft="16dp"
        android:layout_marginTop="32dp"
        android:layout_marginRight="16dp"
        android:text="Enter a Number:"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/editText_number"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginLeft="16dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="16dp"
        android:layout_marginRight="16dp"
        android:ems="10"
        android:inputType="number"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView71" />

</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity.java
package org.o7planning.textwatchernumberexample;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.text.TextWatcher;
import android.widget.EditText;

import java.util.Locale;

public class MainActivity extends AppCompatActivity {

    private EditText editTextNumber;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        this.editTextNumber = (EditText) this.findViewById(R.id.editText_number) ;

        Locale locale = new Locale("en", "US");
        int numDecs = 2; // Let's use 2 decimals

        TextWatcher textWatcher = new NumberTextWatcher(this.editTextNumber, locale, numDecs);
        this.editTextNumber.addTextChangedListener(textWatcher);
    }
}

View more Tutorials: