Android TextWatcher Tutorial
View more Tutorials:
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.

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); } }

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); } }