How to make EditText
accept input in format:
4digit 4digit 4digit 4digit
I tried Custom format edit text input android to acc
Here's the class that I use for credit card numbers. Usage examples below.
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>
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.
Please look at this project . Android form edit text is an extension of EditText that brings data validation facilities to the edittext
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;
}
}
}
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())