starkej2
9/22/2016 - 8:41 PM

A robust TextWatcher that groups input into the specified group sizes, with separators between them.

A robust TextWatcher that groups input into the specified group sizes, with separators between them.

/**
 * Formats the watched EditText to groups of characters, with separators between them.
 */
public class GroupedInputFormatWatcher implements TextWatcher {
    private char separator;
    private String separatorString;
    private int groupSize;

    /**
     * Breakdown of this regex:
     * ^             - Start of the string
     * (\\d{4}\\s)*  - A group of four digits, followed by a whitespace, e.g. "1234 ". Zero or more
     * times.
     * \\d{0,4}      - Up to four (optional) digits.
     * (?<!\\s)$     - End of the string, but NOT with a whitespace just before it.
     * <p>
     * Example of matching strings:
     * - "2304 52"
     * - "2304"
     * - ""
     */
    private final String regex = "^(\\d{4}\\s)*\\d{0,4}(?<!\\s)$";
    private boolean isUpdating = false;
    private final EditText editText;

    public GroupedInputFormatWatcher(@NonNull EditText editText) {
        this(editText, 4, ' ');
    }

    public GroupedInputFormatWatcher(@NonNull EditText editText, int groupSize, char separator) {
        this.editText = editText;
        this.groupSize = groupSize;
        this.separator = separator;
        this.separatorString = String.valueOf(separator);
    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {

    }

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

    }

    @Override
    public void afterTextChanged(Editable s) {
        String originalString = s.toString();

        // Check if we are already updating, to avoid infinite loop.
        // Also check if the string is already in a valid format.
        if (isUpdating || originalString.matches(regex)) {
            return;
        }

        // Set flag to indicate that we are updating the Editable.
        isUpdating = true;

        // First all whitespaces must be removed. Find the index of all whitespace.
        LinkedList<Integer> spaceIndices = new LinkedList<Integer>();
        for (int index = originalString.indexOf(separator); index >= 0; index = originalString.indexOf(separator, index + 1)) {
            spaceIndices.offerLast(index);
        }

        // Delete the whitespace, starting from the end of the string and working towards the beginning.
        Integer spaceIndex = null;
        while (!spaceIndices.isEmpty()) {
            spaceIndex = spaceIndices.removeLast();
            s.delete(spaceIndex, spaceIndex + 1);
        }

        // Loop through the string again and add whitespaces in the correct positions
        for (int i = 0; ((i + 1) * groupSize + i) < s.length(); i++) {
            s.insert((i + 1) * groupSize + i, separatorString);
        }

        // Finally check that the cursor is not placed before a whitespace.
        // This will happen if, for example, the user deleted the digit '5' in
        // the string: "1234 567".
        // If it is, move it back one step; otherwise it will be impossible to delete
        // further numbers.
        int cursorPos = editText.getSelectionStart();
        if (cursorPos > 0 && s.charAt(cursorPos - 1) == separator) {
            editText.setSelection(cursorPos - 1);
        }

        isUpdating = false;
    }
}