Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save danielesegato/a349891f9a474546ed3dd4946eab18f3 to your computer and use it in GitHub Desktop.
Save danielesegato/a349891f9a474546ed3dd4946eab18f3 to your computer and use it in GitHub Desktop.
Android TextInputLayout with credit card mask

This is a Fork of a gist to format credit cards numbers as-you-type on Android using Spans. The fork fixes some issues of the original one and makes this a small utility library. The Gif doesn't exactly match the behavior. See Fork notes below for details.

In this example I will explain how you can build a TextInputLayout that shows credit card numeber.

The goal is to add a margin between every four digits without adding a space (" ") while you are inserting text.

Screen

You need to put a TextWatcher on a EditText.
The TextWatcher needs to add a margin every four digits.

Example:

    private EditText etCreditCard;
    private CreditCardFormatTextWatcher tv;

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

        etCreditCard = (EditText) findViewById(R.id.cc);

        // the EditText here is used to compute the padding (1 EM) which depends
        // on the actual size of the Text rendered on screen taking into account:
        // the font, the text size, the user scaling on text
        tv = new CreditCardFormatTextWatcher(etCreditCard);
        etCreditCard.addTextChangedListener(tv);

    }

FORK notes

Problems fixed:

  • max length is no longer hardcoded, it's actually made optional and disabled by default (there are card of 19 digits)
  • editing the number in the middle used to cause the spacing to pop-up everywhere, this has been fixed
  • padding is no longer hardcoded (20 pixels) and textSize / density independant by default

