package me.pagar.card; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.Typeface; import android.support.annotation.ColorInt; import android.support.annotation.DrawableRes; import android.support.annotation.IntDef; import android.support.annotation.Nullable; import android.text.Editable; import android.text.InputFilter; import android.text.TextWatcher; import android.util.AttributeSet; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.TextView; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.regex.Pattern; import me.pagar.R; import static me.pagar.card.CardNumberFormat.ALL_DIGITS; import static me.pagar.card.CardNumberFormat.MASKED_ALL; import static me.pagar.card.CardNumberFormat.MASKED_ALL_BUT_LAST_FOUR; import static me.pagar.card.CardNumberFormat.ONLY_LAST_FOUR; import static me.pagar.card.CardType.AMERICAN_EXPRESS; import static me.pagar.card.CardType.AUTO; import static me.pagar.card.CardType.DISCOVER; import static me.pagar.card.CardType.MASTERCARD; import static me.pagar.card.CardType.PATTERN_AMERICAN_EXPRESS; import static me.pagar.card.CardType.PATTERN_DISCOVER; import static me.pagar.card.CardType.PATTERN_MASTER_CARD; import static me.pagar.card.CardType.VISA; public class FrontCreditCardView extends RelativeLayout { @IntDef({VISA, MASTERCARD, AMERICAN_EXPRESS, DISCOVER, AUTO}) @Retention(RetentionPolicy.SOURCE) public @interface CreditCardType { } @IntDef({ALL_DIGITS, MASKED_ALL_BUT_LAST_FOUR, ONLY_LAST_FOUR, MASKED_ALL}) @Retention(RetentionPolicy.SOURCE) public @interface CreditCardFormat { } private static final boolean DEBUG = false; private Context mContext; private String mCardNumber = ""; private String mCardName = ""; private String mExpiryDate = ""; private int mCardNumberTextColor = Color.WHITE; private int mCardNumberFormat = ALL_DIGITS; private int mCardNameTextColor = Color.WHITE; private int mExpiryDateTextColor = Color.WHITE; private int mValidTillTextColor = Color.WHITE; private int mType = VISA; private int mBrandLogo; private boolean mPutChip = false; private boolean mIsEditable = false; private boolean mIsCardNumberEditable = false; private boolean mIsCardNameEditable = false; private boolean mIsExpiryDateEditable = false; private int mHintTextColor = Color.WHITE; private Typeface creditCardTypeFace; private TextView cardNumber; private TextView cardName; private TextView expiryDate; private TextView validTill; private ImageView type; private ImageView brandLogo; private ImageView chip; public FrontCreditCardView(Context context) { this(context, null); } public FrontCreditCardView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); if (context != null) { this.mContext = context; } else { this.mContext = getContext(); } init(); loadAttributes(attrs); initDefaults(); addListeners(); } /** * Initialize various views and variables */ private void init() { final LayoutInflater inflater = (LayoutInflater) mContext .getSystemService(Context.LAYOUT_INFLATER_SERVICE); inflater.inflate(R.layout.credit_card_view_front, this, true); cardNumber = (TextView) findViewById(R.id.card_number); cardName = (TextView) findViewById(R.id.card_name); type = (ImageView) findViewById(R.id.card_logo); brandLogo = (ImageView) findViewById(R.id.brand_logo); chip = (ImageView) findViewById(R.id.chip); validTill = (TextView) findViewById(R.id.valid_thru_card); expiryDate = (TextView) findViewById(R.id.thru_date_card); } private void loadAttributes(@Nullable AttributeSet attrs) { final TypedArray a = mContext.getTheme().obtainStyledAttributes(attrs, R.styleable.FrontCreditCardView, 0, 0); try { mCardNumber = a.getString(R.styleable.FrontCreditCardView_cardNumber); mCardName = a.getString(R.styleable.FrontCreditCardView_cardName); mExpiryDate = a.getString(R.styleable.FrontCreditCardView_expiryDate); mCardNumberTextColor = a.getColor(R.styleable.FrontCreditCardView_cardNumberTextColor, Color.WHITE); mCardNumberFormat = a.getInt(R.styleable.FrontCreditCardView_cardNumberFormat, 0); mCardNameTextColor = a.getColor(R.styleable.FrontCreditCardView_cardNumberTextColor, Color.WHITE); mExpiryDateTextColor = a.getColor(R.styleable.FrontCreditCardView_expiryDateTextColor, Color.WHITE); mValidTillTextColor = a.getColor(R.styleable.FrontCreditCardView_validTillTextColor, Color.WHITE); mType = a.getInt(R.styleable.FrontCreditCardView_type, 0); mBrandLogo = a.getResourceId(R.styleable.FrontCreditCardView_brandLogo, 0); // mBrandLogoPosition = a.getInt(R.styleable.CreditCardView_brandLogoPosition, 1); mPutChip = a.getBoolean(R.styleable.FrontCreditCardView_putChip, false); mIsEditable = a.getBoolean(R.styleable.FrontCreditCardView_isEditable, false); //For more granular control to the fields. Issue #7 mIsCardNameEditable = a.getBoolean(R.styleable.FrontCreditCardView_isCardNameEditable, mIsEditable); mIsCardNumberEditable = a.getBoolean(R.styleable.FrontCreditCardView_isCardNumberEditable, mIsEditable); mIsExpiryDateEditable = a.getBoolean(R.styleable.FrontCreditCardView_isExpiryDateEditable, mIsEditable); mHintTextColor = a.getColor(R.styleable.FrontCreditCardView_hintTextColor, Color.WHITE); } finally { a.recycle(); } } private void initDefaults() { if(mIsCardNameEditable!=mIsEditable){ cardName.setEnabled(mIsCardNameEditable); } if(mIsCardNumberEditable!=mIsEditable){ cardNumber.setEnabled(mIsCardNumberEditable); } if(mIsExpiryDateEditable!=mIsEditable){ expiryDate.setEnabled(mIsExpiryDateEditable); } // If card number is not null, add space every 4 characters and format it in the appropriate // format if (mCardNumber != null) { cardNumber.setText(checkCardNumberFormat(addSpaceToCardNumber(mCardNumber))); } // Set the user entered card number color to card number field cardNumber.setTextColor(mCardNumberTextColor); // Added this check to fix the issue of custom view not rendering correctly in the layout // preview. if (!isInEditMode()) { cardNumber.setTypeface(creditCardTypeFace); } // If card name is not null, convert the text to upper case if (mCardName != null) { cardName.setText(mCardName.toUpperCase()); } // This filter will ensure the text entered is in uppercase when the user manually enters // the card name cardName.setFilters(new InputFilter[]{ new InputFilter.AllCaps() }); // Set the user entered card name color to card name field cardName.setTextColor(mCardNumberTextColor); // Added this check to fix the issue of custom view not rendering correctly in the layout // preview. if (!isInEditMode()) { cardName.setTypeface(creditCardTypeFace); } // Set the appropriate logo based on the type of card type.setBackgroundResource(getLogo(mType)); // If background logo attribute is present, set it as the brand logo background resource if (mBrandLogo != 0) { brandLogo.setBackgroundResource(mBrandLogo); // brandLogo.setLayoutParams(params); } // If putChip attribute is present, change the visibility of the putChip view and display it if (mPutChip) { chip.setVisibility(View.VISIBLE); } // If expiry date is not null, set it to the expiryDate TextView if (mExpiryDate != null) { expiryDate.setText(mExpiryDate); } // Set the user entered expiry date color to expiry date field expiryDate.setTextColor(mExpiryDateTextColor); // Added this check to fix the issue of custom view not rendering correctly in the layout // preview. if (!isInEditMode()) { expiryDate.setTypeface(creditCardTypeFace); } // Set the appropriate text color to the validTill TextView validTill.setTextColor(mValidTillTextColor); } private void addListeners() { // Add text change listener cardNumber.addTextChangedListener(new TextWatcher() { @Override public void onTextChanged(CharSequence s, int start, int before, int count) { // Change card type to auto to dynamically detect the card type based on the card // number mType = AUTO; } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void afterTextChanged(Editable s) { // Delete any spaces the user might have entered manually. The library automatically // adds spaces after every 4 characters to the view. mCardNumber = s.toString().replaceAll("\\s+", ""); } }); // Add focus change listener to detect focus being shifted from the cardNumber EditText cardNumber.setOnFocusChangeListener(new OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { // If the field just lost focus if (!hasFocus) { //Fix for NPE. Issue #6 if(mCardNumber != null) { if (mCardNumber.length() > 12) { // If the length of card is >12, add space every 4 characters and format it // in the appropriate format cardNumber .setText(checkCardNumberFormat(addSpaceToCardNumber(mCardNumber))); // If card type is "auto",find the appropriate logo if (mType == AUTO) { type.setBackgroundResource(getLogo(mType)); } } } } } }); cardName.addTextChangedListener(new TextWatcher() { @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) { // Set the mCardName attribute the user entered value in the Card Name field mCardName = s.toString().toUpperCase(); } }); expiryDate.addTextChangedListener(new TextWatcher() { @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) { // Set the mExpiryDate attribute the user entered value in the Expiry Date field mExpiryDate = s.toString(); } }); } private void redrawViews() { invalidate(); requestLayout(); } public String getCardNumber() { return cardNumber.getText().toString(); } public void setCardNumber(String cardNumber) { this.cardNumber.setText(cardNumber.replaceAll("\\s+", "")); } public String getCardName() { return cardName.getText().toString(); } public void setCardName(String cardName) { this.cardName.setText(cardName.toUpperCase()); } @ColorInt public int getCardNumberTextColor() { return mCardNumberTextColor; } public void setCardNumberTextColor(@ColorInt int cardNumberTextColor) { mCardNumberTextColor = cardNumberTextColor; redrawViews(); } @CreditCardFormat public int getCardNumberFormat() { return mCardNumberFormat; } public void setCardNumberFormat(@CreditCardFormat int cardNumberFormat) { if (cardNumberFormat < 0 | cardNumberFormat > 3) { throw new UnsupportedOperationException("CardNumberFormat: " + cardNumberFormat + " " + "is not supported. Use `CardNumberFormat.*` or `CardType.ALL_DIGITS` if " + "unknown"); } mCardNumberFormat = cardNumberFormat; redrawViews(); } @ColorInt public int getCardNameTextColor() { return mCardNameTextColor; } public void setCardNameTextColor(@ColorInt int cardNameTextColor) { mCardNameTextColor = cardNameTextColor; redrawViews(); } public String getExpiryDate() { return expiryDate.getText().toString(); } public void setExpiryDate(String expiryDate) { this.expiryDate.setText(expiryDate); } @ColorInt public int getExpiryDateTextColor() { return mExpiryDateTextColor; } public void setExpiryDateTextColor(@ColorInt int expiryDateTextColor) { mExpiryDateTextColor = expiryDateTextColor; redrawViews(); } @ColorInt public int getValidTillTextColor() { return mValidTillTextColor; } public void setValidTillTextColor(@ColorInt int validTillTextColor) { mValidTillTextColor = validTillTextColor; redrawViews(); } @CreditCardType public int getType() { return mType; } public void setType(@CreditCardType int type) { if (type < 0 | type > 4) { throw new UnsupportedOperationException("CardType: " + type + " is not supported. " + "Use `CardType.*` or `CardType.AUTO` if unknown"); } mType = type; redrawViews(); } public boolean getIsEditable() { return mIsEditable; } public void setIsEditable(boolean isEditable) { mIsEditable = isEditable; redrawViews(); } public boolean getIsCardNameEditable() { return mIsCardNameEditable; } public void setIsCardNameEditable(boolean isCardNameEditable) { mIsCardNameEditable = isCardNameEditable; redrawViews(); } public boolean getIsCardNumberEditable() { return mIsCardNumberEditable; } public void setIsCardNumberEditable(boolean isCardNumberEditable) { mIsCardNumberEditable = isCardNumberEditable; redrawViews(); } public boolean getIsExpiryDateEditable() { return mIsExpiryDateEditable; } public void setIsExpiryDateEditable(boolean isExpiryDateEditable) { mIsExpiryDateEditable = isExpiryDateEditable; redrawViews(); } @ColorInt public int getHintTextColor() { return mHintTextColor; } public void setHintTextColor(@ColorInt int hintTextColor) { mHintTextColor = hintTextColor; redrawViews(); } @DrawableRes public int getBrandLogo() { return mBrandLogo; } public void setBrandLogo(@DrawableRes int brandLogo) { mBrandLogo = brandLogo; redrawViews(); } public int getBrandLogoPosition() { return mBrandLogo; } public void setBrandLogoPosition(int brandLogoPosition) { redrawViews(); } public void putChip(boolean flag) { mPutChip = flag; redrawViews(); } /** * Return the appropriate drawable resource based on the card type * * @param type type of card. */ @DrawableRes private int getLogo(@CreditCardType int type) { switch (type) { case VISA: return R.mipmap.visa; case MASTERCARD: return R.mipmap.mastercard; case AMERICAN_EXPRESS: return R.mipmap.amex; case DISCOVER: return R.mipmap.discover; case AUTO: return findCardType(); default: throw new UnsupportedOperationException("CardType: " + type + " is not supported" + ". Use `CardType.*` or `CardType.AUTO` if unknown"); } } /** * Returns the formatted card number based on the user entered value for card number format * * @param cardNumber Card Number. */ private String checkCardNumberFormat(String cardNumber) { if (DEBUG) { Log.e("Card Number", cardNumber); } switch (getCardNumberFormat()) { case MASKED_ALL_BUT_LAST_FOUR: cardNumber = "**** **** **** " + cardNumber.substring(cardNumber.length() - 4, cardNumber.length()); break; case ONLY_LAST_FOUR: cardNumber = cardNumber.substring(cardNumber.length() - 4, cardNumber.length()); break; case MASKED_ALL: cardNumber = "**** **** **** ****"; break; default: //do nothing. break; } return cardNumber; } /** * Returns the appropriate card type drawable resource based on the regex pattern of the card * number */ @DrawableRes private int findCardType() { int type = VISA; if (cardNumber.length() > 0) { final String cardNumber = getCardNumber().replaceAll("\\s+", ""); if (Pattern.compile(PATTERN_MASTER_CARD).matcher(cardNumber).matches()) { type = MASTERCARD; } else if (Pattern.compile(PATTERN_AMERICAN_EXPRESS).matcher(cardNumber) .matches()) { type = AMERICAN_EXPRESS; } else if (Pattern.compile(PATTERN_DISCOVER).matcher(cardNumber).matches()) { type = DISCOVER; } } setType(type); return getLogo(type); } /** * Adds space after every 4 characters to the card number if the card number is divisible by 4 * * @param cardNumber Card Number. */ private String addSpaceToCardNumber(String cardNumber) { if (cardNumber.length() % 4 != 0) { return cardNumber; } else { final StringBuilder result = new StringBuilder(); for (int i = 0; i < cardNumber.length(); i++) { if (i % 4 == 0 && i != 0 && i != cardNumber.length() - 1) { result.append(" "); } result.append(cardNumber.charAt(i)); } return result.toString(); } } }