o7planning

Android TextWatcher Tutorial with Examples

  1. Android TextWatcher
  2. TextWatcher Methods
  3. Example: Date Pattern
  4. Example: Number

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

Android Programming Tutorials

Show More