Differences:

  • the padding right after every 4th character is added only when the 5th character is added and not immediately after the 4th character (the cursor always stay next to the last typed character until you type a new character)
  • padding can be set in Pixels, SP (Scale DIP) or EM units (see below)
  • max length disabled by default and modifiable (still think it shouldn't be handled by this widget)
  • single TextChanger class implementation instead of inline
  • padding can be specified in EM units, which is more interesting then SP because it change with the text size and font, just provide the TextView along with the em value
  • utility can be used statically to format an Editable

Gif differences:

  • this code does not handle any built-in N/16 counter below the edit text (nor does the original) -- and it's a bad idea anyway since the length of the card number is not always 16 characters
  • the margin every 4 character is not added immediately after the firth character is typed but when the fifth one is
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.Editable;
import android.text.Spannable;
import android.text.TextWatcher;
import android.text.style.ReplacementSpan;
import android.util.TypedValue;
import android.widget.TextView;
/**
* Format a credit card number with padding every 4 digits. Optionally cut to specified maxLength
*/
public class CreditCardFormatTextWatcher implements TextWatcher {
public static final int NO_MAX_LENGTH = -1;
private int maxLength = NO_MAX_LENGTH;
private int paddingPx;
private boolean internalStopFormatFlag;
/**
* Create a credit card formatter with no max length and a padding specified in pixels
*
* @param paddingPx padding in pixels unit
*/
public CreditCardFormatTextWatcher(int paddingPx) {
setPaddingPx(paddingPx);
}
/**
* Create a credit card formatter with no max length and a padding of 1 em (depends on text size).
* <p>
* The padding is not automatically updated if the text size or typeface are changed in the textview).
*
* @param textView the widget you want to format
*/
public CreditCardFormatTextWatcher(@NonNull TextView textView) {
setPaddingEm(textView, 1f);
}
/**
* Create a credit card formatter with no max length and a padding specified in em (depends on text size).
* <p>
* The padding is not automatically updated if the text size or typeface are changed in the textview).
*
* @param textView the widget you want to format
* @param paddingEm padding in em unit (character size unit)
*/
public CreditCardFormatTextWatcher(@NonNull TextView textView, float paddingEm) {
setPaddingEm(textView, paddingEm);
}
/**
* Create a credit card formatter with no max length and a padding specified in SP Unit (depends on the scale applied to text).
*
* @param context any Context
* @param paddingSp the padding in SP unit
*/
public CreditCardFormatTextWatcher(@NonNull Context context, float paddingSp) {
setPaddingSp(context, paddingSp);
}
/**
* Change the padding, do not take effect until next text change
*
* @param paddingPx padding in pixels unit
*/
public void setPaddingPx(int paddingPx) {
this.paddingPx = paddingPx;
}
/**
* Change the padding, do not take effect until next text change
*
* @param textView the widget you want to format
* @param em padding in em unit (character size unit)
*/
public void setPaddingEm(@NonNull TextView textView, float em) {
float emSize = textView.getPaint().measureText("x");
setPaddingPx((int) (em * emSize));
}
/**
* Change the padding, do not take effect until next text change
*
* @param context any Context
* @param paddingSp the padding in SP unit
*/
public void setPaddingSp(@NonNull Context context, float paddingSp) {
setPaddingPx((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, paddingSp, context.getResources().getDisplayMetrics()));
}
/**
* Change maxLength of the credit card number, does not take effect until next text change
*
* @param maxLength new max length
*/
public void setMaxLength(int maxLength) {
this.maxLength = maxLength;
}
@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 (internalStopFormatFlag) {
return;
}
internalStopFormatFlag = true;
formatCardNumber(s, paddingPx, maxLength);
internalStopFormatFlag = false;
}
/**
* Format the provided widget card number (useful if you want to reformat it after changing padding)
*
* @param textView the widget containing the credit card number
*/
public void formatCardNumber(@NonNull TextView textView) {
afterTextChanged(textView.getEditableText());
}
public static void formatCardNumber(@NonNull Editable ccNumber, int paddingPx) {
formatCardNumber(ccNumber, paddingPx, NO_MAX_LENGTH);
}
public static void formatCardNumber(@NonNull Editable ccNumber, int paddingPx, int maxLength) {
int textLength = ccNumber.length();
// first remove any previous span
PaddingRightSpan[] spans = ccNumber.getSpans(0, ccNumber.length(), PaddingRightSpan.class);
for (int i = 0; i < spans.length; i++) {
ccNumber.removeSpan(spans[i]);
}
// then truncate to max length
if (maxLength > 0 && textLength > maxLength - 1) {
ccNumber.replace(maxLength, textLength, "");
}
// finally add margin spans
for (int i = 1; i <= ((textLength - 1) / 4); i++) {
int end = i * 4;
int start = end - 1;
PaddingRightSpan marginSPan = new PaddingRightSpan(paddingPx);
ccNumber.setSpan(marginSPan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
public static class PaddingRightSpan extends ReplacementSpan {
private int mPadding;
public PaddingRightSpan(int padding) {
mPadding = padding;
}
@Override
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
float[] widths = new float[end - start];
paint.getTextWidths(text, start, end, widths);
int sum = mPadding;
for (int i = 0; i < widths.length; i++) {
sum += widths[i];
}
return sum;
}
@Override
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) {
canvas.drawText(text, start, end, x, y, paint);
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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.support.design.widget.TextInputLayout
android:layout_width="300dp"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
app:counterMaxLength="16"
app:counterEnabled="true">
<android.support.design.widget.TextInputEditText
android:id="@+id/cc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Credit Card Number"
android:digits="1234567890"
android:inputType="number|textNoSuggestions"
/>
</android.support.design.widget.TextInputLayout>
</RelativeLayout>
@theGraeme
Copy link

Hey, this is really great, thanks!

Quick note: There's a bug in this gist. If maxlength is set to 16, then if user tries to enter a 17th character, it will correctly strip the extra character, but it will add a space after the last digit.

The way to fix this to add the following at line 146:

textLength--;

(Since the last character was removed in line 145, the textLength needs to be decremented to reflect the shorter string, or the for loop at line 148 will iterate one time too many)

@kingnightdrifter
Copy link

None solution did work but this ... Really thank you mate

@Ami1608
Copy link

Ami1608 commented Nov 9, 2020

wow, I try to solve the backspace issue. This is a great code. Now, I don't get that issue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment