78

How to make EditText accept input in format:

4digit 4digit 4digit 4digit 

I tried Custom format edit text input android to accept credit card number, but unfortunately I was unable to delete the spaces. Whenever there is a space, I could not to delete it. Please help me in finding out the issue.

Community
  • 1
  • 1
Preethi
  • 2,112
  • 7
  • 38
  • 54

29 Answers29

101

After finding multiple answers that are 'OK'. I moved towards a better TextWatcher which is designed to work correctly and independently from the TextView.

TextWatcher class is as follows:

/**
 * Formats the watched EditText to a credit card number
 */
public static class FourDigitCardFormatWatcher implements TextWatcher {

    // Change this to what you want... ' ', '-' etc..
    private static final char space = ' ';

    @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) {
        // Remove spacing char
        if (s.length() > 0 && (s.length() % 5) == 0) {
            final char c = s.charAt(s.length() - 1);
            if (space == c) {
                s.delete(s.length() - 1, s.length());
            }
        }
        // Insert char where needed.
        if (s.length() > 0 && (s.length() % 5) == 0) {
            char c = s.charAt(s.length() - 1);
            // Only if its a digit where there should be a space we insert a space
            if (Character.isDigit(c) && TextUtils.split(s.toString(), String.valueOf(space)).length <= 3) {
                s.insert(s.length() - 1, String.valueOf(space));
            }
        }
    }
}

Then add it to your TextView as you would any other TextWatcher.

{
  //...
  mEditTextCreditCard.addTextChangedListener(new FourDigitCardFormatWatcher()); 
}

This will auto delete the space sensibly going back so the user can actually do less keystrokes when editing.

Caveat

If you are using inputType="numberDigit" this will disable the '-' and ' ' chars, so I recommend using, inputType="phone". This enables other chars, but just use a custom inputfilter and problem solved.

NPike
  • 13,136
  • 12
  • 63
  • 80
Chris.Jenkins
  • 13,051
  • 4
  • 60
  • 61
  • 28
    I'd also like to add that you can keep the inputType as number and use android:digit="0123456789 _". – mpellegr Feb 18 '14 at 19:55
  • 1
    @mpellegr This works for spaces but if you use a dash as your separator (which shows up on the 'number' input keyboard), users can type it in. A simple fix is to remove the ```&& (s.length() % 5) == 0``` in the first if statement. – Tom Jan 28 '15 at 20:55
  • 11
    This does not support mid-string editing... the user can move to the middle of the field and add/delete stuff and the spacing will not be correct. – Tom Jan 28 '15 at 21:10
  • @Tom you are correct, Would be interested to see if you are able to improve it, i guess would need to parse the entire String for every entry and format it accordingly. Should be pretty simple. – Chris.Jenkins Jan 30 '15 at 10:31
  • At your request I've posted my solution :). It has only one drawback (can't delete mid-string spacing characters), but the result is guaranteed to be in the correct format. – Tom Feb 02 '15 at 01:36
  • @Chris.Jenkins, I added another solution which does allow removing mid-string spacing. – MW. Apr 21 '15 at 13:49
  • 24
    Just spent 15 minutes trying to use android:digit="0123456789 _" (as mentioned by @mpellegr), with no luck, until I saw here - http://stackoverflow.com/questions/3402693/how-to-set-input-type-to-be-numberdecimal-but-also-allow-a that it's "digits" and not "digit". Maybe it will help someone. – Aviv Ben Shabat Dec 21 '15 at 12:32
  • 1
    @Chris.Jenkins What if I want to enter a 19digit number from user, it is not working. Can you help me please – Gaurav Arora May 11 '16 at 06:41
  • @GauravArora thats out of scope of the original question. I would open a new question. – Chris.Jenkins May 26 '16 at 13:51
  • @Chris.Jenkins any luck with 17-19 digits ? I have a new type of card VERVE, which can be 17-19 digits. Have to do the same in the same watcher with Master and VISA cards – Gaurav Arora Jul 19 '17 at 04:27
  • It can not work well during mid-string operation. I post my solution can work well whatever mid-string or copy-paster etc. Hope that can help u. @GauravArora – Luna Kong Aug 07 '18 at 08:43
  • I think `&& (s.length() % 5) == 0` in the first if condition, in method `afterTextChanged`, below `// Remove spacing char` is not needed since we don't want user to type the space character directly – HendraWD Aug 21 '18 at 06:44
  • @Chris.Jenkins If my app set the Arab language, When there is a space, it will be confused.The order of display is not a normal order – Better Jan 18 '19 at 03:42
  • This does not work when card digits are removed from middle of entered text or digits are entered from the first digit position and many such cases. Can anyone tell me how to fix this? – Kruti Parekh Nov 29 '19 at 12:58
  • not working well. https://stackoverflow.com/a/33072227/5853262 is better – Mahdi Moqadasi Apr 12 '21 at 10:45
88

Demo - How this works

Example on github.com

Late answer, but I guess it may helpful for somebody:

    cardNumberEditText.addTextChangedListener(new TextWatcher() {

        private static final int TOTAL_SYMBOLS = 19; // size of pattern 0000-0000-0000-0000
        private static final int TOTAL_DIGITS = 16; // max numbers of digits in pattern: 0000 x 4
        private static final int DIVIDER_MODULO = 5; // means divider position is every 5th symbol beginning with 1
        private static final int DIVIDER_POSITION = DIVIDER_MODULO - 1; // means divider position is every 4th symbol beginning with 0
        private static final char DIVIDER = '-';

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

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

        @Override
        public void afterTextChanged(Editable s) {
            if (!isInputCorrect(s, TOTAL_SYMBOLS, DIVIDER_MODULO, DIVIDER)) {
                s.replace(0, s.length(), buildCorrectString(getDigitArray(s, TOTAL_DIGITS), DIVIDER_POSITION, DIVIDER));
            }
        }

        private boolean isInputCorrect(Editable s, int totalSymbols, int dividerModulo, char divider) {
            boolean isCorrect = s.length() <= totalSymbols; // check size of entered string
            for (int i = 0; i < s.length(); i++) { // check that every element is right
                if (i > 0 && (i + 1) % dividerModulo == 0) {
                    isCorrect &= divider == s.charAt(i);
                } else {
                    isCorrect &= Character.isDigit(s.charAt(i));
                }
            }
            return isCorrect;
        }

        private String buildCorrectString(char[] digits, int dividerPosition, char divider) {
            final StringBuilder formatted = new StringBuilder();

            for (int i = 0; i < digits.length; i++) {
                if (digits[i] != 0) {
                    formatted.append(digits[i]);
                    if ((i > 0) && (i < (digits.length - 1)) && (((i + 1) % dividerPosition) == 0)) {
                        formatted.append(divider);
                    }
                }
            }

            return formatted.toString();
        }

        private char[] getDigitArray(final Editable s, final int size) {
            char[] digits = new char[size];
            int index = 0;
            for (int i = 0; i < s.length() && index < size; i++) {
                char current = s.charAt(i);
                if (Character.isDigit(current)) {
                    digits[index] = current;
                    index++;
                }
            }
            return digits;
        }
    });

this works perfectly with start-string/end-string/mid-string editing, also paste works perfectly.

azizbekian
  • 60,783
  • 13
  • 169
  • 249
Igor Tyulkanov
  • 5,487
  • 2
  • 32
  • 49
  • 9
    Doesn't work perfectly.. If you enter numbers, then place the cursor back at the beginning of the field and type '12345' it ends up as '1234 65' – James Goodwin Jan 05 '16 at 17:01
  • 3
    @IgorTyulkanov, I added your code to my app and I can add only 4 numbers in my EditText, what I've forgotten? – piotrek1543 Jan 07 '16 at 13:40
  • @piotrek1543, could you post your code in gist e. g. I'll try to check it – Igor Tyulkanov Jan 07 '16 at 14:33
  • ok, i find my mistake - bad `inputType` :-) Solved and working fine, +1 – piotrek1543 Jan 07 '16 at 14:55
  • 8
    Awesome! Thanks man! Also check that `android:digits="0123456789 "` contains the CARD_NUMBER_DIVIDER (beware of the space at the end) because otherwise it will crash with a stackoverflow! Thanks again! – Andrei Verdes Jan 25 '16 at 12:56
  • What is this method 'buildCorrecntString'? Edit: nvm didn't scroll down lol – PLOW Mar 20 '16 at 03:59
  • @PLOW, I just remove there all symbols besides digits, and build new string with those digits and dividers. So if you paste for example something like this "ugg6544+&544", first we find all digits - "6544544" and then build correct string - "6544-544" – Igor Tyulkanov Mar 20 '16 at 04:07
  • I just tried this, and it still has the problem JamesGoodwin noted. – stevehs17 Mar 21 '16 at 22:17
  • @IgorTyulkanov I followed your code in github but it does not work as per the gif above. Any ideas? – QWERTY May 08 '17 at 08:23
  • I'm trying to implement this for 6 digits with a divider after each digit, I tried changing the values of the constants but my app crashed, an error showed up in the logcat but I couldn't read it because there was too much output to process, any suggestions? – Odai Mohammed Oct 03 '17 at 23:49
  • @OdaiMohammed it's a StackOverflowError, this is not a valid response. – Rafael Ruiz Muñoz Dec 27 '18 at 16:11
  • This is how I fixed the stackoverflow, add an if above `if (!isInputCorrect(s, TOTAL_SYMBOLS, DIVIDER_MODULO, DIVIDER))` checking the length of the editable against your total symbols. This stops the leaky part of the code from executing after your length is reached. `if (s.length() <= TOTAL_SYMBOLS) ` Then in the XML add this to your EditText (or figure out filters to do this via Java) `android:maxLength="5"` Change the 5 to the **total length** that includes dashes – paxton91michael Aug 19 '19 at 04:08
  • @paxton91michael can you please past your code, it doesn't work even with your suggestion. – Androbito Sep 19 '19 at 15:24
  • @Androbito Absolutely. Sorry I didn't before. Here is my entire code for formatting a 16-digit card number: [link](https://pastebin.com/mD57ZrPd) – paxton91michael Sep 19 '19 at 17:03
  • When erasing, and the last character is a space, it should delete the last digit too. – John Sardinha Sep 23 '19 at 23:03
  • @Andrei Verdes Thanks Man – Aravindhan Gs Nov 06 '19 at 06:22
  • My app was crashing after add this, reason is that - input type was number Signed and when it adds - or space then it crashes. Hope it helps someone. REMOVE INPUT TYPE! – Vijay Jan 09 '21 at 17:59
31

I modified Chris Jenkins answer to make it more robust. With this, even if the user edits the middle of the text, the spacing characters are still inserted (and automatically removed on wrong places) correctly.

To make this work correctly, make sure the EditText attributes are set as follows (note the space on digits):

android:digits="01234 56789"
android:inputType="number"
android:maxLength="19"

Then here is the TextWatcher you need. The anonymous class can also be made static since this is independent of the EditText.

    yourTextView.addTextChangedListener(new TextWatcher() {
        private static final char space = ' ';

        @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) {
            // Remove all spacing char
            int pos = 0;
            while (true) {
                if (pos >= s.length()) break;
                if (space == s.charAt(pos) && (((pos + 1) % 5) != 0 || pos + 1 == s.length())) {
                    s.delete(pos, pos + 1);
                } else {
                    pos++;
                }
            }

            // Insert char where needed.
            pos = 4;
            while (true) {
                if (pos >= s.length()) break;
                final char c = s.charAt(pos);
                // Only if its a digit where there should be a space we insert a space
                if ("0123456789".indexOf(c) >= 0) {
                    s.insert(pos, "" + space);
                }
                pos += 5;
            }
        }
    });
Randy Sugianto 'Yuku'
  • 71,383
  • 57
  • 178
  • 228
23

Here is a cleaner solution using regular expressions. Although regular expressions can be inefficient, they would be sufficient in this case since it's processing a string of at most 19 characters, even if the processing occurs after each key press.

editTxtCardNumber.addTextChangedListener(new TextWatcher() {

    @Override
    public void onTextChanged(CharSequence s, int arg1, int arg2,
            int arg3) { }

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

    @Override
    public void afterTextChanged(Editable s) {
        String initial = s.toString();
        // remove all non-digits characters
        String processed = initial.replaceAll("\\D", "");
        // insert a space after all groups of 4 digits that are followed by another digit
        processed = processed.replaceAll("(\\d{4})(?=\\d)", "$1 ");
        // to avoid stackoverflow errors, check that the processed is different from what's already
        //  there before setting
        if (!initial.equals(processed)) {
            // set the value
            s.replace(0, initial.length(), processed);
        }

    }

});
Brais Gabin
  • 5,827
  • 6
  • 57
  • 92
Umran
  • 231
  • 2
  • 4
  • 1
    your missing a closing bracket right before your final semicolon – jesses.co.tt Aug 18 '16 at 06:39
  • 7
    how do you sleep at night!? Close the bracket! – DaveDev Sep 16 '16 at 06:30
  • Should be marked as best answer. Works perfectly in all use cases with very little code. – Nicolás Arias Aug 30 '17 at 21:42
  • 1
    Add the number 9999, then move the cursor to the start of the EditText, then add the number 123456. The result is "1234 6599 99" instead of "1234 5699 99". Note that 5 and 6 are reversed. – TheIT Dec 18 '17 at 21:09
  • As @TheIT said, still has bug from start position to insert. As my solution is simple and also work well. – Luna Kong Aug 07 '18 at 08:36
  • best of all answers. has no bug in inserting in start or middle. even when paste text it works ok. Thanks – Mahdi Moqadasi Apr 12 '21 at 10:45
  • Thanks for your answer. There is still one issue which is that we still have to remove the space but it should remove the pre-space number directly. – Zeeshan Jan 22 '22 at 16:27
16

Here's the class that I use for credit card numbers. Usage examples below.

enter image description here

FormattedNumberEditText.kt

import android.content.Context
import android.text.Editable
import android.text.InputType
import android.text.TextWatcher
import android.text.method.DigitsKeyListener
import android.util.AttributeSet
import android.widget.EditText

open class FormattedNumberEditText : AppCompatEditText {

    var prefix = ""
        private set

    var groupSeparator = ' '
        private set

    var numberOfGroups = 4
        private set

    var groupLength = 4
        private set

    var inputLength = numberOfGroups * (groupLength + 1) - 1
        private set

    private val digitsKeyListener = DigitsKeyListener.getInstance("0123456789")

    private lateinit var separatorAndDigitsKeyListener: DigitsKeyListener

    private var initCompleted = false

    constructor(context: Context) : super(context) {
        init(null)
    }

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
        init(attrs)
    }

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
        init(attrs)
    }

    private fun init(attrs: AttributeSet?) {
        if (attrs != null) {
            val a = context.theme.obtainStyledAttributes(attrs, R.styleable.FormattedNumberEditText, 0, 0)
            prefix = a.getString(R.styleable.FormattedNumberEditText_prefix) ?: prefix
            val separatorStr = a.getString(R.styleable.FormattedNumberEditText_groupSeparator)
            if (!separatorStr.isNullOrEmpty()) {
                groupSeparator = separatorStr[0]
            }
            numberOfGroups = a.getInteger(R.styleable.FormattedNumberEditText_numberOfGroups, numberOfGroups)
            groupLength = a.getInteger(R.styleable.FormattedNumberEditText_groupLength, groupLength)
        }

        inputLength = numberOfGroups * (groupLength + 1) - 1
        separatorAndDigitsKeyListener = DigitsKeyListener.getInstance("0123456789$groupSeparator")

        setText(prefix)
        setSelection(text!!.length)
        inputType = InputType.TYPE_CLASS_NUMBER
        keyListener = digitsKeyListener
        addTextChangedListener(TextChangeListener())

        initCompleted = true
    }

    override fun onSelectionChanged(start: Int, end: Int) {
        if (!initCompleted) {
            return
        }

        // make sure input always starts with the prefix
        if (!text!!.startsWith(prefix)) {
            setText(prefix)
            setSelection(text!!.length, text!!.length)
            return
        }

        // make sure cursor is always at the end of the string
        if (start != text!!.length || end != text!!.length) {
            setSelection(text!!.length)
        } else {
            super.onSelectionChanged(start, end)
        }
    }

    private inner class TextChangeListener : TextWatcher {

        var textBefore = ""
        var enteredText = ""
        var deletedChars = 0

        var listenerEnabled = true

        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
            if (!listenerEnabled) return

            textBefore = text.toString()
            enteredText = ""
            deletedChars = 0
        }

        override fun onTextChanged(text: CharSequence?, start: Int, lengthBefore: Int, lengthAfter: Int) {
            if (!listenerEnabled) return

            if (text == null) {
                deletedChars = textBefore.length
                return
            }

            if (text.length < textBefore.length) {
                deletedChars = textBefore.length - text.length
                return
            }

            enteredText = text.toString().substring(textBefore.length, text.length)
        }

        override fun afterTextChanged(s: Editable?) {
            if (!listenerEnabled) return

            if (s == null) {
                return
            }

            listenerEnabled = false

            if (deletedChars > 0) {
                handleTextChange(s)
            } else {
                if (enteredText.length > 1) {
                    s.replace(s.length - enteredText.length, s.length, "")

                    // Append one char at a time
                    enteredText.forEach {
                        s.append("$it")
                        handleTextChange(s)
                    }
                } else {
                    handleTextChange(s)
                }
            }

            listenerEnabled = true
        }

        fun handleTextChange(s: Editable) {
            if (s.length > inputLength) {
                while (s.length > inputLength) {
                    s.delete(s.length - 1, s.length)
                }
            } else if (s.isNotEmpty() && s.length % (groupLength + 1) == 0) {
                if (s.last() == groupSeparator) {
                    s.delete(s.length - 1, s.length)
                } else if (s.last().isDigit() && s.length < inputLength) {
                    keyListener = separatorAndDigitsKeyListener
                    s.insert(s.length - 1, groupSeparator.toString())
                    keyListener = digitsKeyListener
                }
            }
        }

    }

}

attrs.xml (belongs in /res/values)

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="FormattedNumberEditText">
        <attr name="prefix" format="string" />
        <attr name="numberOfGroups" format="integer" />
        <attr name="groupLength" format="integer" />
        <attr name="groupSeparator" format="string" />
    </declare-styleable>
</resources>

Usage examples

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Credit card number" />

    <com.example.myapplication.FormattedNumberEditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="Credit card number (different separator)" />

    <com.example.myapplication.FormattedNumberEditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:groupSeparator="-" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="Phone number starting with +370" />

    <com.example.myapplication.FormattedNumberEditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:groupLength="13"
        app:groupSeparator=" "
        app:numberOfGroups="1"
        app:prefix="+370\u0020" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="IBAN number starting with LT" />

    <com.example.myapplication.FormattedNumberEditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:groupLength="4"
        app:groupSeparator=" "
        app:numberOfGroups="5"
        app:prefix="LT" />

</LinearLayout>
Egis
  • 5,081
  • 5
  • 39
  • 61
11

I'm adding my solution to the list. As far as I am aware, it has no drawback; you can edit in the middle, delete spacing characters, copy and paste into it etc.

To allow editing to take place anywhere in the string, and to maintain cursor position, the Editable is traversed and all whitespace (if any) are taken out one by one. New whitespace is then added at appropriate positions. This will ensure that the cursor moves along with the changes made to the contents.

import java.util.LinkedList;


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


/**
 * Formats the watched EditText to groups of characters, with spaces between them.
 */
public class GroupedInputFormatWatcher implements TextWatcher {

    private static final char SPACE_CHAR = ' ';
    private static final String SPACE_STRING = String.valueOf(SPACE_CHAR);
    private static final int GROUPSIZE = 4;

    /**
     * Breakdown of this regexp:
     * ^             - 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.
     * 
     * Example of matching strings:
     *  - "2304 52"
     *  - "2304"
     *  - ""
     */
    private final String regexp = "^(\\d{4}\\s)*\\d{0,4}(?<!\\s)$";
    private boolean isUpdating = false;

    private final EditText editText;

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

