问题
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.
回答1:
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;
回答2:
@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....
回答3:
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..!!!
回答4:
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 EditText
s 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/"
/>
回答5:
I used the solution of Uche Dim, fixed some issues and cleaned up the code.
So key improvements in my code are:
- if a user tries to enter "13", only "1" will get entered.
- 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.
来源:https://stackoverflow.com/questions/20607860/formatting-expiry-date-in-mm-yy-format