Hướng dẫn sử dụng Android TextWatcher

Xem thêm các chuyên mục:

1- Android TextWatcher

Như bạn đã biết TextEdit cho phép người dùng nhập và sửa đổi văn bản. TextEdit sử dụng interface TextWatcher để xem các thay đổi xẩy ra đối với văn bản của nó, hoặc sửa đổi nội dung văn bản.
editText.addTextChangedListener(TextWatcher watcher)editText.addTextChangedListener(TextWatcher watcher)
Các phương thức của interface TextWatcher:
  • 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 có thể được sử dụng để đảm bảo rằng người dùng sẽ nhập vào một văn bản phù hợp với một khuôn mẫu (pattern) cho trước.

2- TextWatcher Methods

3- Example: Date Pattern

Trong ví dụ này chúng ta sẽ sử dụng TextWatcher để đảm bảo rằng người dùng sẽ nhập vào một ngày tháng (date) hợp lệ theo định dạng 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) {

    }
}
Giao diện của ví dụ:
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);
    }

}
Giao diện của ví dụ:
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);
    }
}

Xem thêm các chuyên mục: