Format credit card in edit text in android

Viewed 72126
28 Answers

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>

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())

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.

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
}

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"

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.

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

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

}

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

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" />
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;
        }

    }
}
Related