Domanda

Hi I am writing an edittext in which I want expiry date of the credit card in MM/YY format. The algorithm I want to implement is as follows: If user enters anything from 2 to 9. I change the text input to 02/ to 09/ If the user enters 1, then I wait for the next digit and check if the int value month if less than 12. Here is my code for this.

@Override
            public void afterTextChanged(Editable s) { 
            String input = s.toString();
                if (s.length() == 1) {
                        int month = Integer.parseInt(input);
                        if (month > 1) {
                            mExpiryDate.setText("0" + mExpiryDate.getText().toString() + "/");
                            mExpiryDate.setSelection(mExpiryDate.getText().toString().length());
                            mSeperator = true;
                        }

                }
                else if (s.length() == 2) {
                        int month = Integer.parseInt(input);
                        if (month <= 12) {
                            mExpiryDate.setText(mExpiryDate.getText().toString() + "/");
                            mExpiryDate.setSelection(mExpiryDate.getText().toString().length());                            
                            mSeperator = true;
                        }
                }
                else {

                }

            }

This works fine until I press a softkey back button. The back slash never goes back. The reason is the second if condition is always met. I am confused about how to solve this. How do I handle the back button inside aftertextchanged? Please help.

È stato utile?

Soluzione

See my comment above to understand your issue. You might use this to verify the user input with your textwatcher:

SimpleDateFormat formatter = 
    new SimpleDateFormat("MM/yy", Locale.GERMANY);
Calendar expiryDateDate = Calendar.getInstance();
try {
    expiryDateDate.setTime(formatter.parse(mExpiryDate.getText().toString()));
} catch (ParseException e) {
    //not valid
}
// expiryDateDate has a valid date from the user

So in complete it would be:

String lastInput ="";