    @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(regexp)) {
            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(SPACE_CHAR); index >= 0; index = originalString.indexOf(SPACE_CHAR, 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, SPACE_STRING);
        }

        // 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) == SPACE_CHAR) {
            editText.setSelection(cursorPos - 1);
        }

        isUpdating = false;
    }
}
MW.
  • 12,550
  • 9
  • 36
  • 65
  • This approach doesn't allow automatically deleting non-leading spaces. Add the text 123456, which gets converted to "1234 56", now move the cursor to before the 5 ie "1234 |56", now attempt to delete the space. I expected the number 4 to be deleted, but instead the cursor is moved. – TheIT Dec 18 '17 at 21:22
  • Having said that, this is the best answer I've found so far – TheIT Dec 18 '17 at 21:46
8

Not sure the TextWatcher is the right thing to use - we should use InputFilter

According to Android documentation, TextWatcher should be used for an external usage example : one [EditView] for password input + one [TextView] view which displays "weak", "strong", etc...

For Credit Card Format I am using InputFilter:

public class CreditCardInputFilter implements InputFilter {
    public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
        if (dest != null & dest.toString().trim().length() > 24) return null;
        if (source.length() == 1 && (dstart == 4 || dstart == 9 || dstart == 14))
            return " " + new String(source.toString());
        return null; // keep original
    }
}

And combine with a length filter (Android SDK) :

mEditCardNumber.setFilters(new InputFilter[]{
     new InputFilter.LengthFilter(24),
     new CreditCardInputFilter(),
});

This handle the case when typing and removing a digit.

(!) But this does not handle the case for a copy/paste of an entire string, this one should be done in a different InputFilter class

Hope it helps !

Dounaka DK
  • 81
  • 1
  • 1
  • 2
    Someone who cares about good practices and efficient working! – Rafa0809 Apr 13 '17 at 08:55
  • I have what you have, but I am struggling with copy/pasting the whole string as you mentioned. Since I had to bump up the length for additional spaces, now user can copy/paste me a string of that length and what do I do then? The entire idea of adding additional characters (delims) to a value seems pretty sad to me, cause when extracting value for validating, you have to remove those delims as well. I wish there was a cleaner way to do this in Android. Anyway, can you point me in the right direction of how to solve the copy/paste issue? Is it via Clipboard manager somehow? – arenaq Jul 15 '17 at 18:54
  • not working for text which includes characters (e.g. IBANs) – woodii Apr 13 '18 at 11:39
  • I improved this input filter for a little, with copy/paste support. https://gist.github.com/Anrimian/168f3ed6b12df7d7da6230f418171dbd – Anrimian Apr 24 '19 at 10:49
  • This worked perfectly for what I was trying to do. Thank you so much! – Marty Miller Mar 17 '21 at 19:32
6

I just did the next implementation and works well for me, even with pasting and typing new text in any position of the EditText.

Gist file

/**
 * Text watcher for giving "#### #### #### ####" format to edit text.
 * Created by epool on 3/14/16.
 */
public class CreditCardFormattingTextWatcher implements TextWatcher {

    private static final String EMPTY_STRING = "";
    private static final String WHITE_SPACE = " ";
    private String lastSource = EMPTY_STRING;

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

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

    @Override
    public void afterTextChanged(Editable s) {
        String source = s.toString();
        if (!lastSource.equals(source)) {
            source = source.replace(WHITE_SPACE, EMPTY_STRING);
            StringBuilder stringBuilder = new StringBuilder();
            for (int i = 0; i < source.length(); i++) {
                if (i > 0 && i % 4 == 0) {
                    stringBuilder.append(WHITE_SPACE);
                }
                stringBuilder.append(source.charAt(i));
            }
            lastSource = stringBuilder.toString();
            s.replace(0, s.length(), lastSource);
        }
    }

}

Usage: editText.addTextChangedListener(new CreditCardFormattingTextWatcher());

epool
  • 6,710
  • 7
  • 38
  • 43
  • 1
    Hi @epool! The code works very well (even removing a char in the middle of entire text). Thanks for it! But when I tried to mask with a different format (for other purpose, like inserting the char '-' at position 14), the cursor, after removing a char, in a String which its size is 16, goes two position back. The maxLength of my editText is 16. Do you know how to solve it? – Filipe Brito Jun 02 '16 at 14:04
  • I am not sure, I need to see your code or what you exactly you want to do, perhaps you can share a gits link of your code. – epool Jun 02 '16 at 15:41
  • I noted now that if the current text of your EditText is 1234 5 (using your approach) and you need to delete the char "3", for example, the EditText's cursor goes two position back. I can create a Gist if you need see my trouble, but you can see this problem happening with your own class. – Filipe Brito Jun 02 '16 at 17:07
  • You are right about the issue, perhaps you can play with `int selectionStart = yourEditText.getSelectionStart(); int selectionEnd = yourEditText.getSelectionEnd(); yourEditText.setSelection(selectionStart, selectionEnd);` to fix it. – epool Jun 02 '16 at 17:30
  • I tried here, but it doesn't work. If you can fix it, I'll (and probably other people) be grateful to you. Thanks anyway @epool! – Filipe Brito Jun 02 '16 at 17:51
5

This implementation ensures correct placement of spacing chars, even if the user edits mid-string. Other characters that show up on the soft keyboard (such as dash) are also supported; that is, the user can't enter them. One improvement that could be made: this implementation doesn't allow for the deletion of spacing characters mid-string.

public class CreditCardTextWatcher implements TextWatcher {

    public static final char SPACING_CHAR = '-'; // Using a Unicode character seems to stuff the logic up.

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

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

    @Override
    public void afterTextChanged(final Editable s) {
        if (s.length() > 0) {

            // Any changes we make to s in here will cause this method to be run again.  Thus we only make changes where they need to be made,
            // otherwise we'll be in an infinite loop.

            // Delete any spacing characters that are out of place.
            for (int i=s.length()-1; i>=0; --i) {
                if (s.charAt(i) == SPACING_CHAR  // There is a spacing char at this position ,
                        && (i+1 == s.length()    // And it's either the last digit in the string (bad),
                        || (i+1) % 5 != 0)) {    // Or the position is not meant to contain a spacing char?

                    s.delete(i,i+1);
                }
            }

            // Insert any spacing characters that are missing.
            for (int i=14; i>=4; i-=5) {
                if (i < s.length() && s.charAt(i) != SPACING_CHAR) {
                    s.insert(i, String.valueOf(SPACING_CHAR));
                }
            }
        }
    }
}

Works well with an appropriate PasswordTransformationMethod implementation to mask CC digits.

Tom
  • 6,946
  • 2
  • 47
  • 63
5

If you are using Kotlin, this can be helpful:

class CreditCardTextFormatter(
    private var separator: String = " - ",
    private var divider: Int = 5
) : TextWatcher {

    override fun afterTextChanged(s: Editable?) {
        if (s == null) {
            return
        }
        val oldString = s.toString()
        val newString = getNewString(oldString)
        if (newString != oldString) {
            s.replace(0, oldString.length, getNewString(oldString))
        }
    }

    private fun getNewString(value: String): String {

        var newString = value.replace(separator, "")

        var divider = this.divider
        while (newString.length >= divider) {
            newString = newString.substring(0, divider - 1) + this.separator + newString.substring(divider - 1)
            divider += this.divider + separator.length - 1
        }
        return newString
    }

    override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
    }

    override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
    }
}

The XML:

<EditText
        android:id="@+id/etCardNumber"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:digits="0123456789- "
        android:inputType="number"
        android:hint="____ - ____ - ____ - ____"
        android:maxLength="25" />

And how to use it:

etCardNumber.addTextChangedListener(CreditCardTextFormatter())
Stefano
  • 61
  • 1
  • 5
4

After searching a lot and not getting any satisfactory answer to meet my needs, I ended up writing my own function.

Here is an example to format entered credit card details based on the type of card being entered. Currently it takes care of Visa, MasterCard and American Express for the purpose of formatting.

    editTxtCardNumber.addTextChangedListener(new TextWatcher() {

        private boolean spaceDeleted;

        @Override
        public void onTextChanged(CharSequence s, int arg1, int arg2,
                int arg3) {

        }

        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            CharSequence charDeleted = s.subSequence(start, start + count);
            spaceDeleted = " ".equals(charDeleted.toString());
        }

        @Override
        public void afterTextChanged(Editable editable) {

            if(editTxtCardNumber.getText().length() > 0 && editTxtCardNumber.getText().charAt(0) == '3') {
                editTxtCardNumber.setFilters(new InputFilter[] { new InputFilter.LengthFilter(Constants.MAX_LENGTH_CARD_NUMBER_AMEX) });

                editTxtCardNumber.removeTextChangedListener(this);
                int cursorPosition = editTxtCardNumber.getSelectionStart();
                String withSpaces = formatTextAmEx(editable);
                editTxtCardNumber.setText(withSpaces);
                editTxtCardNumber.setSelection(cursorPosition + (withSpaces.length() - editable.length()));

                if (spaceDeleted) {
                    editTxtCardNumber.setSelection(editTxtCardNumber.getSelectionStart() - 1);
                    spaceDeleted = false;
                }

                editTxtCardNumber.addTextChangedListener(this);
            } else if(editTxtCardNumber.getText().length() > 0 
                    && (editTxtCardNumber.getText().charAt(0) == '4' || editTxtCardNumber.getText().charAt(0) == '5')) {
                editTxtCardNumber.setFilters(new InputFilter[] { new InputFilter.LengthFilter(Constants.MAX_LENGTH_CARD_NUMBER_VISA_MASTERCARD) });

                editTxtCardNumber.removeTextChangedListener(this);
                int cursorPosition = editTxtCardNumber.getSelectionStart();
                String withSpaces = formatTextVisaMasterCard(editable);
                editTxtCardNumber.setText(withSpaces);
                editTxtCardNumber.setSelection(cursorPosition + (withSpaces.length() - editable.length()));

                if (spaceDeleted) {
                    editTxtCardNumber.setSelection(editTxtCardNumber.getSelectionStart() - 1);
                    spaceDeleted = false;
                }

                editTxtCardNumber.addTextChangedListener(this);
            } else {
                editTxtCardNumber.setFilters(new InputFilter[] { new InputFilter.LengthFilter(Constants.MAX_LENGTH_CARD_NUMBER_VISA_MASTERCARD) });

                editTxtCardNumber.removeTextChangedListener(this);
                int cursorPosition = editTxtCardNumber.getSelectionStart();
                String withSpaces = formatTextVisaMasterCard(editable);
                editTxtCardNumber.setText(withSpaces);
                editTxtCardNumber.setSelection(cursorPosition + (withSpaces.length() - editable.length()));

                if (spaceDeleted) {
                    editTxtCardNumber.setSelection(editTxtCardNumber.getSelectionStart() - 1);
                    spaceDeleted = false;
                }

                editTxtCardNumber.addTextChangedListener(this);
            }
        }
    });

    private String formatTextVisaMasterCard(CharSequence text)
    {
        StringBuilder formatted = new StringBuilder();
        int count = 0;
        for (int i = 0; i < text.length(); ++i)
        {
            if (Character.isDigit(text.charAt(i)))
            {
                if (count % 4 == 0 && count > 0)
                    formatted.append(" ");
                formatted.append(text.charAt(i));
                ++count;
            }
        }
        return formatted.toString();
    }

    private String formatTextAmEx(CharSequence text)
    {
        StringBuilder formatted = new StringBuilder();
        int count = 0;
        for (int i = 0; i < text.length(); ++i)
        {
            if (Character.isDigit(text.charAt(i)))
            {
                if (count > 0 && ((count == 4) || (count == 10))) {
                    formatted.append(" ");
                }
                formatted.append(text.charAt(i));
                ++count;
            }
        }
        return formatted.toString();
    }

Other than formatting spaces, I also applied checks to make sure that card number doesn't exceed their maximum limit and user gets notified that he has entered all the digits by performing a change in font when the maximum limit is reached. Here is the function to perform the above mentioned operation.

public void checkCardNoEnteredCorrectly() {
if(editTxtCardNumber.getText().length() > 0 && editTxtCardNumber.getText().charAt(0) == '3') {
    if(editTxtCardNumber.getText().length() == Constants.MAX_LENGTH_CARD_NUMBER_AMEX) {
        editTxtCardNumber.setCompoundDrawablesWithIntrinsicBounds(getResources().getDrawable(R.drawable.amex), null, null, null);
    } else {
        editTxtCardNumber.setCompoundDrawablesWithIntrinsicBounds(getResources().getDrawable(R.drawable.amex), null, null, null);
    }
} else if(editTxtCardNumber.getText().length() > 0 && editTxtCardNumber.getText().charAt(0) == '4') {
    if(editTxtCardNumber.getText().length() == Constants.MAX_LENGTH_CARD_NUMBER_VISA_MASTERCARD) {
        editTxtCardNumber.setCompoundDrawablesWithIntrinsicBounds(getResources().getDrawable(R.drawable.visa), null, null, null);
    } else {
        editTxtCardNumber.setCompoundDrawablesWithIntrinsicBounds(getResources().getDrawable(R.drawable.visa), null, null, null);
    }
} else if(editTxtCardNumber.getText().length() > 0 && editTxtCardNumber.getText().charAt(0) == '5') {
    if(editTxtCardNumber.getText().length() == Constants.MAX_LENGTH_CARD_NUMBER_VISA_MASTERCARD) {
        editTxtCardNumber.setCompoundDrawablesWithIntrinsicBounds(getResources().getDrawable(R.drawable.master_card), null, null, null);
    } else {
        editTxtCardNumber.setCompoundDrawablesWithIntrinsicBounds(getResources().getDrawable(R.drawable.master_card), null, null, null);
    }
} else {
    editTxtCardNumber.setCompoundDrawablesWithIntrinsicBounds(getResources().getDrawable(R.drawable.credit_card_number), null, null, null);
}

}

Note: The declarations made in Constants.java is as follows:

public static final int MAX_LENGTH_CARD_NUMBER_VISA_MASTERCARD = 19;
public static final int MAX_LENGTH_CARD_NUMBER_AMEX = 17;

Hope it helps!

Gaurav Saluja
  • 357
  • 3
  • 11
4

I think that my solution can work well whatever middle text operation or copy-paste operation.

Please see code as below,

  class BankNumberTextWatcher implements TextWatcher {
      private int previousCodeLen = 0;

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

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

      @Override
      public void afterTextChanged(Editable s) {
          if (s.length() > 0) {
              String numbersOnly = s.toString().replaceAll("[^0-9]", "");
              // current code pattern miss-match, then handle cursor position and format the code
              handleEditInput(numbersOnly);
          } else {
              previousCodeLen = 0;
          }
      }

      /**
       * Handle EditText input process for credit card including insert, delete during middle position,
       * end position or copy-paste controller
       *
       * @param numbersOnly the pure number without non-digital characters
       */
      private void handleEditInput(final String numbersOnly) {
          String code = formatNumbersAsCode(numbersOnly);
          int cursorStart = etBankCardNumber.getSelectionStart();
          etBankCardNumber.removeTextChangedListener(this);
          etBankCardNumber.setText(code);
          int codeLen = code.length();
          if (cursorStart != codeLen) {
             // middle-string operation
             if (cursorStart > 0 && cursorStart % 5 == 0) {
                if (codeLen > previousCodeLen) {
                    // insert, move cursor to next
                    cursorStart++;
                } else if (codeLen < previousCodeLen) {
                    // delete, move cursor to previous
                    cursorStart--;
                }
             }
             etBankCardNumber.setSelection(cursorStart);
          } else {
             // end-string operation
             etBankCardNumber.setSelection(codeLen);
          }
          etBankCardNumber.addTextChangedListener(this);
          previousCodeLen = codeLen;
      }

      /**
       * formats credit code like 1234 1234 5123 1234
       *
       * @param s
       * @return
       */
       public String formatNumbersAsCode(CharSequence s) {
          if (TextUtils.isEmpty(s)) {
            return "";
          }
          int len = s.length();
          StringBuilder tmp = new StringBuilder();
          for (int i = 0; i < len; ++i) {
              tmp.append(s.charAt(i));
              if ((i + 1) % 4 == 0 && (i + 1) != len) {
                  tmp.append(" ");
              }
          }
          return tmp.toString();
        }
  }

Makes inputType to number for EditText to avoid other characters in the layout file.

Hope that be helpful for you.

Luna Kong
  • 3,065
  • 25
  • 20
3

Please look at this project . Android form edit text is an extension of EditText that brings data validation facilities to the edittext

Chirag
  • 56,621
  • 29
  • 151
  • 198
  • wish i had known earlier but I am want to find out why my existing code is not able to delete the spaces . – Preethi Aug 03 '12 at 14:28
  • 1
    The project looks interesting but it seems to be fundamentally a validator and not an input formatter, which is what the OP (and I) need. I can't see from any of the screenshots that it also does input formatting. Does it? – Anne Gunn Oct 01 '14 at 15:42
1

You may have figured it out already, but here is what I did. The only method I had to override was AfterTextChanged.

Check if the form of the credit card is already valid, base case to prevent infinite recursion

If the form is not valid, remove all whitespace, and copy over into another string, inserting white space where appropriate.

Then simply replace the editable with your new string.

If you need code for a particular step, feel free to ask.

And Preethi, the reason you can't delete spaces is because you can't change text in the onTextChanged callback. From the developer site:

public abstract void onTextChanged (CharSequence s, int start, int before, int count) Added in API level 1

This method is called to notify you that, within s, the count characters beginning at start have just replaced old text that had length before. It is an error to attempt to make changes to s from this callback.

Brent Hronik
  • 2,357
  • 1
  • 27
  • 43
1
int          keyDel;
String       a;
String       a0;
int          isAppent = 0;
final String ch       = " ";

private void initListner() {


    txtCreditNumber.addTextChangedListener(new TextWatcher() {

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

            boolean flag = true;
            if (s.length() > 19) {
                txtCreditNumber.setText(a0);
                txtCreditNumber.setSelection(txtCreditNumber.getText().length());
                return;
            }
            String eachBlock[] = s.toString().split(ch);
            for(int i = 0; i < eachBlock.length; i++) {
                if (eachBlock[i].length() > 4) {
                    flag = false;
                }
            }
            if (a0.length() > s.toString().length()) {
                keyDel = 1;
            }
            if (flag) {
                if (keyDel == 0) {

                    if (((txtCreditNumber.getText().length() + 1) % 5) == 0) {

                        if (s.toString().split(ch).length <= 3) {
                            isAppent = 1;
                            txtCreditNumber.setText(s + ch);
                            isAppent = 0;
                            txtCreditNumber.setSelection(txtCreditNumber.getText().length());
                            a = txtCreditNumber.getText().toString();
                            return;
                        }
                    }
                    if (isAppent == 0) {
                        String str = s.toString();
                        if (str.lastIndexOf(ch) == str.length() - 1) {
                            str = str.substring(0, str.lastIndexOf(ch));
                            keyDel = 1;
                            txtCreditNumber.setText(str);
                            keyDel = 0;
                            txtCreditNumber.setSelection(txtCreditNumber.getText().length());
                            a = txtCreditNumber.getText().toString();
                            return;
                        }
                    }

                }
                else {
                    String str = s.toString();
                    if (str.length() > 0 && str.lastIndexOf(ch) == str.length() - 1) {
                        str = str.substring(0, str.lastIndexOf(ch));
                        keyDel = 1;
                        txtCreditNumber.setText(str);
                        keyDel = 0;
                        txtCreditNumber.setSelection(txtCreditNumber.getText().length());
                        a = txtCreditNumber.getText().toString();
                        return;
                    }
                    else {
                        a = txtCreditNumber.getText().toString();
                        keyDel = 0;
                    }
                }

            }
            else {
                String str = s.toString();
                str = str.substring(0, str.length() - 1) + ch + str.substring(str.length() - 1, str.length());

                a = str;
                txtCreditNumber.setText(a);
                txtCreditNumber.setSelection(txtCreditNumber.getText().length());
            }

        }

        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            // TODO Auto-generated method stub
            a0 = s.toString();
        }

        @Override
        public void afterTextChanged(Editable s) {
        }
    });
}
Chandanit
  • 57
  • 4
1

Here is an example that use all the function appropriately to make a decision. The code might be a bit longer, but it will be faster as it mainly use the function given values (start, before, count ...). This example add "-" every 4 digits, and delete them as well, when user use backspace. as well, make sure the cursor will be at the end.

public class TextWatcherImplement implements TextWatcher {

private EditText creditCard;
private String beforeText, currentText;
private boolean noAction, addStroke, dontAddChar, deleteStroke;

public TextWatcherImplement(EditText creditCard) {
    // TODO Auto-generated constructor stub
    this.creditCard = creditCard;
    noAction = false;
    addStroke = false;
    dontAddChar = false;
    deleteStroke = false;
}

/* here I save the previous string if the max character had achieved */
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
    // TODO Auto-generated method stub
    Log.i("TextWatcherImplement", "beforeTextChanged start==" + String.valueOf(start) + " count==" + String.valueOf(count) + " after==" + String.valueOf(after));
    if (start >= 19)
        beforeText = s.toString();
}


/* here I check were we add a character, or delete one. 
if we add character and it is time to add a stroke, then I flag it -> addStroke 
if we delete a character and it time to delete a stroke, I flag it -> deleteStroke
if we are in max character for the credit card, don't add char -> dontAddChar 
*/
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
    // TODO Auto-generated method stub
    Log.i("TextWatcherImplement", "onTextChanged start==" + String.valueOf(start) + " before==" + String.valueOf(before) + " count==" + String.valueOf(count) + " noAction ==" + String.valueOf(noAction));
    if ( (before < count) && !noAction ) {
        if ( (start == 3) || (start == 8) || (start == 13) ) {
            currentText = s.toString();
            addStroke = true;
        } else if (start >= 19) {
            currentText = s.toString();
            dontAddChar = true;
        }
    } else {
        if ( (start == 4) ||  (start == 9) ||  (start == 14)  ) { //(start == 5) || (start == 10) || (start == 15)
            currentText = s.toString();
            deleteStroke = true;
        }
    }
}

/* noAction flag is when we change the text, the interface is being called again.
the NoAction flag will prevent any action, and prevent a ongoing loop */

@Override
public void afterTextChanged(Editable stext) {
    // TODO Auto-generated method stub
    if (addStroke) {
        Log.i("TextWatcherImplement", "afterTextChanged String == " + stext + " beforeText == " + beforeText + " currentText == " + currentText);
        noAction = true;
        addStroke = false;
        creditCard.setText(currentText + "-");
    } else if (dontAddChar) {
        dontAddChar = false;
        noAction = true;
        creditCard.setText(beforeText);
    } else if (deleteStroke) {
        deleteStroke = false;
        noAction = true;
        currentText = currentText.substring(0, currentText.length() - 1);
        creditCard.setText(currentText);
    } else {
        noAction = false;
        creditCard.setSelection(creditCard.getText().length()); // set cursor at the end of the line.
    }
}

}

Gilco
  • 1,326
  • 12
  • 13
1

Here's my solution. My comments should suffice enough information for an Android developer to understand what's happening but if you have any questions then please feel free to ask and I'll answer to the best of my knowledge.

private KeyEvent keyEvent;

final TextWatcher cardNumberWatcher = new TextWatcher() {
        @Override
        public void beforeTextChanged(CharSequence charSequence, int start, int before, int count) {
            // NOT USING
        }

        @Override
        public void onTextChanged(CharSequence charSequence, int start, int before, int count) {
            // NOT USING
        }

        @Override
        public void afterTextChanged(Editable editable) {
            String cardNumbersOnly = editable.toString().replace("-", "");

            /**
            * @PARAM keyEvent
            * This gets called upon deleting a character so you must keep a 
            * flag to ensures this gets skipped during character deletion
            */
            if (cardNumbersOnly.length() >= 4 && keyEvent == null) {
                formatCreditCardTextAndImage(this);
            }

            keyEvent = null;
        }
    };

    cardNumberEditText.addTextChangedListener(cardNumberWatcher);

    /**
    * @LISTENER
    * Must keep track of when the backspace event has been fired to ensure    
    * that the delimiter character and the character before it is deleted 
    * consecutively to avoid the user from having to press backspace twice 
    */
    cardNumberEditText.setOnKeyListener(new View.OnKeyListener() {
        @Override
        public boolean onKey(View v, int keyCode, KeyEvent event) {
            if (event.getAction() != KeyEvent.ACTION_UP) {
                // Hold reference of key event for checking within the text watcher
                keyEvent = event;
                String cardNumberString = cardNumberEditText.getText().toString();

                if (keyCode == event.KEYCODE_DEL) {
                    if (cardNumberString.substring(cardNumberString.length() - 1).equals("-")) {
                        // Remove listener to avoid infinite looping
                        cardNumberEditText.removeTextChangedListener(cardNumberWatcher);
                        // Remove hyphen and character before it
                        cardNumberEditText.setText(cardNumberString.substring(0, cardNumberString.length() - 1));
                        // Set the cursor back to the end of the text
                        cardNumberEditText.setSelection(cardNumberEditText.getText().length());
                        // Add the listener back
                        cardNumberEditText.addTextChangedListener(cardNumberWatcher);
                    }
                    else if (cardNumberString.length() < 2) {
                        cardNumberBrandImageView.setImageDrawable(null);
                        cardNumberBrandImageView.setVisibility(View.INVISIBLE);
                    }
                }
            }
            return false;
        }
    });
}

private void formatCreditCardTextAndImage (TextWatcher textWatcher) {
    // Remove to avoid infinite looping
    cardNumberEditText.removeTextChangedListener(textWatcher);

    String cardNumberString = cardNumberEditText.getText().toString();

    /**
    * @CONDITION
    * Append delimiter after every fourth character excluding the 16th
    */
    if ((cardNumberString.length() + 1) % 5 == 0 && !cardNumberString.substring(cardNumberString.length() - 1).equals("-")) {
            cardNumberEditText.setText(cardNumberString + "-");
    }

    // Set the cursor back to the end of the text
    cardNumberEditText.setSelection(cardNumberEditText.getText().length());
    cardNumberEditText.addTextChangedListener(textWatcher);

    /**
    * @CardBrand
    * Is an enum utility class that checks the card numbers 
    * against regular expressions to determine the brand and updates the UI
    */
    if (cardNumberString.length() == 2) {
        switch (CardBrand.detect(cardNumberEditText.getText().toString())) {
            case VISA:
                cardNumberBrandImageView.setImageResource(R.drawable.visa);
                cardNumberBrandImageView.setVisibility(View.VISIBLE);
                card.setBrand(Brand.Visa);
                break;
            case MASTERCARD:
                cardNumberBrandImageView.setImageResource(R.drawable.mastercard);
                cardNumberBrandImageView.setVisibility(View.VISIBLE);
                card.setBrand(Brand.MasterCard);
                break;
            case DISCOVER:
                cardNumberBrandImageView.setImageResource(R.drawable.discover);
                cardNumberBrandImageView.setVisibility(View.VISIBLE);
                card.setBrand(Brand.Discover);
                break;
            case AMERICAN_EXPRESS:
                cardNumberBrandImageView.setImageResource(R.drawable.americanexpress);
                cardNumberBrandImageView.setVisibility(View.VISIBLE);
                card.setBrand(Brand.AmericanExpress);
                break;
            case UNKNOWN:
                cardNumberBrandImageView.setImageDrawable(null);
                cardNumberBrandImageView.setVisibility(View.INVISIBLE);
                card.setBrand(null);
                break;
        }
    }
}
Josh Valdivieso
  • 1,168
  • 1
  • 14
  • 22
1

Here's a simple and easily customizable solution using the TextWatcher class. It may be assigned to your EditText using the addTextChangedListener() method.

new TextWatcher() {
    /** Formats the Field to display user-friendly separation of the input values. */
    @Override public final void afterTextChanged(final Editable pEditable) {
        // Declare the separator.
        final char lSeparator      = '-';
        // Declare the length of separated text. i.e. (XXXX-XXXX-XXXX)
        final int  lSeparationSize = 4;
        // Declare the count; tracks the number of allowed characters in a row.
              int lCount          = 0;
        // Iterate the Characters.
        for(int i = 0; i < pEditable.length(); i++) {
            // Fetch the current character.
            final char c              = pEditable.charAt(i);
            // Is it a usual character. Here, we permit alphanumerics only.
            final boolean lIsExpected = (Character.isDigit(c) || Character.isLetter(c)) && (c != lSeparator);
            // Is the character expected?
            if(lIsExpected) {
                // Increase the count.
                lCount++;
            }
            else {
                // Is it a separator?
                if(c == lSeparator) {
                    // Reset the count.
                    lCount = 0;
                    // Continue the iteration.
                    continue;
                }
            }
            // Has the count been exceeded? Is there more text coming?
            if(lCount >= (lSeparationSize + 1) && (i < pEditable.length())) {
                // Reset the count.
                lCount = 0;
                // Insert the separator.
                pEditable.insert(i, Character.toString(lSeparator));
                // Increase the iteration count.
                i++;
            }
        }
    }
    /** Unused overrides. */
    @Override public final void beforeTextChanged(final CharSequence pCharSequence, final int pStart, final int pCount, final int pAfter) { }
    @Override public final void onTextChanged(final CharSequence pCharSequence, final int pStart, final int pBefore, final int pCount) { }
}

Alternatively, here is a much cleaner implementation based on epool's implementation.

public final class TextGroupFormattingListener implements TextWatcher {

    /* Member Variables. */
    private final int    mGroupLength;
    private final String mSeparator;
    private       String mSource;

    /** Constructor. */
    public TextGroupFormattingListener(final String pSeparator, final int pGroupLength) {
        // Initialize Member Variables.
        this.mSeparator   = pSeparator;
        this.mGroupLength = pGroupLength;
        this.mSource      = "";
    }

    /** Formats the Field to display user-friendly separation of the input values. */
    @Override public final void afterTextChanged(final Editable pEditable) {
        // Fetch the Source.
        String lSource = pEditable.toString();
        // Has the text changed?
        if (!this.getSource().equals(lSource)) {
            // Remove all of the existing Separators.
            lSource = lSource.replace(this.getSeparator(), "");
            // Allocate a StringBuilder.
            StringBuilder lStringBuilder = new StringBuilder();
            // Iterate across the Source String, which contains the raw user input.
            for(int i = 0; i < lSource.length(); i++) {
                // Have we exceeded the GroupLength?
                if(i > 0 && i % this.getGroupLength() == 0) {
                    // Append the separator.
                    lStringBuilder.append(this.getSeparator());
                }
                // Append the user's character data.
                lStringBuilder.append(lSource.charAt(i));
            }
            // Track changes to the Source.
            this.setSource(lStringBuilder.toString());
            // Replace the contents of the Editable with this new String.
            pEditable.replace(0, pEditable.length(), this.getSource());
        }
    }

    /** Unused overrides. */
    @Override public final void beforeTextChanged(final CharSequence pCharSequence, final int pStart, final int pCount, final int pAfter) { }
    @Override public final void onTextChanged(final CharSequence pCharSequence, final int pStart, final int pBefore, final int pCount)    { }

    public final int getGroupLength() {
        return this.mGroupLength;
    }

    public final String getSeparator() {
        return this.mSeparator;
    }

    private final void setSource(final String pSource) {
        this.mSource = pSource;
    }

    private final String getSource() {
        return this.mSource;
    }

}
Community
  • 1
  • 1
Mapsy
  • 4,192
  • 1
  • 37
  • 43
1

None of above answers is perfect for me. I created one that solves the start-string/end-string/mid-string issues. Copy & Paste should also work fine. This supports Mastercard, Visa and Amex. You can change the separator. If you don't need payment method type just remove it. It is Kotlin though. The idea is simple. Everytime when text changed I remove all separators and re-added them base on the format. The solves the issue start-string/mid-string issues. Then the only problem is that you need to work out the the right text position after separators added.

fun addCreditCardNumberTxtWatcher(et: EditText, separator: Char, paymentMethodType: PaymentMethodType): TextWatcher {
    val tw = object : TextWatcher {
        var mBlock = false
        override fun afterTextChanged(s: Editable) {
        }
        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
              Logger.d("_debug", "s: $s, start: $start, count: $count, after $after")
        }
        override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
            if (mBlock)
                return
            var lastPos = et.selectionStart
            val oldStr = et.text.toString().replace(separator.toString(), "", false)
            var newFormattedStr = ""
            if (before > 0) {
                if (lastPos > 0 && et.text.toString()[lastPos - 1] == separator) lastPos--
            }
            Logger.d("_debug", "lastPos: $lastPos, s: $s, start: $start, before: $before, count $count")
            mBlock = true
            oldStr.forEachIndexed { i, c ->
                when (paymentMethodType) {
                    PaymentMethodType.MASTERCARD, PaymentMethodType.VISA -> {
                        if (i > 0 && i % 4 == 0) {
                            newFormattedStr += separator
                        }
                    }
                    PaymentMethodType.AMERICAN_EXPRESS -> {
                        if (i == 4 || i == 10 || i == 15) {
                            newFormattedStr += separator
                        }
                    }
                }
                newFormattedStr += c
            }
            et.setText(newFormattedStr)
            if (before == 0) {
                if (et.text.toString()[lastPos - 1] == separator) lastPos++
            }
            et.setSelection(lastPos)
            mBlock = false
        }
    }
    et.addTextChangedListener(tw)
    return tw
}
Arst
  • 3,098
  • 1
  • 35
  • 42
  • Works flawlessly! Though, I would suggest that rather than having payment method types, which may change (Amex may come out with a different format). I would suggest using a list of indices at which a separator must exist. For instance, for cards that are typically 16 in length, the separators will be a `listOf(4, 9, 13)`. For cards that are typically 15 in length, the separators will be a `listOf(4, 10)` – w3bshark Jan 25 '19 at 22:03
1

This solution was implemented for IBAN's but the principle is the same, I tried to correct all the main problems in the answers above, if you find an error feel free to say it, thank you.

Set the EditText and restrict the characters that can be used:

private void setEditTextIBAN(View view) {
    editTextIBAN = (EditText) view.findViewById(R.id.client_iban);
    editTextIBAN.setKeyListener(
            DigitsKeyListener.getInstance("ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 "));
    editTextIBAN.addTextChangedListener(new IBANTextWatcher());
}

This is the TextWatcher:

private class IBANTextWatcher implements TextWatcher {

    // means divider position is every 5th symbol
    private static final int DIVIDER_MODULO = 5;
    private static final int GROUP_SIZE = DIVIDER_MODULO - 1;
    private static final char DIVIDER = ' ';
    private static final String STRING_DIVIDER = " ";
    private String previousText = "";

    private int deleteLength;
    private int insertLength;
    private int start;

    private String regexIBAN = "(\\w{" + GROUP_SIZE + "}" + DIVIDER +
            ")*\\w{1," + GROUP_SIZE + "}";
    private Pattern patternIBAN = Pattern.compile(regexIBAN);

    @Override
    public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) {
        this.previousText = s.toString();
        this.deleteLength = count;
        this.insertLength = after;
        this.start = start;
    }

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

    }

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

        if (!previousText.equals(originalString) &&
                !isInputCorrect(originalString)) {
            String newString = previousText.substring(0, start);
            int cursor = start;

            if (deleteLength > 0 && s.length() > 0 &&
                    (previousText.charAt(start) == DIVIDER ||
                            start == s.length())) {
                newString = previousText.substring(0, start - 1);
                --cursor;
            }

            if (insertLength > 0) {
                newString += originalString.substring(start, start + insertLength);
                newString = buildCorrectInput(newString);
                cursor = newString.length();
            }

            newString += previousText.substring(start + deleteLength);
            s.replace(0, s.length(), buildCorrectInput(newString));

            editTextIBAN.setSelection(cursor);
        }
    }

    /**
     * Check if String has the white spaces in the correct positions, meaning
     * if we have the String "123456789" and there should exist a white space
     * every 4 characters then the correct String should be "1234 5678 9".
     *
     * @param s String to be evaluated
     * @return true if string s is written correctly
     */
    private boolean isInputCorrect(String s) {
        Matcher matcherDot = patternIBAN.matcher(s);
        return matcherDot.matches();
    }

    /**
     * Puts the white spaces in the correct positions,
     * see the example in {@link IBANTextWatcher#isInputCorrect(String)}
     * to understand the correct positions.
     *
     * @param s String to be corrected.
     * @return String corrected.
     */
    private String buildCorrectInput(String s) {
        StringBuilder sbs = new StringBuilder(
                s.replaceAll(STRING_DIVIDER, ""));

        // Insert the divider in the correct positions
        for (int i = GROUP_SIZE; i < sbs.length(); i += DIVIDER_MODULO) {
            sbs.insert(i, DIVIDER);
        }

        return sbs.toString();
    }
}
1

If anyone still looking for answer,

Try the format-edit-text library for auto-formatting text in one line of code. This library uses dash(es) to define the format of the input.

editText.setFormat("any (dash) format");

How to use

add format-edit-text library dependency in app/build.gradle

implementation 'com.androidwidgets:formatedittext:0.2.0'

Add FormatEditText view in 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"
    android:focusableInTouchMode="true"
    android:focusable="true">

    <com.androidwidgets.formatedittext.widgets.FormatEditText
        android:id="@+id/edit_text_1"
        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:imeOptions="actionSend"
        android:inputType="number"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Set credit-card format to FormatEditText view in MainActivity.java

public class MainActivity extends AppCompatActivity {

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

        final FormatEditText editText1 = findViewById(R.id.edit_text_1);
        editText1.setFormat("---- ---- ---- ----");
    }
}

This will produce the below output

PS: Make sure parameter inputType is added to the FormatEditText view in the layout file.

android:inputType="number"
1

I know this question is a bit old but I need an implemantation of this for IBAN's and not satisfied with the given answers. So I wrote some code for this. But it takes the "pattern" and "divider" as parameters so it can be use for credit card numbers too.

This is the extended text watcher class.

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

public class IbanTextWatcher implements TextWatcher {

    private int[] pattern;
    private String divider;
    private String before;
    private EditText field;
    private boolean dividerDeleted;
    

    public IbanTextWatcher(int[] pattern, String divider, EditText field) {
        this.divider = divider;
        this.pattern = pattern;
        this.field = field;
    }


    @Override
    public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
        before = charSequence.toString();

        if (!String.valueOf(charSequence).equals("") && charSequence.length() > i) {
            if (String.valueOf(before.charAt(i)).equals(getDivider())) {
                dividerDeleted = true;
            } else {
                dividerDeleted = false;
            }
        }
    }

    @Override
    public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {

    }

    @Override
    public void afterTextChanged(Editable editable) {

        String input = editable.toString().replaceAll("\\s", "");
        StringBuilder output = new StringBuilder();
        boolean error = false;
        int currentIndex = 0;
        int cursorPosition = getField().getSelectionStart();
        int lengthBefore;
        int currentPatternMember = 0;
        
        //prevent user to delete the divider
        if (dividerDeleted && cursorPosition != getField().getText().length()) {
            getField().setText(getBefore());
            getField().setSelection(cursorPosition + 1);
            return;
        } else if (input.equals(getBefore().replaceAll("\\s", ""))) {
            return;
        }

        for (int i = 0; i < getPattern().length; i++) {
            error = false;
            currentPatternMember = getPattern()[i];
            try {
                output.append(input.substring(currentIndex, currentIndex + currentPatternMember));
            } catch (StringIndexOutOfBoundsException e) {
                error = true;
            }

            if (!error) {
                if (i != getPattern().length - 1) {
                    output.append(getDivider());
                }
                currentIndex += currentPatternMember;
            } else {
                break;
            }
        }

        if (error) {
            output.append(input.substring(currentIndex, input.length()));
        } 

        cursorPosition = getField().getSelectionStart();
        lengthBefore = getBefore().length();
        getField().setText(output.toString());

        if (cursorPosition != lengthBefore && cursorPosition != lengthBefore + 1) {
            getField().setSelection(cursorPosition);
        } else {
            getField().setSelection(getField().getText().length());
        }
    }

    public int[] getPattern() {
        return pattern;
    }

    public String getDivider() {
        return divider;
    }

    public String getBefore() {
        return before;
    }

    public EditText getField() {
        return field;
    }
}

And this is how I use it:

int[] pattern = {2,4,4,4,4,4,2}; // 
iban.addTextChangedListener(new IbanTextWatcher(pattern, " ", iban)); //here iban is my edittext field

By the way, I set the max length of the field in xml.

log0
  • 10,489
  • 4
  • 28
  • 62
omer.ersoy
  • 324
  • 1
  • 10
  • Surprised I could not find a well established library for handling ibans in UI... This works ok but I will probably add a few improvements (display letters uppercase, forbid special characters....) – log0 Jan 18 '21 at 14:51
  • Also delete after space removes the space and not the last input. – log0 Jan 18 '21 at 14:52
0

In your layout:

    <android.support.design.widget.TextInputEditText
        android:id="@+id/et_credit_card_number"
        android:digits=" 1234567890"
        android:inputType="number"
        android:maxLength="19"/>

Here the TextWachter which sets a space on every 4 digits in a 16 number credit card.

class CreditCardFormatWatcher : TextWatcherAdapter() {

    override fun afterTextChanged(s: Editable?) {
        if (s == null || s.isEmpty()) return

        s.forEachIndexed { index, c ->
            val spaceIndex = index == 4 || index == 9 || index == 14
            when {
                !spaceIndex && !c.isDigit()     -> s.delete(index, index + 1)
                spaceIndex && !c.isWhitespace() -> s.insert(index, " ")
            }
        }

        if (s.last().isWhitespace())
            s.delete(s.length - 1, s.length)
    }

}
chriskrueger
  • 126
  • 1
  • 3
0
 private class TextWatcherIBAN implements TextWatcher {

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

        }

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

        }

        @Override
        public void afterTextChanged(Editable s) {
            textInputEditText.removeTextChangedListener(this);
            formatIBANEditText(textInputEditText);
            textInputEditText.addTextChangedListener(this);

        }
    }


public void formatIBANEditText(TextInputEditText editText) {
    String decimalAmount = editText.getText().toString();
    int selection = editText.getSelectionEnd() == decimalAmount.length() ? -1 : editText.getSelectionEnd();
    decimalAmount = formatIBAN(decimalAmount);
    editText.setText(decimalAmount);

    if (selection != -1) {
        editText.setSelection(selection);
    } else {
        editText.setSelection(decimalAmount.length());
    }

}

public String formatIBAN(String text) {
    return formatterIBAN(new StringBuilder(text));
}

private String formatterIBAN(StringBuilder text) {
    int group = text.toString().length() / 5;
    int spaceCount = getSpaceCount(text);
    if (spaceCount < group) {
        return formatterIBAN(text.insert(4 + 5 * spaceCount, space));
    } else {
        return text.toString();
    }
}

private int getSpaceCount(StringBuilder text) {
    int spaceCount = 0;
    for (int index = 0; index < text.length(); index++) {
        if (text.charAt(index) == space.charAt(0)) {
            spaceCount++;
        }
    }
    return spaceCount;
}


textInputEditText.addTextChangedListener(new TextWatcherIBAN());
Samet ÖZTOPRAK
  • 3,112
  • 3
  • 32
  • 33
0
class XYZ : TextWatcher {

private val formatSymbols = DecimalFormatSymbols(Locale.getDefault())

private lateinit var formatter: DecimalFormat

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    .
    .
    formatSymbols.groupingSeparator = ' '
    formatter = DecimalFormat("####,####", formatSymbols)
    .
    .
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    editText.addTextChangedListener(this)
}

override fun afterTextChanged(s: Editable?) {
    if (editText.error != null) {
        editText.error = null
    }
    editText.removeTextChangedListener(this)
    try {
        var originalString = s.toString()
        if (originalString.contains(" ")) {
            originalString = originalString.replace(" ", "", true)
        }
        val longVal: Long? = originalString.toLong()
        val formattedString = formatter.format(longVal)
        editText.setText(formattedString)
        editText.setSelection(editText.text.length)
    } catch (error: NumberFormatException) {
        // Print Error Or Do Whatever you want.
    }
    editText.addTextChangedListener(this)
}

}
Praveen
  • 388
  • 3
  • 7
0

This is my implementation base on Igor Tyulkanov's idea, it has a small improvement that fix the cursor position problem

class CardNumbersInputWatcher(private val editText: EditText) : TextWatcher {
  companion object {
    private const val TOTAL_SYMBOLS = 19
    private const val DIVIDER_DISTANCE = 4
    private const val DIVIDER = ' '
  }

  override fun afterTextChanged(s: Editable) {
    if (!isInputCorrect(s, TOTAL_SYMBOLS, DIVIDER_DISTANCE, DIVIDER)) {
      val beforeCurPos = editText.selectionStart
      val beforeLength = s.length
      s.replace(0, s.length, buildCorrectString(s, TOTAL_SYMBOLS, DIVIDER_DISTANCE, DIVIDER))
      if (beforeLength > TOTAL_SYMBOLS && beforeCurPos <= s.length && editText.selectionStart < beforeCurPos) {
        editText.setSelection(beforeCurPos)
      }
    }
  }

  override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
  override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
}

private fun isInputCorrect(s: Editable, totalSymbols: Int, dividerDistance: Int, divider: Char): Boolean {
  if (s.length > totalSymbols) {
    return false
  }
  return s.withIndex().all { (index, c) ->
    if (index != 0 && ((index + 1) % (dividerDistance + 1) == 0)) {
      // it should be divider
      c == divider
    } else {
      c.isDigit()
    }
  }
}

private fun buildCorrectString(s: Editable, totalSymbols: Int, dividerDistance: Int, divider: Char): String {
  return buildString {
    for (c in s) {
      if (length >= totalSymbols) break
      if (!c.isDigit()) continue
      if (length > 0 && ((length + 1) % (dividerDistance + 1)) == 0) append(divider)
      append(c)
    }
  }
}

Khang .NT
  • 1,504
  • 6
  • 24
  • 46
0

1. Copy and paste this class

class EditTextForCards @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = androidx.appcompat.R.attr.editTextStyle
) : AppCompatEditText(context, attrs, defStyleAttr) {

    private var mCCPatterns = SparseArray<Pattern>()
    private var mSeparator: Separator = Separator.NONE
    private var mDrawableGravity: Gravity? = null/*Gravity.END*/
    private var isValidCard: Boolean = false
    private var mCurrentDrawableResId = Card.UNKNOWN.drawableRes

    val textWithoutSeparator
        get() = if (mSeparator == Separator.NONE) {
            text.toString()
        } else {
            text.toString().replace(mSeparator.toRegex(), "")
        }

    val isCardValid: Boolean
        get() = textWithoutSeparator.length > 12 && isValidCard

    val cardType: Card
        get() = Card.from(mCurrentDrawableResId)

    enum class Separator(private val stringValue: String) {
        NONE(""), SPACES(" "), DASHES("-");

        override fun toString() = stringValue

        internal fun toRegex() = stringValue.toRegex()

        internal val length
            get() = stringValue.length
    }

    enum class Gravity {
        START, END, LEFT, RIGHT
    }

    enum class Card(internal val value: Int, @field:DrawableRes internal val drawableRes: Int) {
        VISA(1, R.drawable.ic_visa),
        MASTERCARD(2, R.drawable.ic_mastercard),
        AMEX(4, R.drawable.amex),
        DISCOVER(8, R.drawable.discover),
        UNKNOWN(-1, R.drawable.ic_visa);

        companion object {
            internal fun from(@DrawableRes drawableRes: Int): Card {
                for (card in values()) {
                    if (card.drawableRes == drawableRes) {
                        return card
                    }
                }
                return UNKNOWN
            }
        }
    }

    private val textWatcher = object : TextWatcher {
        override fun afterTextChanged(s: Editable?) {}

        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}

        override fun onTextChanged(
            text: CharSequence,
            start: Int,
            lengthBefore: Int,
            lengthAfter: Int
        ) {
            val textWithoutSeparator = textWithoutSeparator

            var mDrawableResId = 0
            for (i in 0 until mCCPatterns.size()) {
                val key = mCCPatterns.keyAt(i)

                val p = mCCPatterns.get(key)

                val m = p.matcher(textWithoutSeparator)
                isValidCard = m.find()
                if (isValidCard) {
                    mDrawableResId = key
                    break
                }
            }
//            if (mDrawableResId != 0 && mDrawableResId != mCurrentDrawableResId) {
//                mCurrentDrawableResId = mDrawableResId
//            } else if (mDrawableResId == 0) {
//                mCurrentDrawableResId = Card.UNKNOWN.drawableRes
//            }
//            addDrawable()
            addSeparators()
        }
    }

    init {
        setDisabledCards()
        inputType = InputType.TYPE_CLASS_PHONE
        setSeparator(Separator.NONE)
//        setDrawableGravity(Gravity.END)
        attrs?.let { applyAttributes(it) }
        addTextChangedListener(textWatcher)
    }

    private fun applyAttributes(attrs: AttributeSet) {
        val a = context.theme.obtainStyledAttributes(
            attrs,
            R.styleable.EditTextForCards,
            0, 0
        )
        try {
            setSeparator(
                Separator.values()[a.getInt(
                    R.styleable.EditTextForCards_separator,
                    Separator.NONE.ordinal
                )]
            )
            setDisabledCardsInternal(a.getInt(R.styleable.EditTextForCards_disabledCards, 0))
            setDrawableGravity(
                Gravity.values()[a.getInt(
                    R.styleable.EditTextForCards_drawableGravity,
                    Gravity.END.ordinal
                )]
            )
        } finally {
            a.recycle()
        }
    }

    private fun addDrawable() {
        var currentDrawable = ContextCompat.getDrawable(context, mCurrentDrawableResId)
        if (currentDrawable != null && error.isNullOrEmpty()) {
            currentDrawable = resize(currentDrawable)
            when (mDrawableGravity) {
                Gravity.START -> setDrawablesRelative(start = currentDrawable)
                Gravity.RIGHT -> setDrawables(right = currentDrawable)
                Gravity.LEFT -> setDrawables(left = currentDrawable)
                else -> setDrawablesRelative(end = currentDrawable)
            }
        }
    }

    private fun addSeparators() {
        val text = text.toString()
        if (mSeparator != Separator.NONE) {
            if (text.length > 4 && !text.matches("(?:[0-9]{4}$mSeparator)+[0-9]{1,4}".toRegex())) {
                val sp = StringBuilder()
                val caretPosition = selectionEnd
                val segments = splitString(text.replace(mSeparator.toRegex(), ""))
                for (segment in segments) {
                    sp.append(segment).append(mSeparator)
                }
                setText("")
                append(sp.delete(sp.length - mSeparator.length, sp.length).toString())
                if (caretPosition < text.length)
                    setSelection(caretPosition)
            }
        }
    }

    private fun removeSeparators() {
        var text = text.toString()
        text = text.replace(" ".toRegex(), "").replace("-".toRegex(), "")
        setText("")
        append(text)
    }

    private fun splitString(s: String): Array<String?> {
        val arrayLength = ceil(s.length / 4.toDouble()).toInt()
        val result = arrayOfNulls<String>(arrayLength)

        var j = 0
        val lastIndex = result.size - 1
        for (i in 0 until lastIndex) {
            result[i] = s.substring(j, j + 4)
            j += 4
        }
        result[lastIndex] = s.substring(j)

        return result
    }

    /*@Deprecated("Please use the method that accepts a Separator enum instead.", ReplaceWith("this.setSeparator(Separator.)"))
    fun setSeparator(@IntRange(from = 0, to = 2) separator: Int) {
        require(!(separator > 2 || separator < 0)) {
            "The separator has to be one of the following:" +
                    "NO_SEPARATOR." +
                    "SPACES_SEPARATOR." +
                    "DASHES_SEPARATOR."
        }
        setSeparator(Separator.values()[separator])
    }*/

    /**
     * Use this method to set the separator style.
     * The default separator is [Separator.NONE].
     *
     * @param separator the style of the separator.
     */
    fun setSeparator(separator: Separator) {
        mSeparator = separator
        if (mSeparator != Separator.NONE) {
            filters = arrayOf<InputFilter>(InputFilter.LengthFilter(23))
            keyListener = DigitsKeyListener.getInstance("0123456789$mSeparator")
            addSeparators()
        } else {
            filters = arrayOf<InputFilter>(InputFilter.LengthFilter(19))
            keyListener = DigitsKeyListener.getInstance("0123456789")
            removeSeparators()
        }
    }

    /**
     * Use this method to set the location of the card drawable.
     * The default gravity is [Gravity.END].
     *
     * @param gravity the drawable location.
     */
    fun setDrawableGravity(gravity: Gravity) {
        mDrawableGravity = gravity
        addDrawable()
    }

    private fun setDisabledCardsInternal(disabledCards: Int) {
        val cards = ArrayList<Card>()
        if (containsFlag(disabledCards, Card.VISA.value)) {
            cards.add(Card.VISA)
        }
        if (containsFlag(disabledCards, Card.MASTERCARD.value)) {
            cards.add(Card.MASTERCARD)
        }
        /*if (containsFlag(disabledCards, Card.AMEX.value)) {
            cards.add(Card.AMEX)
        }
        if (containsFlag(disabledCards, Card.DISCOVER.value)) {
            cards.add(Card.DISCOVER)
        }*/
        setDisabledCards(*cards.toTypedArray())
    }

    @Deprecated(
        "Please use the method that accepts an array of Cards instead.",
        ReplaceWith("this.setDisabledCards(cards)")
    )
    fun setDisabledCards(disabledCards: Int) {
        setDisabledCardsInternal(disabledCards)
    }

    /**
     * Use this method to set which cards are disabled.
     * By default all supported cards are enabled.
     *
     * @param cards the cards to be disabled.
     */
    fun setDisabledCards(vararg cards: Card) {
        var disabledCards = 0
        for (card in cards) {
            disabledCards = disabledCards or card.value
        }
        mCCPatterns.clear()
        if (!containsFlag(disabledCards, Card.VISA.value)) {
            mCCPatterns.put(Card.VISA.drawableRes, Pattern.compile("^4[0-9]{1,12}(?:[0-9]{6})?$"))
        }
        if (!containsFlag(disabledCards, Card.MASTERCARD.value)) {
            mCCPatterns.put(Card.MASTERCARD.drawableRes, Pattern.compile("^5[1-5][0-9]{0,14}$"))
        }
        /*if (!containsFlag(disabledCards, Card.AMEX.value)) {
            mCCPatterns.put(Card.AMEX.drawableRes, Pattern.compile("^3[47][0-9]{0,13}$"))
        }
        if (!containsFlag(disabledCards, Card.DISCOVER.value)) {
            mCCPatterns.put(Card.DISCOVER.drawableRes, Pattern.compile("^6(?:011|5[0-9]{1,2})[0-9]{0,12}$"))
        }*/
        textWatcher.onTextChanged("", 0, 0, 0)
    }

    private fun containsFlag(flagSet: Int, flag: Int): Boolean {
        return flagSet or flag == flagSet
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        var noDrawablesVisible = true
        for (drawable in compoundDrawables) {
            if (drawable != null) {
                noDrawablesVisible = false
                break
            }
        }
        if (noDrawablesVisible) {
            addDrawable()
        }
    }

    private fun resize(image: Drawable) =
        when (val height = measuredHeight - (paddingTop + paddingBottom)) {
            in 1 until image.intrinsicHeight -> {
                val bitmap = (image as BitmapDrawable).bitmap
                val ratio = image.getIntrinsicWidth().toFloat() / image.intrinsicHeight.toFloat()
                val resizedBitmap =
                    Bitmap.createScaledBitmap(bitmap, (height * ratio).toInt(), height, false)
                resizedBitmap.density = Bitmap.DENSITY_NONE
                BitmapDrawable(resources, resizedBitmap)
            }
            in Int.MIN_VALUE..0 -> null
            else -> image
        }

    private fun setDrawablesRelative(
        start: Drawable? = null,
        top: Drawable? = null,
        end: Drawable? = null,
        bottom: Drawable? = null
    ) =
        /*TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(this, start, top, end, bottom)*/
        TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(this, null, null, null, null)

    private fun setDrawables(
        left: Drawable? = null,
        top: Drawable? = null,
        right: Drawable? = null,
        bottom: Drawable? = null
    ) =
        /*setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom)*/
        setCompoundDrawablesWithIntrinsicBounds(null, null, null, null)

    companion object {
        @Deprecated("This constant has been replace with an enum.", ReplaceWith("Separator.NONE"))
        const val NO_SEPARATOR = 0

        @Deprecated("This constant has been replace with an enum.", ReplaceWith("Separator.SPACES"))
        const val SPACES_SEPARATOR = 1

        @Deprecated("This constant has been replace with an enum.", ReplaceWith("Separator.DASHES"))
        const val DASHES_SEPARATOR = 2

        @Deprecated("This constant has been replace with an enum.", ReplaceWith("null"))
        const val NONE = 0

        @Deprecated("This constant has been replace with an enum.", ReplaceWith("Card.VISA"))
        const val VISA = 1

        @Deprecated("This constant has been replace with an enum.", ReplaceWith("Card.MASTERCARD"))
        const val MASTERCARD = 2

        @Deprecated("This constant has been replace with an enum.", ReplaceWith("Card.AMEX"))
        const val AMEX = 4

        @Deprecated("This constant has been replace with an enum.", ReplaceWith("Card.DISCOVER"))
        const val DISCOVER = 8
    }
}

2. paste this style

<declare-styleable name="EditTextForCards">
    <attr name="separator" format="enum">
        <enum name="no_separator" value="0" />
        <enum name="spaces" value="1" />
        <enum name="dashes" value="2" />
    </attr>
    <attr name="disabledCards">
        <flag name="none" value="0" />
        <flag name="visa" value="1" />
        <flag name="mastercard" value="2" />
        <flag name="amex" value="4" />
        <flag name="discover" value="8" />
    </attr>
    <attr name="drawableGravity">
        <enum name="start" value="0" />
        <enum name="end" value="1" />
        <enum name="left" value="2" />
        <enum name="right" value="3" />
    </attr>
</declare-styleable>

3. In your layout file, use it by

<EditTextForCards
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="@dimen/dp_5"
        android:digits="0123456789 "
        android:hint="@string/card_number"
        android:padding="@dimen/dp_20"
        android:textColor="@android:color/white"
        android:textColorHint="@android:color/white"
        android:textSize="@dimen/sp_16"
        app:separator="spaces" />
Suraj Rao
  • 29,388
  • 11
  • 94
  • 103
Kishan Solanki
  • 13,761
  • 4
  • 85
  • 82
0

benefits:

  1. It works when you insert numbers one by one
  2. It works when you paste all 16 character
  3. It handles back press
  4. It removes other characters automatically

in XML

android:digits="0123456789-"
android:maxLength="19"
android:inputType="number"

and the TextWatcher

class FourDigitCardFormatWatcher : TextWatcher {
    private var current = ""
    override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
    override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
    }
    override fun afterTextChanged(s: Editable) {

        if (s.toString() != current) {
            val userInput = s.toString().replace(nonDigits,"")
            if (userInput.length <= 16) {
                current = userInput.chunked(4).joinToString("-")
                s.filters = arrayOfNulls<InputFilter>(0)
            }
            s.replace(0, s.length, current, 0, current.length)
        }
    }

    companion object {
        private val nonDigits = Regex("[^\\d]")
    }
}
mohsen
  • 1,065
  • 16
  • 24
-1
import android.text.Editable;
import android.text.TextWatcher;
import android.widget.EditText;`

public class CreditCard implements TextWatcher
{
    EditText editText;

    public CreditCard(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) {

    }

    @Override
    public void afterTextChanged(Editable s) {
        try
        {
            editText.removeTextChangedListener(this);

            String str = editText.getText().toString().replaceAll("-", "");

            editText.setText(setDash(str));

            editText.setSelection(editText.getText().toString().length());

            editText.addTextChangedListener(this);
            return;
        }

        catch (Exception ex)
        {
            ex.printStackTrace();
            editText.addTextChangedListener(this);
        }

    }

    public static String setDash(String value)
    {
        String str = "";
        int j = 0;

        for (int i = 0;i<value.length(); i++)
        {
            j++;

            if (j == 5)
            {
                str = str+"-";
                j = 1;
            }

            str = str + value.charAt(i);
        }

        return str;

    }

    public static String trimDashOfString(String string)
    {
        if (string.contains("-")) {
            return string.replace("-", "");
            } else {
            return string;
        }

    }
}
Mesut Akcan
  • 899
  • 7
  • 19