@Override
public void afterTextChanged(Editable s) { 
     String input = s.toString();
     SimpleDateFormat formatter = new SimpleDateFormat("MM/yy", Locale.GERMANY);
    Calendar expiryDateDate = Calendar.getInstance();
    try {
        expiryDateDate.setTime(formatter.parse(input));
    } catch (ParseException e) {
        if (s.length() == 2 && !lastInput.endsWith("/")) {
            int month = Integer.parseInt(input);
            if (month <= 12) {
               mExpiryDate.setText(mExpiryDate.getText().toString() + "/");
            }
        }else if (s.length() == 2 && lastInput.endsWith("/")) {
            int month = Integer.parseInt(input);
            if (month <= 12) {
               mExpiryDate.setText(mExpiryDate.getText().toString().subStr(0,1);
            }
        }
        lastInput = mExpiryDate.getText().toString();
        //because not valid so code exits here
        return;
    }
    // expiryDateDate has a valid date from the user
    // Do something with expiryDateDate here
}

Finally the complete solution:

String input = s.toString();
SimpleDateFormat formatter = new SimpleDateFormat("MM/yy", Locale.GERMANY);
Calendar expiryDateDate = Calendar.getInstance();
try {
   expiryDateDate.setTime(formatter.parse(input));
} catch (ParseException e) {

} catch (java.text.ParseException e) {
if (s.length() == 2 && !mLastInput.endsWith("/")) {
   int month = Integer.parseInt(input);
   if (month <= 12) {
      mExpiryDate.setText(mExpiryDate.getText().toString() + "/");
      mExpiryDate.setSelection(mExpiryDate.getText().toString().length());
   }
}else if (s.length() == 2 && mLastInput.endsWith("/")) {
   int month = Integer.parseInt(input);
    if (month <= 12) {
       mExpiryDate.setText(mExpiryDate.getText().toString().substring(0,1));
       mExpiryDate.setSelection(mExpiryDate.getText().toString().length());
    } else {
       mExpiryDate.setText("");
       mExpiryDate.setSelection(mExpiryDate.getText().toString().length());
       Toast.makeText(getApplicationContext(), "Enter a valid month", Toast.LENGTH_LONG).show();
    }
} else if (s.length() == 1){
    int month = Integer.parseInt(input);
    if (month > 1) {
       mExpiryDate.setText("0" + mExpiryDate.getText().toString() + "/");
       mExpiryDate.setSelection(mExpiryDate.getText().toString().length());
    }
}
else {

}
mLastInput = mExpiryDate.getText().toString();
return;

Altri suggerimenti

@alex solution above was good but it failed in a couple of instances. Like when you try to delete the slash, because it never really reaches if(s.length() == 2 && mLastInput.endsWith("/")) when you try to delete slash it will be stock at if(s.length() == 2 && !mLastInput.endsWith("/") and so it will give the illusion that the slash does not delete.

It also fails if the user complete the date i.e 08/16, and then take their cursor back to month and delete, it also fails if the date maybe some how ends up like 0/1. So I just made some changes to @alex's solution above.

//Make sure for mExpiryDate to be accepting Numbers only
boolean isSlash = false; //class level initialization 
private void formatCardExpiringDate(Editable s){
    String input = s.toString();
    String mLastInput = "";

    SimpleDateFormat formatter = new SimpleDateFormat("MM/yy",     Locale.ENGLISH);
    Calendar expiryDateDate = Calendar.getInstance();

    try {
        expiryDateDate.setTime(formatter.parse(input));
    } catch (java.text.ParseException e) {
        if (s.length() == 2 && !mLastInput.endsWith("/") && isSlash) {
            isSlash = false;
            int month = Integer.parseInt(input);
            if (month <= 12) {
                     mExpiryDate.setText(mExpiryDate.getText().toString().substring(0, 1));
                mExpiryDate.setSelection(mExpiryDate.getText().toString().length());
            } else {
                s.clear();
                mExpiryDate.setText("");
                mExpiryDate.setSelection(mExpiryDate.getText().toString().length());
                Toast.makeText(context.getApplicationContext(), "Enter a valid month", Toast.LENGTH_LONG).show();
            }
        }else if (s.length() == 2 && !mLastInput.endsWith("/") && !isSlash) {
            isSlash = true;
            int month = Integer.parseInt(input);
            if (month <= 12) {
                mExpiryDate.setText(mExpiryDate.getText().toString() + "/");
                mExpiryDate.setSelection(mExpiryDate.getText().toString().length());
            }else if(month > 12){
                edCardDate.setText("");
                mExpiryDate.setSelection(mExpiryDate.getText().toString().length());
                s.clear();
                _toastMessage("invalid month", context);
            }


        } else if (s.length() == 1) {

            int month = Integer.parseInt(input);
            if (month > 1 && month < 12) {
                isSlash = true;
                mExpiryDate.setText("0" + mExpiryDate.getText().toString() + "/");
                mExpiryDate.setSelection(mExpiryDate.getText().toString().length());
            }
        }

        mLastInput = mExpiryDate.getText().toString();
        return;
    }
}
//wrap method formatCardExpiringDate around try catch or wrap the entire code in try catch, catching NumberFormateException. To take care of situations when s.length() == 2 and there is a a number in from of the slash
@Override
public void afterTextChanged(Editable s) { 
   try{
     formatCardExpiringDate(s)
    }catch(NumberFormatException e){
      s.clear(); 
      //Toast message here.. Wrong date formate

    }
}

I would check Month again when user click on submit just to be extra sure.

String expdate[] = mExpiryDate.getText().toString().split("/");
if(Integer.ParseInt(expDate[0]) > 12){
  // Toast message "wrong date format".... 
}

I hope this helps....

I used the solution of Uche Dim, fixed some issues and cleaned up the code.

So key improvements in my code are:

  1. if a user tries to enter "13", only "1" will get entered.
  2. when the user starts entering the year, after he has deleted the slash, a slash will be added so it keeps the format of MM/yy.

All in all this is almost like the Play Store's expiry field of new cards.

I've created a Kotlin class but usage is also added for Java.

CardExpiryTextWatcher class:

class CardExpiryTextWatcher(private val mTextInputLayout: TextInputLayout,
                            private val mServerDate: Date,
                            private val mListener: DateListener) : TextWatcher {

    private val mExpiryDateFormat = SimpleDateFormat("MM/yy", Locale.US).apply {
        isLenient = false
    }
    private var mLastInput = ""
    private var mIgnoreAutoValidationOnce = false

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

    override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
    }

    @SuppressLint("SetTextI18n")
    override fun afterTextChanged(s: Editable) {
        val input = s.toString()
        when (s.length) {
            1 -> handleMonthInputForFirstCharacter(input)
            2 -> handleMonthInputForSecondCharacter(input)
            3 -> addSlashIfNotAddedAtEnd(input)
            4 -> addSlashIfNotAddedInMiddle(input)
            5 -> validateDateAndCallListener(input)
        }
        mLastInput = mTextInputLayout.editText!!.text.toString()
    }

    private fun validateDateAndCallListener(input: String) {
        try {
            if (mIgnoreAutoValidationOnce) {
                mIgnoreAutoValidationOnce = false
                return
            }
            if (input[2] == '/') {
                val date = mExpiryDateFormat.parse(input)
                validateCardIsNotExpired(date)
            }
        } catch (e: ParseException) {
            mTextInputLayout.error = mTextInputLayout.context.getString(R.string.card_exp_date_error)
        }
    }

    private fun validateCardIsNotExpired(cardExpiry: Date) {
        if (DateUtils.isDateBefore(cardExpiry, mServerDate)) {
            mTextInputLayout.error = mTextInputLayout.context.getString(R.string.card_expired)
            return
        }
        mListener.onExpiryEntered(cardExpiry)
    }

    @SuppressLint("SetTextI18n")
    private fun addSlashIfNotAddedAtEnd(input: String) {
        val lastCharacter = input[input.length - 1]
        if (lastCharacter != '/' && !input.startsWith('/')) {
            val month = input.substring(0, 2)
            mTextInputLayout.editText!!.setText("$month/$lastCharacter")
            mTextInputLayout.editText!!.setSelection(mTextInputLayout.editText!!.text.toString().length)
        }
    }

    @SuppressLint("SetTextI18n")
    private fun addSlashIfNotAddedInMiddle(input: String) {
        if (input.contains('/')) {
            return
        }
        val month = input.substring(0, 2)
        val year = input.substring(2, 4)
        mIgnoreAutoValidationOnce = true
        mTextInputLayout.editText!!.setText("$month/$year")
        mTextInputLayout.editText!!.setSelection(2)
    }

    @SuppressLint("SetTextI18n")
    private fun handleMonthInputForSecondCharacter(input: String) {
        if (mLastInput.endsWith("/")) {
            return
        }
        val month = Integer.parseInt(input)
        if (month > 12) {
            mTextInputLayout.editText!!.setText(mLastInput)
            mTextInputLayout.editText!!.setSelection(mTextInputLayout.editText!!.text.toString().length)
            mTextInputLayout.error = mTextInputLayout.context.getString(R.string.card_exp_date_error)
        } else {
            mTextInputLayout.editText!!.setText("${mTextInputLayout.editText!!.text}/")
            mTextInputLayout.editText!!.setSelection(mTextInputLayout.editText!!.text.toString().length)
        }
    }

    @SuppressLint("SetTextI18n")
    private fun handleMonthInputForFirstCharacter(input: String) {
        val month = Integer.parseInt(input)
        if (month in 2..11) {
            mTextInputLayout.editText!!.setText("0${mTextInputLayout.editText!!.text}/")
            mTextInputLayout.editText!!.setSelection(mTextInputLayout.editText!!.text.toString().length)
        }
    }

    interface DateListener {
        fun onExpiryEntered(date: Date)
    }

    companion object {

        @JvmStatic
        fun attachTo(textInputLayout: TextInputLayout, serverDate: Date, listener: DateListener) {
            textInputLayout.editText!!.addTextChangedListener(
                    CardExpiryTextWatcher(textInputLayout, serverDate, listener))
        }
    }
}

Usage (Kotlin):

CardExpiryTextWatcher.attachTo(inputCardExpiry, mServerDate, object : CardExpiryTextWatcher.DateListener {
    override fun onExpiryEntered(date: Date) {
        // TODO implement your handling
    }
})

Usage (Java):

CardExpiryTextWatcher.attachTo(inputCardExpiry, mServerDate, new CardExpiryTextWatcher.DateListener() {
    @Override
    public void onExpiryEntered(@NonNull Date date) {
        // TODO implement your handling
    }
});

Caveat: The date will always be 2 digit long (e.g. December 4 will be 04/12, not 4/12) but if the user removes one digit from the date, it can become 4/12 so you need to run following method before validation:

/**
 * Makes sure that the date's day is of 2 digits, (e.g. 4/12 will be converted to 04/12)
 * */
fun normalizeExpiryDate(expiryDate: String): String {
    if (expiryDate.length == 4 && expiryDate.indexOf('/') == 1) {
        return "0$expiryDate"
    }
    return expiryDate
}

Note: inputCardExpiry is the InputTextLayout which contains the EditText.

May you be you can do like this:

boolean validateCardExpiryDate(String expiryDate) {
    return expiryDate.matches("(?:0[1-9]|1[0-2])/[0-9]{2}");
}

which translates as:

a non-capturing group ( Non capturing group? ) of: 0 followed by 1-9, or 1 followed by 0-2 followed by "/" followed by 0-9, twice. ...so this version requires zero-padded months (01 - 12). Add a ? after the first 0 to prevent this.

Hope it will help you..!!!

TextWatchers are used to update an external property (e.g. in your ViewModel) each time an edit is made.

TextWatchers should not be used to modify the EditTexts own text.

For Formatting input, you should use an InputFilter instead of a TextWatcher. Please try the following:

Include the following class in your project:

/**
 * InputFilter to ensure user enters valid expiry date in a credit card.
 * User is only allowed to type from beginning-to-end without copy-pasting or inserting characters in the middle.
 * The user may enter any month 01 -> 12.
 * The user can enter, at minimum, the current year or any year that follows.
 *
 * Note: `inputType` of the EditText should be `number` and `digits` should be `0123456789/`.
 *
 * Created by W.K.S on 30/07/2017 (Licensed under GNU Public License - original author must be credited)
 */

public class CreditCardExpiryInputFilter implements InputFilter {

    private final String currentYearLastTwoDigits;

    public CreditCardExpiryInputFilter() {
        currentYearLastTwoDigits = new SimpleDateFormat("yy", Locale.US).format(new Date());
    }

    public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
        //do not insert if length is already 5
        if (dest != null & dest.toString().length() == 5) return "";
        //do not insert more than 1 character at a time
        if (source.length() > 1) return "";
        //only allow character to be inserted at the end of the current text
        if (dest.length() > 0 && dstart != dest.length()) return "";

        //if backspace, skip
        if (source.length() == 0) {
            return source;
        }

        //At this point, `source` is a single character being inserted at `dstart`. 
        //`dstart` is at the end of the current text.

        final char inputChar = source.charAt(0);

        if (dstart == 0) {
            //first month digit
            if (inputChar > '1') return "";
        }
        if (dstart == 1) {
            //second month digit
            final char firstMonthChar = dest.charAt(0);
            if (firstMonthChar == '0' && inputChar == '0') return "";
            if (firstMonthChar == '1' && inputChar > '2') return "";

        }
        if (dstart == 2) {
            final char currYearFirstChar = currentYearLastTwoDigits.charAt(0);
            if (inputChar < currYearFirstChar) return "";
            return "/".concat(source.toString());
        }
        if (dstart == 4){
            final String inputYear = ""+dest.charAt(dest.length()-1)+source.toString();
            if (inputYear.compareTo(currentYearLastTwoDigits) < 0) return "";
        }

        return source;
    }
}

Apply the CreditCardExpiryInputFilter to your EditText:

EditText expiryEditText = findViewById(this, R.id.edittext_expiry_date);
expiryEditText.setFilters(new InputFilter[]{new CreditCardExpiryInputFilter()});

In the xml, set inputType to number, and digits to 0123456789/:

<EditText
    android:id="@+id/edittext_expiry_date"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:inputType="number"
    android:digits="0123456789/"
    />

Add TextWatcher to your EditText & do the Validation with REGEX.

TextWatcher:

etCardExpiry.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {

            }

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

            }

            @Override
            public void afterTextChanged(Editable editable) {
                if (editable.length() > 0 && (editable.length() % 3) == 0) {
                    final char c = editable.charAt(editable.length() - 1);
                    if ('/' == c) {
                        editable.delete(editable.length() - 1, editable.length());
                    }
                }
                if (editable.length() > 0 && (editable.length() % 3) == 0) {
                    char c = editable.charAt(editable.length() - 1);
                    if (Character.isDigit(c) && TextUtils.split(editable.toString(), String.valueOf("/")).length <= 2) {
                        editable.insert(editable.length() - 1, String.valueOf("/"));
                    }
                }
            }
        });

Validation with REGEX:

if (etCardExpiry.getText().toString().isEmpty()) {
                etCardExpiry.setError("Expiry cannot be empty. Format: MM/YY");
                return;
            } else if (etCardExpiry.getText().toString().length() < 5) {
                etCardExpiry.setError("Please check Card expiry & try again");
                return;
            } else if (etCardExpiry.getText().toString().matches("(?:0[1-9]|1[0-2])/[0-9]{2}")) {
                etCardExpiry.setError("Please check Card expiry format & try again");
                return;
            } else {
                // Passed Card Expiry validation
            }
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top