EditText selection on focus not working - android

Since I am very disappointed in a way that Android is doing decimal input I decided to write my own EditDecimal control that inherits from EditText. I want it to select all on click and to put cursor on first position on focus. I don't want cursor to show because it's usless on most Android phones (you mostly cannot put it in right place and it makes me very nervous)
Problem is when you tap on control - FocusChanged is called but it does not set position of (hidden) cursor on first position but on position where user tapped. I can not find the problem... is there some other event that happens after FocusChange that moves cursor?
public class EditDecimal : EditText
{
// Every constructor is calling Initialize ...
private void Initialize()
{
FocusChange += OnFocusChanged;
Click += OnClicked;
SetCursorVisible(false);
}
private void OnFocusChanged(object sender, FocusChangeEventArgs e)
{
if (IsFocused)
SetSelection(1);
}
private void OnClicked(object sender, EventArgs e)
{
SelectAll();
}
}

I finnaly solved it. I had to subscribe to OnTouch event and to change cursor position from there.
public class EditDecimal : EditText
{
// Every constructor is calling Initialize ...
private InputMethodManager _imm ;
public int DecimalSpaces { get; set; }
readonly DecimalFormatSymbols _dfs = new DecimalFormatSymbols();
private void Initialize(Context context, IAttributeSet attrs)
{
AfterTextChanged += OnAfterTextChanged;
SetSelectAllOnFocus(true);
SetCursorVisible(false);
Touch += OnTouch;
_imm = (InputMethodManager)context.GetSystemService(Context.InputMethodService);
var a = context.ObtainStyledAttributes(attrs, Resource.Styleable.EditDecimal);
try
{
DecimalSpaces = a.GetInteger(Resource.Styleable.EditDecimal_decimalSpaces, 2);
}
finally
{
a.Recycle();
}
}
private void OnTouch(object sender, TouchEventArgs e)
{
base.OnTouchEvent(e.Event);
if (e.Event.Action == MotionEventActions.Up)
{
SelectAll();
}
}
protected override void OnFocusChanged(bool gainFocus, FocusSearchDirection direction, Rect previouslyFocusedRect)
{
base.OnFocusChanged(gainFocus, direction, previouslyFocusedRect);
SelectAll();
}
private void OnAfterTextChanged(object sender, AfterTextChangedEventArgs e)
{
const char ch1 = '.';
const char ch2 = ',';
switch (_dfs.DecimalSeparator)
{
case ch2:
{
if (Text.Contains(ch1.ToString()))
{
var position = Text.IndexOf(ch1);
if (Text.Contains(ch2.ToString()))
e.Editable.Delete(position, position + 1);
else
e.Editable.Replace(position, position + 1, ch2.ToString());
}
// we have to prevent showing two commas in number
var firstCommaPosition = Text.IndexOf(ch2);
var lastCommaPosition = Text.LastIndexOf(ch2);
if (firstCommaPosition > 0 && lastCommaPosition > 0 && firstCommaPosition != lastCommaPosition)
e.Editable.Delete(lastCommaPosition, lastCommaPosition+1);
}
break;
case ch1:
{
if (Text.Contains(ch2.ToString()))
{
var position = Text.IndexOf(ch2);
e.Editable.Delete(position, position + 1);
}
// we have to prevent showing two points in number
var firstPointPosition = Text.IndexOf(ch1);
var lastPointPosition = Text.LastIndexOf(ch1);
if (firstPointPosition > 0 && lastPointPosition > 0 && firstPointPosition != lastPointPosition)
e.Editable.Delete(lastPointPosition, lastPointPosition + 1);
}
break;
}
//thnx to http://stackoverflow.com/users/2240673/tom
var length = e.Editable.Length();
if (length <= 0) return;
if (NrOfDecimal(e.Editable.ToString()) > DecimalSpaces)
e.Editable.Delete(length - 1, length);
}
private int NrOfDecimal(string nr)
{
if (nr == null) return 0;
var nrCharArray = nr.ToCharArray();
var len = nr.Length;
var pos = len;
for (var i = 0; i < len; i++)
{
if (nrCharArray[i] != '.') continue;
pos = i + 1;
break;
}
return len - pos;
}
}
What came as suprize to me was a fact that EditText selection behaves differently if SetCursorVisible is set to true than when it is set to false. I thought that it was just visibility property.

Grepcode is your friend. It shows that TextView's onFocusChanged method checks if there's an Editor object defined for the View (which is the case with EditText) and a look at the Editor code shows this at line 889 ff:
// If a tap was used to give focus to that view, move cursor at tap position.
// Has to be done before onTakeFocus, which can be overloaded.
final int lastTapPosition = getLastTapPosition();
if (lastTapPosition >= 0) {
Selection.setSelection((Spannable) mTextView.getText(), lastTapPosition);
}
So perhaps the best way would be to write your own implementation of Editor. Although if have no idea right now how you attach that to your TextView resp. EditText. class.
Or you just set the selection back to the first position if that is what you want.

Related

TextView with spans , how can I know which one is clicked on?

I am using this library, but from the onClick styles that they have, neither handles my needed case.
https://github.com/splitwise/TokenAutoComplete
So I wanted to do my own. So I have a ContactsCompletionView, which is a TextView. and I override the onTouchEvent like this:
override fun onTouchEvent(event: MotionEvent): Boolean {
val action = event.actionMasked
val text = text
var handled = super.onTouchEvent(event)
if (isFocused && text != null && action == MotionEvent.ACTION_UP) {
val offset = getOffsetForPosition(event.x, event.y)
if (offset != -1) {
var offseted = text.substring(offset, text.length)
var indexLeft = offseted.indexOf("(") + 1
var indexRight = offseted.indexOf(")")
if (indexLeft > 0 && indexRight > indexLeft)
Toast.makeText(context, offseted.substring(indexLeft, indexRight), Toast.LENGTH_SHORT).show()
}
}
return handled
}
This is what they had, but I cannot use TokenImageSpan because it is a protected class:
#Override
public boolean onTouchEvent(#NonNull MotionEvent event) {
int action = event.getActionMasked();
Editable text = getText();
boolean handled = false;
if (tokenClickStyle == TokenClickStyle.None) {
handled = super.onTouchEvent(event);
}
if (isFocused() && text != null && lastLayout != null && action == MotionEvent.ACTION_UP) {
int offset = getOffsetForPosition(event.getX(), event.getY());
if (offset != -1) {
TokenImageSpan[] links = text.getSpans(offset, offset, TokenImageSpan.class);
if (links.length > 0) {
links[0].onClick();
handled = true;
} else {
//We didn't click on a token, so if any are selected, we should clear that
clearSelections();
}
}
}
if (!handled && tokenClickStyle != TokenClickStyle.None) {
handled = super.onTouchEvent(event);
}
return handled;
}
My code works, but my issue is that whenever I press an TAG at the end of it. It gets the next object. I assume that this is because I just use:
val offset = getOffsetForPosition(event.x, event.y)
if (offset != -1) {
var offseted = text.substring(offset, text.length)
}
When they use:
if (offset != -1) {
TokenImageSpan[] links = text.getSpans(offset, offset, TokenImageSpan.class);
}
TokenImageSpan extends ImageSpan, so I can use it like that, But I do not know how to take the text from the ImageSpan. Any ideas how I can fix this please?
You can use clickable span as below:
SpannableString ss = new SpannableString("your string comes
here");
ClickableSpan clickableSpan = new ClickableSpan() {
#Override
public void onClick(View textView) {
//do your stuff here on click
}
#Override
public void updateDrawState(TextPaint ds) {
super.updateDrawState(ds);
ds.setUnderlineText(false);
}
};
//set click range
ss.setSpan(clickableSpan, 8, 15,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
//your text view or edittext
textView.setText(ss);
textView.setMovementMethod(LinkMovementMethod.getInstance());
textView.setHighlightColor(Color.TRANSPARENT);
This is what I used:
override fun onTouchEvent(event: MotionEvent): Boolean {
val action = event.actionMasked
val text = text
var handled = super.onTouchEvent(event)
var offset = getOffsetForPosition(event.getX(), event.getY())
if (isFocused && text != null && action == MotionEvent.ACTION_UP) {
var cursor = this#ContactsCompletionView.selectionEnd
if (cursor < 0)
cursor = 0
val links: Array<ViewSpan> = text.getSpans(0, cursor, ViewSpan::class.java)
if (objects.size > 0 && objects.size >= links.size && links.size > 0 && offset < cursor)
Snackbar().make(this, objects[links.size - 1].address, com.google.android.material.snackbar.Snackbar.LENGTH_SHORT).show()
}
return handled
}
The library if ClickStyle is set to NONE, it moves the cursor at the end of the chosen object. So I create a substring of that, check how many spans I have, and then I use that as an index for my objects. in order to show the first one on the left of cursor

EditText Cursor positioning Issue

I am using below function to forcefully making the first letter of each word to Capital letter in Edittext.
public String capitalizeFirstLetterWord(String s) {
StringBuilder cap = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
try {
char x = s.charAt(i);
if (x == ' ') {
cap.append(" ");
char y = s.charAt(i + 1);
cap.append(Character.toUpperCase(y));
i++;
} else {
cap.append(x);
}
} catch (IndexOutOfBoundsException ignored) {
}
}
//finally, capitalize the first letter of the sentence
String sentence = cap.toString();
if (sentence.length() > 0) {
sentence = String.valueOf(sentence.charAt(0)).toUpperCase(); //capitalize first letter
if (cap.toString().length() > 1) { //check if there's succeeding letters
sentence += cap.toString().substring(1); //append it also
}
}
return sentence;
}
and calling it in afterTextChange() method as below :
#Override
public void afterTextChanged(Editable editable) {
if (getActivity().getCurrentFocus() == mEdtName) {
if (editable.toString().length() > 0 &&
!editable.toString().equals(mOldName)) {
mOldName = editable.toString(); //prevent infinite loop
mEdtName.setText(capitalizeFirstLetterWord(editable.toString()));
mEdName.setSelection(mEdGymName.getText().length()); //set the cursor to the end of the editText
}
}
}
But, the issue is when I trying to erase a character from the middle of the Whole string in Editext. The Cursor is moving at the end of the Text.
.
It's because of the below line in afterTextChanged() method.
If I comment that line, Cursor moves to the first position.
.
What might be the solution ?
try this :
#Override
public void afterTextChanged(Editable editable) {
if (getActivity().getCurrentFocus() == mEdtName) {
if (editable.toString().length() > 0 && !editable.toString().equals(mOldName)) {
int selection = mEditName.getSelectionEnd();
mOldName = editable.toString(); //prevent infinite loop
mEditName.removeTextChangedListener(this);
mEditName.setText(capitalizeFirstLetterWord(editable.toString()));
mEditName.setSelection(selection);
mEditName.addTextChangedListener(this);
}
}
}
I think you should use InputFilter to do that.

android numberpicker can't read keyboard number input

I'm trying to create a numberpicker to select a month.
It works aslong as I select the value through scrolling or if i use the keyboard to input the month by text (e.g. "jan" for januari)
I also want my users to be able to input '1' to select januari.
From numberpicker source code, it seems this should be possible:
/**
* #return The selected index given its displayed <code>value</code>.
*/
private int getSelectedPos(String value) {
if (mDisplayedValues == null) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
// Ignore as if it's not a number we don't care
}
} else {
for (int i = 0; i < mDisplayedValues.length; i++) {
// Don't force the user to type in jan when ja will do
value = value.toLowerCase();
if (mDisplayedValues[i].toLowerCase().startsWith(value)) {
return mMinValue + i;
}
}
/*
* The user might have typed in a number into the month field i.e.
* 10 instead of OCT so support that too.
*/
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
// Ignore as if it's not a number we don't care
}
}
return mMinValue;
}
The problem is, if I try to input a number, the EditText just stays empty.
This is how I initialise my numberpicker:
//getting the months using Calendar
Calendar cal = Calendar.getInstance();
Map<String, Integer> monthMap = cal.getDisplayNames(Calendar.MONTH, Calendar.LONG, Locale.getDefault());
TreeMap<Integer, String> sorted = new TreeMap<>();
for(Map.Entry<String, Integer> entry : monthMap.entrySet()) {
sorted.put(entry.getValue(), entry.getKey());
}
String[] displayNames = sorted.values().toArray(new String[]{});
mMonthPicker.setMinValue(0);
mMonthPicker.setMaxValue(11);
mMonthPicker.setDisplayedValues(displayNames);
mMonthPicker.setWrapSelectorWheel(false);
I tried setting the inputtype for the edittext to InputType.TYPE_NULL using the answer given here but that didn't change anything.
The Edit Text stays empty if I try to input a number.
I finally figured it out. The numberpicker didn't allow numbers for month input because of the filter on the EditText.
I solved it by copying some code of the NumberKeyListener from numberpicker source code and adjusting it so it would accept numeric input.
Then i added this filter on the EditText, which I look up by going through the childviews and checking if the current view is an EditText.
I found the code for looking up the EditText in the answer here: NumberPicker doesn't work with keyboard
my code looks like this:
mInputText = findInput(mMonthPicker);
mInputText.setFilters(new InputFilter[]{ new InputTextFilter() });
this is my filter:
/**
* Filter for accepting only valid indices or prefixes of the string
* representation of valid indices.
*/
class InputTextFilter extends NumberKeyListener {
/**
* The numbers accepted by the input text's {#link android.view.LayoutInflater.Filter}
*/
private final char[] DIGIT_CHARACTERS = new char[] {
// Latin digits are the common case
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
// Arabic-Indic
'\u0660', '\u0661', '\u0662', '\u0663', '\u0664', '\u0665', '\u0666', '\u0667', '\u0668'
, '\u0669',
// Extended Arabic-Indic
'\u06f0', '\u06f1', '\u06f2', '\u06f3', '\u06f4', '\u06f5', '\u06f6', '\u06f7', '\u06f8'
, '\u06f9',
// Hindi and Marathi (Devanagari script)
'\u0966', '\u0967', '\u0968', '\u0969', '\u096a', '\u096b', '\u096c', '\u096d', '\u096e'
, '\u096f',
// Bengali
'\u09e6', '\u09e7', '\u09e8', '\u09e9', '\u09ea', '\u09eb', '\u09ec', '\u09ed', '\u09ee'
, '\u09ef',
// Kannada
'\u0ce6', '\u0ce7', '\u0ce8', '\u0ce9', '\u0cea', '\u0ceb', '\u0cec', '\u0ced', '\u0cee'
, '\u0cef'
};
// XXX This doesn't allow for range limits when controlled by a
// soft input method!
public int getInputType() {
return InputType.TYPE_CLASS_TEXT;
}
#Override
protected char[] getAcceptedChars() {
return DIGIT_CHARACTERS;
}
#Override
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
Log.v("filter", "source:" + source.toString());
CharSequence filtered = String.valueOf(source.subSequence(start, end));
Log.v("filter", "filtered:" + filtered.toString());
if (TextUtils.isEmpty(filtered)) {
return "";
}
String result = String.valueOf(dest.subSequence(0, dstart)) + filtered
+ dest.subSequence(dend, dest.length());
String str = String.valueOf(result).toLowerCase();
try{
int value = Integer.parseInt(str);
if(1 <= value && value <= 12) {
return source;
}
} catch(NumberFormatException e) {
//continue with the checking
}
for (String val : mMonthPicker.getDisplayedValues()) {
String valLowerCase = val.toLowerCase();
if (valLowerCase.startsWith(str)) {
final int selstart = result.length();
final int selend = val.length();
mInputText.post(new Runnable() {
#Override
public void run() {
mInputText.setSelection(selstart, selend);
}
});
return val.subSequence(dstart, val.length());
}
}
return "";
}
}
and this is the code to find the EditText:
private EditText findInput(ViewGroup np) {
int count = np.getChildCount();
for (int i = 0; i < count; i++) {
final View child = np.getChildAt(i);
if (child instanceof ViewGroup) {
findInput((ViewGroup) child);
} else if (child instanceof EditText) {
return (EditText) child;
}
}
return null;
}
Just to elaborate onto this, here is what I did to get the EditText input value (expanding on SnyersK code). The custom edit text can also be set to disable keyboard input and have a min and max value in xml:
xml use :
<com.octane.smartlink.widgets.SmartLinkNumberPicker
android:id="#+id/distance_tenths_of_distance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
max="9"
min="0"/>
Android widget class:
public class SmartLinkNumberPicker extends NumberPicker {
//declare needed variables
private EditText numberEditText;
private boolean shouldDisableEditText = true; //set to true to disable edit text input and focus
public SmartLinkNumberPicker(Context context) {
super(context);
}
public SmartLinkNumberPicker(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs);
}
public SmartLinkNumberPicker(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(attrs);
}
private void init(AttributeSet attrs) {
numberEditText = populateEditText((ViewGroup) this);
if(numberEditText != null && shouldDisableEditText)
numberEditText.setFocusable(false);
processAttributeSet(attrs);
}
private EditText populateEditText(ViewGroup viewGroup) {
int count = viewGroup.getChildCount();
for(int i = 0; i < count; i++) {
final View child = viewGroup.getChildAt(i);
if (child instanceof ViewGroup)
populateEditText((ViewGroup) child);
else if (child instanceof EditText)
return (EditText) child;
}
return null;
}
/*
* used to set min, max attributes in xml
* set by min = "0" and max = "9"
*/
private void processAttributeSet(AttributeSet attrs) {
this.setMinValue(attrs.getAttributeIntValue(null, "min", 0));
this.setMaxValue(attrs.getAttributeIntValue(null, "max", 0));
}
/*
* always returns a valid number since number input is only allowed
* this will capture value set by either plus/minus buttons or edit text input
*/
#Override
public int getValue() {
return Integer.valueOf(numberEditText.getEditableText().toString());
}
}

How to Automatically add thousand separators as number is input in EditText

Im creating a convertor application. I want to set the EditText so that when the user is inputting the number to be converted, a thousand separator (,) should be added automatically in realtime to the number once it increments by 3 figures: thousand, million, billion etc.
And when erased to below 4 figures the number goes back to normal.
Any help?
Even-though It's late. Intended for future visitors.
Fetures of the following codes
Puts thousand separator in EditText as it's text changes.
adds 0. Automatically when pressed period (.) At First.
Ignores 0 input at Beginning.
Just copy the following
Class named
NumberTextWatcherForThousand which implements TextWatcher
import android.text.Editable;
import android.text.TextWatcher;
import android.widget.EditText;
import java.util.StringTokenizer;
/**
* Created by skb on 12/14/2015.
*/
public class NumberTextWatcherForThousand implements TextWatcher {
EditText editText;
public NumberTextWatcherForThousand(EditText editText) {
this.editText = editText;
}
#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) {
try
{
editText.removeTextChangedListener(this);
String value = editText.getText().toString();
if (value != null && !value.equals(""))
{
if(value.startsWith(".")){
editText.setText("0.");
}
if(value.startsWith("0") && !value.startsWith("0.")){
editText.setText("");
}
String str = editText.getText().toString().replaceAll(",", "");
if (!value.equals(""))
editText.setText(getDecimalFormattedString(str));
editText.setSelection(editText.getText().toString().length());
}
editText.addTextChangedListener(this);
return;
}
catch (Exception ex)
{
ex.printStackTrace();
editText.addTextChangedListener(this);
}
}
public static String getDecimalFormattedString(String value)
{
StringTokenizer lst = new StringTokenizer(value, ".");
String str1 = value;
String str2 = "";
if (lst.countTokens() > 1)
{
str1 = lst.nextToken();
str2 = lst.nextToken();
}
String str3 = "";
int i = 0;
int j = -1 + str1.length();
if (str1.charAt( -1 + str1.length()) == '.')
{
j--;
str3 = ".";
}
for (int k = j;; k--)
{
if (k < 0)
{
if (str2.length() > 0)
str3 = str3 + "." + str2;
return str3;
}
if (i == 3)
{
str3 = "," + str3;
i = 0;
}
str3 = str1.charAt(k) + str3;
i++;
}
}
public static String trimCommaOfString(String string) {
// String returnString;
if(string.contains(",")){
return string.replace(",","");}
else {
return string;
}
}
}
Use This Class on your EditText as follows
editText.addTextChangedListener(new NumberTextWatcherForThousand(editText));
To get the input as plain Double Text
Use the trimCommaOfString method of the same class like this
NumberTextWatcherForThousand.trimCommaOfString(editText.getText().toString())
Git
You can use String.format() in a TextWatcher. The comma in the format specifier does the trick.
This does not work for floating point input. And be careful not to set an infinite loop with the TextWatcher.
public void afterTextChanged(Editable view) {
String s = null;
try {
// The comma in the format specifier does the trick
s = String.format("%,d", Long.parseLong(view.toString()));
} catch (NumberFormatException e) {
}
// Set s back to the view after temporarily removing the text change listener
}
public static String doubleToStringNoDecimal(double d) {
DecimalFormat formatter = (DecimalFormat) NumberFormat.getInstance(Locale.US);
formatter.applyPattern("#,###");
return formatter.format(d);
}
This sample app deconstructs formatting numbers clearly.
To summarize the link above, use a TextWatcher and in the afterTextChanged() method format the EditText view with the following logic:
#Override
public void afterTextChanged(Editable s) {
editText.removeTextChangedListener(this);
try {
String originalString = s.toString();
Long longval;
if (originalString.contains(",")) {
originalString = originalString.replaceAll(",", "");
}
longval = Long.parseLong(originalString);
DecimalFormat formatter = (DecimalFormat) NumberFormat.getInstance(Locale.US);
formatter.applyPattern("#,###,###,###");
String formattedString = formatter.format(longval);
//setting text after format to EditText
editText.setText(formattedString);
editText.setSelection(editText.getText().length());
} catch (NumberFormatException nfe) {
nfe.printStackTrace();
}
editText.addTextChangedListener(this);
}
I know i am very late to the party but it may be very useful for future users. My answer is an extension of Shree Krishna's answer.
Improvements:
Thousands separators and Decimal markers are locale aware i.e. they are used accordingly to the Locale of the device.
The cursor position doesn't change after deleting or adding elements in the middle also (In his answer cursor was reset to the end).
The overall quality of the code has been improved specially the getDecimalFormattedString method.
Code:
import android.text.Editable;
import android.text.TextWatcher;
import android.widget.EditText;
import java.text.DecimalFormat;
/**
* Created by srv_twry on 4/12/17.
* Source: https://stackoverflow.com/a/34265406/137744
* The custom TextWatcher that automatically adds thousand separators in EditText.
*/
public class ThousandSeparatorTextWatcher implements TextWatcher {
private DecimalFormat df;
private EditText editText;
private static String thousandSeparator;
private static String decimalMarker;
private int cursorPosition;
public ThousandSeparatorTextWatcher(EditText editText) {
this.editText = editText;
df = new DecimalFormat("#,###.##");
df.setDecimalSeparatorAlwaysShown(true);
thousandSeparator = Character.toString(df.getDecimalFormatSymbols().getGroupingSeparator());
decimalMarker = Character.toString(df.getDecimalFormatSymbols().getDecimalSeparator());
}
#Override
public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
cursorPosition = editText.getText().toString().length() - editText.getSelectionStart();
}
#Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
#Override
public void afterTextChanged(Editable s) {
try {
editText.removeTextChangedListener(this);
String value = editText.getText().toString();
if (value != null && !value.equals("")) {
if (value.startsWith(decimalMarker)) {
String text = "0" + decimalMarker;
editText.setText(text);
}
if (value.startsWith("0") && !value.startsWith("0" + decimalMarker)) {
int index = 0;
while (index < value.length() && value.charAt(index) == '0') {
index++;
}
String newValue = Character.toString(value.charAt(0));
if (index != 0) {
newValue = value.charAt(0) + value.substring(index);
}
editText.setText(newValue);
}
String str = editText.getText().toString().replaceAll(thousandSeparator, "");
if (!value.equals("")) {
editText.setText(getDecimalFormattedString(str));
}
editText.setSelection(editText.getText().toString().length());
}
//setting the cursor back to where it was
editText.setSelection(editText.getText().toString().length() - cursorPosition);
editText.addTextChangedListener(this);
} catch (Exception ex) {
ex.printStackTrace();
editText.addTextChangedListener(this);
}
}
private static String getDecimalFormattedString(String value) {
String[] splitValue = value.split("\\.");
String beforeDecimal = value;
String afterDecimal = null;
String finalResult = "";
if (splitValue.length == 2) {
beforeDecimal = splitValue[0];
afterDecimal = splitValue[1];
}
int count = 0;
for (int i = beforeDecimal.length() - 1; i >= 0 ; i--) {
finalResult = beforeDecimal.charAt(i) + finalResult;
count++;
if (count == 3 && i > 0) {
finalResult = thousandSeparator + finalResult;
count = 0;
}
}
if (afterDecimal != null) {
finalResult = finalResult + decimalMarker + afterDecimal;
}
return finalResult;
}
/*
* Returns the string after removing all the thousands separators.
* */
public static String getOriginalString(String string) {
return string.replace(thousandSeparator,"");
}
}
This solution has some advantage over other answers. For example, it keeps the user's cursor position even if they edit the beginning or middle of the number. Other solutions always jump the cursor to the end of the number. It handles decimals and whole numbers, as well as locales that use characters other than . for the decimal separator and , for the thousands grouping separator.
class SeparateThousands(val groupingSeparator: String, val decimalSeparator: String) : TextWatcher {
private var busy = false
override fun afterTextChanged(s: Editable?) {
if (s != null && !busy) {
busy = true
var place = 0
val decimalPointIndex = s.indexOf(decimalSeparator)
var i = if (decimalPointIndex == -1) {
s.length - 1
} else {
decimalPointIndex - 1
}
while (i >= 0) {
val c = s[i]
if (c == groupingSeparator[0] ) {
s.delete(i, i + 1)
} else {
if (place % 3 == 0 && place != 0) {
// insert a comma to the left of every 3rd digit (counting from right to
// left) unless it's the leftmost digit
s.insert(i + 1, groupingSeparator)
}
place++
}
i--
}
busy = false
}
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
}
Then in xml:
<EditText
android:id="#+id/myNumberField"
android:digits=",.0123456789"
android:inputType="numberDecimal"
.../>
And finally register the watcher:
findViewById(R.id.myNumberField).addTextChangedListener(
SeparateThousands(groupingSeparator, decimalSeparator))
To handle . vs , in different locales use groupingSeparator and decimalSeparator, which can come from DecimalFormatSymbols or localized strings.
I just wanted comma to be placed and this is working for me:
String.format("%,.2f", myValue);
Here is my ThousandNumberEditText class
public class ThousandNumberEditText extends android.support.v7.widget.AppCompatEditText {
// TODO: 14/09/2017 change it if you want
private static final int MAX_LENGTH = 20;
private static final int MAX_DECIMAL = 3;
public ThousandNumberEditText(Context context) {
this(context, null);
}
public ThousandNumberEditText(Context context, AttributeSet attrs) {
this(context, attrs, android.support.v7.appcompat.R.attr.editTextStyle);
}
public ThousandNumberEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
addTextChangedListener(new ThousandNumberTextWatcher(this));
setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
setFilters(new InputFilter[] { new InputFilter.LengthFilter(MAX_LENGTH) });
setHint("0"); // TODO: 14/09/2017 change it if you want
}
private static class ThousandNumberTextWatcher implements TextWatcher {
private EditText mEditText;
ThousandNumberTextWatcher(EditText editText) {
mEditText = editText;
}
#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) {
String originalString = editable.toString();
String cleanString = originalString.replaceAll("[,]", "");
if (cleanString.isEmpty()) {
return;
}
String formattedString = getFormatString(cleanString);
mEditText.removeTextChangedListener(this);
mEditText.setText(formattedString);
mEditText.setSelection(mEditText.getText().length());
mEditText.addTextChangedListener(this);
}
/**
* Return the format string
*/
private String getFormatString(String cleanString) {
if (cleanString.contains(".")) {
return formatDecimal(cleanString);
} else {
return formatInteger(cleanString);
}
}
private String formatInteger(String str) {
BigDecimal parsed = new BigDecimal(str);
DecimalFormat formatter;
formatter = new DecimalFormat("#,###");
return formatter.format(parsed);
}
private String formatDecimal(String str) {
if (str.equals(".")) {
return ".";
}
BigDecimal parsed = new BigDecimal(str);
DecimalFormat formatter;
formatter =
new DecimalFormat("#,###." + getDecimalPattern(str)); //example patter #,###.00
return formatter.format(parsed);
}
/**
* It will return suitable pattern for format decimal
* For example: 10.2 -> return 0 | 10.23 -> return 00 | 10.235 -> return 000
*/
private String getDecimalPattern(String str) {
int decimalCount = str.length() - 1 - str.indexOf(".");
StringBuilder decimalPattern = new StringBuilder();
for (int i = 0; i < decimalCount && i < MAX_DECIMAL; i++) {
decimalPattern.append("0");
}
return decimalPattern.toString();
}
}
}
Using
<.ThousandNumberEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
You can use this method:
myEditText.addTextChangedListener(new TextWatcher() {
#Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
#Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
String input = s.toString();
if (!input.isEmpty()) {
input = input.replace(",", "");
DecimalFormat format = new DecimalFormat("#,###,###");
String newPrice = format.format(Double.parseDouble(input));
myEditText.removeTextChangedListener(this); //To Prevent from Infinite Loop
myEditText.setText(newPrice);
myEditText.setSelection(newPrice.length()); //Move Cursor to end of String
myEditText.addTextChangedListener(this);
}
}
#Override
public void afterTextChanged(final Editable s) {
}
});
And to get original text use this:
String input = myEditText.getText().toString();
input = input.replace(",", "");
Since i had the same problem i decided to find a solution to it
Find my function below i hope it helps people finding solution
securityDeposit.addTextChangedListener(new TextWatcher() {
#Override
public void onTextChanged(CharSequence s, int start,
int before, int count) {
// TODO Auto-generated method stub
}
#Override
public void beforeTextChanged(CharSequence s, int start,
int before, int count) {
// TODO Auto-generated method stub
}
#Override
public void afterTextChanged(Editable s) {
// TODO Auto-generated method stub
if (s.toString().trim().length() > 0) {
int rentValue = Integer.parseInt(s.toString()
.replaceAll(",", ""));
StringBuffer rentVal = new StringBuffer();
if (rentValue > 10000000) {
s.clear();
s.append("10,000,000");
} else {
if (s.length() == 4) {
char x[] = s.toString().toCharArray();
char y[] = new char[x.length + 1];
for (int z = 0; z < y.length; z++) {
if (z == 1) {
y[1] = ',';
} else {
if (z == 0)
y[z] = x[z];
else {
y[z] = x[z - 1];
}
}
}
for (int z = 0; z < y.length; z++) {
rentVal = rentVal.append(y[z]);
}
s.clear();
s.append(rentVal);
}
}
}
}
});
you can use this code in many ways in your program, you give it a string and it separate each three from right and place space there.
private String Spacer(String number){
StringBuilder strB = new StringBuilder();
strB.append(number);
int Three = 0;
for(int i=number.length();i>0;i--){
Three++;
if(Three == 3){
strB.insert(i-1, " ");
Three = 0;
}
}
return strB.toString();
}// end Spacer()
u can change it a bit and use it ontextchangelistener.
good luck
The answers here lack a method to handle actual user input, such as deleting characters or copying and pasting. This is an EditText field. If you want to add formatting in, you need to support editing that formatted value.
This implementation still has a deficiency depending on your use case. I didn't care about decimal values and assumed I would only be handling whole numbers. There's enough of how to handle that on this page and how to handle actual internationalization that I'll leave that as an exercise to the reader. If you need to do that, it shouldn't be too difficult to add "." to the regular expression to keep the decimal; you'll just have to be careful to acknowledge the numeral string still has a non numerical character.
This is designed to be used throughout multiple activities. New it once, give it your edit text and your data model and ignore it. The model binding can be removed if you don't need it.
public class EditNumberFormatter implements TextWatcher {
private EditText watched;
private Object model;
private Field field;
private IEditNumberFormatterListener listener;
private ActiveEdit activeEdit;
/**
* Binds an EditText to a data model field (Such as a room entity's public variable)
* Whenever the edit text is changed, the text is formatted to the local numerical format.
*
* Handles copy/paste/backspace/select&delete/typing
*
* #param model An object with a public field to bind to
* #param fieldName A field defined on the object
* #param watched The edit text to watch for changes
* #param listener Another object that wants to know after changes & formatting are done.
*/
public EditNumberFormatter(Object model, String fieldName, EditText watched, IEditNumberFormatterListener listener) {
this.model = model;
this.watched = watched;
this.listener = listener;
try {
field = model.getClass().getDeclaredField(fieldName);
} catch(Exception e) { }
watched.addTextChangedListener(this);
}
#Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
activeEdit = new ActiveEdit(s.toString(), start, count);
}
#Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
activeEdit.recordChangedText(s.toString(),count);
}
#Override
public void afterTextChanged(Editable s) {
this.watched.removeTextChangedListener(this);
activeEdit.processEdit(); // Override the user's edit of the formatted string with what the user intended to do to the numeral.
watched.setText(activeEdit.getCurrentFormattedString());
watched.setSelection(activeEdit.getCursorPosition());
updateDataModel(activeEdit.getCurrentRawValue());
listener.FormatUpdated(watched.getId(), activeEdit.getCurrentRawValue(), activeEdit.getCurrentFormattedString());
this.watched.addTextChangedListener(this);
}
private void updateDataModel(int rawValue) {
try {
field.set(model, rawValue);
} catch (IllegalAccessException e) { }
}
/**
* Tracks the active editing of an EditText formatted for integer input
*/
private class ActiveEdit {
private String priorFormattedString;
private String currentFormattedString;
private String currentNumericalString;
private int currentRawValue;
private boolean removal;
private boolean addition;
private int changeStart;
private int removedCount;
private int additionCount;
private int numeralCountBeforeSelection;
private int numeralCountAdded;
private int numeralCountRemoved;
/**
* Call in beforeEdit to begin recording changes
*
* #param beforeEdit string before edit began
* #param start start position of edit
* #param removed number of characters removed
*/
public ActiveEdit(String beforeEdit, int start, int removed) {
removal = (removed > 0);
priorFormattedString = beforeEdit;
changeStart = start;
removedCount = removed;
numeralCountBeforeSelection = countNumerals(priorFormattedString.substring(0, changeStart));
numeralCountRemoved = countNumerals(priorFormattedString.substring(changeStart, changeStart + removedCount));
}
/**
* Call in onTextChanged to record new text and how many characters were added after changeStart
*
* #param afterEdit new string after user input
* #param added how many characters were added (same start position as before)
*/
public void recordChangedText(String afterEdit, int added) {
addition = (added > 0);
additionCount = added;
numeralCountAdded = countNumerals(afterEdit.substring(changeStart, changeStart + additionCount));
currentNumericalString = afterEdit.replaceAll("[^0-9]", "");
}
/**
* Re-process the edit for our particular formatting needs.
*/
public void processEdit() {
forceRemovalPastFormatting();
finalizeEdit();
}
/**
* #return Integer value of the field after an edit.
*/
public int getCurrentRawValue() {
return currentRawValue;
}
/**
* #return Formatted number after an edit.
*/
public String getCurrentFormattedString() {
return currentFormattedString;
}
/**
* #return Cursor position after an edit
*/
public int getCursorPosition() {
int numeralPosition = numeralCountBeforeSelection + numeralCountAdded;
return positionAfterNumeralN(currentFormattedString,numeralPosition);
}
/**
* If a user deletes a value, but no numerals are deleted, then delete the numeral proceeding
* their cursor. Otherwise, we'll just add back the formatting character.
*
* Assumes formatting uses a single character and not multiple formatting characters in a row.
*/
private void forceRemovalPastFormatting() {
if (removal && (!addition) && (numeralCountRemoved == 0)) {
String before = currentNumericalString.substring(0, numeralCountBeforeSelection - 1);
String after = currentNumericalString.substring(numeralCountBeforeSelection);
currentNumericalString = before + after;
numeralCountRemoved++;
numeralCountBeforeSelection--;
}
}
/**
* Determine the result of the edit, including new display value and raw value
*/
private void finalizeEdit() {
currentFormattedString = "";
currentRawValue = 0;
if (currentNumericalString.length() == 0) {
return; // There is no entry now.
}
try {
currentRawValue = Integer.parseInt(currentNumericalString);
} catch (NumberFormatException nfe) {
abortEdit(); // Value is not an integer, return to previous state.
return;
}
currentFormattedString = String.format("%,d", currentRawValue);
}
/**
* Current text, same as the old text.
*/
private void abortEdit() {
currentFormattedString = priorFormattedString;
currentNumericalString = currentFormattedString.replaceAll("[^0-9]", "");
numeralCountRemoved = 0;
numeralCountAdded = 0;
try {
currentRawValue = Integer.parseInt(currentNumericalString);
} catch (Exception e) { currentRawValue = 0; }
}
/**
* Determine how many numerical characters exist in a string
* #param s
* #return the number of numerical characters in the string
*/
private int countNumerals(String s) {
String newString = s.replaceAll("[^0-9]", "");
return newString.length();
}
/**
* Determine how to place a cursor after the Nth Numeral in a formatted string.
* #param s - Formatted string
* #param n - The position of the cursor should follow the "Nth" number in the string
* #return the position of the nth character in a formatted string
*/
private int positionAfterNumeralN(String s, int n) {
int numeralsFound = 0;
if (n == 0) {
return 0;
}
for (int i = 0; i < s.length(); i++) {
if(s.substring(i,i+1).matches("[0-9]")) {
if(++numeralsFound == n) {
return i + 1;
}
}
}
return s.length();
}
}
}
At a highlevel, what that does is:
Determine which numbers were actually in the string after it was edited
Process the edit to the numeral version of the string if the numbers weren't edited
Convert the numeral back to a formatted string
Determine, where the cursor should be based on where editing began and how much text was added
It also nicely handles edge cases like completely deleted input, integer overflow and erroneous input.
You can use a custom TextInputEditText :
public class NumberTextInputEditText extends TextInputEditText {
public NumberTextInputEditText(#NonNull Context context) {
super(context);
}
public NumberTextInputEditText(#NonNull Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
}
public NumberTextInputEditText(#NonNull Context context, #Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
addTextChangedListener(textWatcher);
}
public String formatNumber(double number) {
DecimalFormat decimalFormat = new DecimalFormat("#,###");
return decimalFormat.format(number);
}
public TextWatcher textWatcher = 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) {
removeTextChangedListener(this);
String text = getText().toString();
String format = "";
if (!TextUtils.isEmpty(text)) {
try {
format = formatNumber(Double.parseDouble(new BigDecimal(text.replaceAll(",", "")).toString()));
} catch (NumberFormatException e) {
format = "";
}
setText(format);
setSelection(format.length());
}
addTextChangedListener(this);
}
};}
just use it like a view in your layout:
<com.your.package.name.NumberTextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
Here i have tested my application code. text-watcher how to add comma in currency thousand, lake currency.
private TextWatcher textWatcherAmount = new TextWatcher() {
#Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
#Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
String initial = s.toString();
if (inputEdtHawalaRate == null) return;
if (!TextUtils.isEmpty(initial)) {
initial = initial.replace(",", "");
NumberFormat formatter = new DecimalFormat("##,##,###");
inputEdtHawalaRate.removeTextChangedListener(this);
double myNumber = Double.parseDouble(initial);
String processed = formatter.format(myNumber);
//Assign processed text
inputEdtHawalaRate.setText(processed);
try {
inputEdtHawalaRate.setSelection(processed.length());
} catch (Exception e) {
e.printStackTrace();
}
//Give back the listener
inputEdtHawalaRate.addTextChangedListener(this);
}
}
#Override
public void afterTextChanged(Editable s) {
}
};
if (inputEdtHawalaRate != null) {
inputEdtHawalaRate.addTextChangedListener(textWatcherAmount);
}
// getting amount on double type varaible (On textwatcher editetxt value get).
String amount = Objects.requireNonNull(inputEdtHawalaRate.getText()).toString().trim();
double hawalaAmount = 0.0;
String[] a = amount.split(",");
finalAmount = TextUtils.join("", a);
hawalaAmount = Double.parseDouble(finalAmount);
I was looking for a locale aware solution since we have customers across the globe. So I built upon dr0pdb's answer.
Here's a TextWatcher class (in kotlin) I have created to solve this.
https://github.com/abhilashd-locus/edittext-locale-aware-thousands-separator
Features:
Add thousands separator dynamically as the user types
Enable editing in between the string and not only at the ends
Style of thousands separation is based upon the locale (eg: 100,000 vs 1,00,000)
Symbol of thousands separator and decimal marker is based on the locale (eg: 100,000.00 vs 100.000,00)
Supports all languages and locales
Disadvantages:
Does not support copy/paste operations
In right-to-left languages (eg. Arabic), the cursor jumps to the end on deleting the first number
.
// ThousandsSeparatorTextWatcher.kt --> add this TextWatcher to the
// EditText you want to add the functionality of dynamic locale aware thousands separator
class ThousandsSeparatorTextWatcher(private var editText: EditText?, private val callback: TextChangedCallback) : TextWatcher {
//keeping a count of the digits before the cursor to reset the cursor at the correct place
private var digitsBeforeCursor = -1
private val thousandSeparator: Char = DecimalFormatSymbols(Locale.getDefault()).groupingSeparator
private val decimalMarker: Char = DecimalFormatSymbols(Locale.getDefault()).decimalSeparator
init {
editText?.apply {
addTextChangedListener(this#ThousandsSeparatorTextWatcher)
//disabling copy/paste to avoid format and parse errors
disableTextSelection(this)
//diabling text selection
isLongClickable = false
setTextIsSelectable(false)
//ensuring correct input type
keyListener = DigitsKeyListener.getInstance("0123456789$decimalMarker");
}
}
private fun disableTextSelection(editText: EditText) {
editText.customSelectionActionModeCallback = object : android.view.ActionMode.Callback {
override fun onActionItemClicked(mode: android.view.ActionMode?, item: MenuItem?) = false
override fun onCreateActionMode(mode: android.view.ActionMode?, menu: Menu?) = false
override fun onPrepareActionMode(mode: android.view.ActionMode?, menu: Menu?) = false
override fun onDestroyActionMode(mode: android.view.ActionMode?) {}
}
}
/***
* We are going to calculate the number of numeric digits before the cursor when user starts editing
* We will keep a count of this number to reset the cursor to the correct position after editing is complete
*/
override fun beforeTextChanged(sequenceBeforeEdit: CharSequence, startPos: Int, count: Int, after: Int) {
val textBeforeEdit = sequenceBeforeEdit.toString()
if (textBeforeEdit.isEmpty()) {
//in an empty string, cursor position is at 1 if a character is being added (after == 1)
//if a character is not being added, cursor position remains at the beginning
digitsBeforeCursor = if (after == 0) -1 else 1
return
}
digitsBeforeCursor = if (after == 0) {
//if characters are being removed
//count will always be 1 since we have disabled selection (in which case count will be equal to the number of characters selected)
val textBeforeNewCursor = textBeforeEdit.substring(0, startPos)
textBeforeNewCursor.count { it != thousandSeparator }
} else {
//if characters are being added
//after will always be 1 since we have disabled pasting (in which case after will be equal to the number of characters being pasted)
if (startPos == textBeforeEdit.length) {
//if adding a character to the end of the string
textBeforeEdit.count { it != thousandSeparator } + 1
} else {
//if adding a character in between the string
val textBeforeNewCursor = textBeforeEdit.substring(0, startPos + 1)
textBeforeNewCursor.count { it != thousandSeparator }
}
}
}
override fun onTextChanged(textAfterEdit: CharSequence, start: Int, before: Int, count: Int) {}
/***
* We will get the numeric value in the editText after stripping all the formatting
* We will then reformat this number to add the correct thousands separation and decimal marker according to the locale
* We then set the cursor to the correct position as we calculated in beforeTextChanged()
*/
override fun afterTextChanged(editable: Editable) {
val text = editable.toString()
//if the EditText is cleared, trigger callback with a null value to indicate an empty field
if (text.isEmpty()) {
digitsBeforeCursor = -1
callback.onChanged(null)
return
}
//get the double value of the entered number
val numberValue = getNumberFromFormattedCurrencyText(text)
//re-format the number to get the correct separation format and symbols
var newText = getCurrencyFormattedAmountValue(numberValue)
//If user was inputting decimal part of the number, reformatting will return a string without decimal point.
//So we need to add it back after the reformatting is complete
if (text.endsWith(decimalMarker)) {
newText += decimalMarker
} else if (text.endsWith(decimalMarker + "0")) {
newText += decimalMarker + "0"
}
//removing the listener to prevent infinite triggers
editText?.removeTextChangedListener(this)
//set the reformatted text
editText?.setText(newText)
//send the number typed to the callback
callback.onChanged(numberValue)
//set the cursor to the right position after reformatting the string
if (digitsBeforeCursor != -1) {
var numbersParsed = 0
for (i in newText.indices) {
if (newText[i] != thousandSeparator) {
numbersParsed++
}
if (numbersParsed == digitsBeforeCursor) {
editText?.setSelection(i + 1)
break
}
}
digitsBeforeCursor = -1
}
//add the listener back
editText?.addTextChangedListener(this)
}
/***
* Function to remove the listener and release reference to the EditText
*/
fun removeWatcherFromEditText() {
editText?.removeTextChangedListener(this)
editText = null
}
interface TextChangedCallback {
fun onChanged(newNumber: Double?)
}
companion object{
#JvmStatic
fun getNumberFromFormattedCurrencyText(formattedText: String?) = formattedText?.let {
val numberFormat = NumberFormat.getNumberInstance(Locale.getDefault())
try {
numberFormat.parse(it)?.toDouble()
} catch (exception: ParseException) {
0.0
}
} ?: 0.0
#JvmStatic
fun getCurrencyFormattedAmountValue(amount: Double?) = amount?.let {
val numberFormat = NumberFormat.getNumberInstance(Locale.getDefault())
numberFormat.maximumFractionDigits = 2
numberFormat.format(amount)
} ?: ""
}
}
I know it's late but maybe can help
fun generate_seprators(input: String?): String? {
var input = input
var result = ""
var float_section = ""
if (input == null) input = ""
var temp = input.trim { it <= ' ' }
temp = temp.replace(",", "")
var input_array = temp.split(".")
var decimal_section = input_array[0]
if(input_array.size>1)
float_section = input_array[1]
if (decimal_section.length > 3) {
var num = 0
for (i in decimal_section.length downTo 1) {
if (num == 3) {
num = 0
result = ",$result"
}
num++
result = decimal_section.substring(i - 1, i) + result
}
if(float_section!="")
result = "$result.$float_section"
} else {
result = decimal_section.replace(",", "")
if(float_section!="")
result = "$result.$float_section"
}
return result
}

Format credit card in edit text in android

How to make EditText accept input in format:
4digit 4digit 4digit 4digit
I tried Custom format edit text input android to accept credit card number, but unfortunately I was unable to delete the spaces. Whenever there is a space, I could not to delete it. Please help me in finding out the issue.
After finding multiple answers that are 'OK'. I moved towards a better TextWatcher which is designed to work correctly and independently from the TextView.
TextWatcher class is as follows:
/**
* Formats the watched EditText to a credit card number
*/
public static class FourDigitCardFormatWatcher implements TextWatcher {
// Change this to what you want... ' ', '-' etc..
private static final char space = ' ';
#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) {
// Remove spacing char
if (s.length() > 0 && (s.length() % 5) == 0) {
final char c = s.charAt(s.length() - 1);
if (space == c) {
s.delete(s.length() - 1, s.length());
}
}
// Insert char where needed.
if (s.length() > 0 && (s.length() % 5) == 0) {
char c = s.charAt(s.length() - 1);
// Only if its a digit where there should be a space we insert a space
if (Character.isDigit(c) && TextUtils.split(s.toString(), String.valueOf(space)).length <= 3) {
s.insert(s.length() - 1, String.valueOf(space));
}
}
}
}
Then add it to your TextView as you would any other TextWatcher.
{
//...
mEditTextCreditCard.addTextChangedListener(new FourDigitCardFormatWatcher());
}
This will auto delete the space sensibly going back so the user can actually do less keystrokes when editing.
Caveat
If you are using inputType="numberDigit" this will disable the '-' and ' ' chars, so I recommend using, inputType="phone". This enables other chars, but just use a custom inputfilter and problem solved.
Example on github.com
Late answer, but I guess it may helpful for somebody:
cardNumberEditText.addTextChangedListener(new TextWatcher() {
private static final int TOTAL_SYMBOLS = 19; // size of pattern 0000-0000-0000-0000
private static final int TOTAL_DIGITS = 16; // max numbers of digits in pattern: 0000 x 4
private static final int DIVIDER_MODULO = 5; // means divider position is every 5th symbol beginning with 1
private static final int DIVIDER_POSITION = DIVIDER_MODULO - 1; // means divider position is every 4th symbol beginning with 0
private static final char DIVIDER = '-';
#Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
// noop
}
#Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
// noop
}
#Override
public void afterTextChanged(Editable s) {
if (!isInputCorrect(s, TOTAL_SYMBOLS, DIVIDER_MODULO, DIVIDER)) {
s.replace(0, s.length(), buildCorrectString(getDigitArray(s, TOTAL_DIGITS), DIVIDER_POSITION, DIVIDER));
}
}
private boolean isInputCorrect(Editable s, int totalSymbols, int dividerModulo, char divider) {
boolean isCorrect = s.length() <= totalSymbols; // check size of entered string
for (int i = 0; i < s.length(); i++) { // check that every element is right
if (i > 0 && (i + 1) % dividerModulo == 0) {
isCorrect &= divider == s.charAt(i);
} else {
isCorrect &= Character.isDigit(s.charAt(i));
}
}
return isCorrect;
}
private String buildCorrectString(char[] digits, int dividerPosition, char divider) {
final StringBuilder formatted = new StringBuilder();
for (int i = 0; i < digits.length; i++) {
if (digits[i] != 0) {
formatted.append(digits[i]);
if ((i > 0) && (i < (digits.length - 1)) && (((i + 1) % dividerPosition) == 0)) {
formatted.append(divider);
}
}
}
return formatted.toString();
}
private char[] getDigitArray(final Editable s, final int size) {
char[] digits = new char[size];
int index = 0;
for (int i = 0; i < s.length() && index < size; i++) {
char current = s.charAt(i);
if (Character.isDigit(current)) {
digits[index] = current;
index++;
}
}
return digits;
}
});
this works perfectly with start-string/end-string/mid-string editing, also paste works perfectly.
I modified Chris Jenkins answer to make it more robust. With this, even if the user edits the middle of the text, the spacing characters are still inserted (and automatically removed on wrong places) correctly.
To make this work correctly, make sure the EditText attributes are set as follows (note the space on digits):
android:digits="01234 56789"
android:inputType="number"
android:maxLength="19"
Then here is the TextWatcher you need. The anonymous class can also be made static since this is independent of the EditText.
yourTextView.addTextChangedListener(new TextWatcher() {
private static final char space = ' ';
#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) {
// Remove all spacing char
int pos = 0;
while (true) {
if (pos >= s.length()) break;
if (space == s.charAt(pos) && (((pos + 1) % 5) != 0 || pos + 1 == s.length())) {
s.delete(pos, pos + 1);
} else {
pos++;
}
}
// Insert char where needed.
pos = 4;
while (true) {
if (pos >= s.length()) break;
final char c = s.charAt(pos);
// Only if its a digit where there should be a space we insert a space
if ("0123456789".indexOf(c) >= 0) {
s.insert(pos, "" + space);
}
pos += 5;
}
}
});
Here is a cleaner solution using regular expressions. Although regular expressions can be inefficient, they would be sufficient in this case since it's processing a string of at most 19 characters, even if the processing occurs after each key press.
editTxtCardNumber.addTextChangedListener(new TextWatcher() {
#Override
public void onTextChanged(CharSequence s, int arg1, int arg2,
int arg3) { }
#Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
#Override
public void afterTextChanged(Editable s) {
String initial = s.toString();
// remove all non-digits characters
String processed = initial.replaceAll("\\D", "");
// insert a space after all groups of 4 digits that are followed by another digit
processed = processed.replaceAll("(\\d{4})(?=\\d)", "$1 ");
// to avoid stackoverflow errors, check that the processed is different from what's already
// there before setting
if (!initial.equals(processed)) {
// set the value
s.replace(0, initial.length(), processed);
}
}
});
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'm adding my solution to the list. As far as I am aware, it has no drawback; you can edit in the middle, delete spacing characters, copy and paste into it etc.
To allow editing to take place anywhere in the string, and to maintain cursor position, the Editable is traversed and all whitespace (if any) are taken out one by one. New whitespace is then added at appropriate positions. This will ensure that the cursor moves along with the changes made to the contents.
import java.util.LinkedList;
import android.text.Editable;
import android.text.TextWatcher;
import android.widget.EditText;
/**
* Formats the watched EditText to groups of characters, with spaces between them.
*/
public class GroupedInputFormatWatcher implements TextWatcher {
private static final char SPACE_CHAR = ' ';
private static final String SPACE_STRING = String.valueOf(SPACE_CHAR);
private static final int GROUPSIZE = 4;
/**
* Breakdown of this regexp:
* ^ - Start of the string
* (\\d{4}\\s)* - A group of four digits, followed by a whitespace, e.g. "1234 ". Zero or more times.
* \\d{0,4} - Up to four (optional) digits.
* (?<!\\s)$ - End of the string, but NOT with a whitespace just before it.
*
* Example of matching strings:
* - "2304 52"
* - "2304"
* - ""
*/
private final String regexp = "^(\\d{4}\\s)*\\d{0,4}(?<!\\s)$";
private boolean isUpdating = false;
private final EditText editText;
public GroupedInputFormatWatcher(EditText editText) {
this.editText = editText;
}
#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) {
String originalString = s.toString();
// Check if we are already updating, to avoid infinite loop.
// Also check if the string is already in a valid format.
if (isUpdating || originalString.matches(regexp)) {
return;
}
// Set flag to indicate that we are updating the Editable.
isUpdating = true;
// First all whitespaces must be removed. Find the index of all whitespace.
LinkedList<Integer> spaceIndices = new LinkedList <Integer>();
for (int index = originalString.indexOf(SPACE_CHAR); index >= 0; index = originalString.indexOf(SPACE_CHAR, index + 1)) {
spaceIndices.offerLast(index);
}
// Delete the whitespace, starting from the end of the string and working towards the beginning.
Integer spaceIndex = null;
while (!spaceIndices.isEmpty()) {
spaceIndex = spaceIndices.removeLast();
s.delete(spaceIndex, spaceIndex + 1);
}
// Loop through the string again and add whitespaces in the correct positions
for(int i = 0; ((i + 1) * GROUPSIZE + i) < s.length(); i++) {
s.insert((i + 1) * GROUPSIZE + i, SPACE_STRING);
}
// Finally check that the cursor is not placed before a whitespace.
// This will happen if, for example, the user deleted the digit '5' in
// the string: "1234 567".
// If it is, move it back one step; otherwise it will be impossible to delete
// further numbers.
int cursorPos = editText.getSelectionStart();
if (cursorPos > 0 && s.charAt(cursorPos - 1) == SPACE_CHAR) {
editText.setSelection(cursorPos - 1);
}
isUpdating = false;
}
}
Not sure the TextWatcher is the right thing to use - we should use InputFilter
According to Android documentation, TextWatcher should be used for an external usage example :
one [EditView] for password input +
one [TextView] view which displays "weak", "strong", etc...
For Credit Card Format I am using InputFilter:
public class CreditCardInputFilter implements InputFilter {
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
if (dest != null & dest.toString().trim().length() > 24) return null;
if (source.length() == 1 && (dstart == 4 || dstart == 9 || dstart == 14))
return " " + new String(source.toString());
return null; // keep original
}
}
And combine with a length filter (Android SDK) :
mEditCardNumber.setFilters(new InputFilter[]{
new InputFilter.LengthFilter(24),
new CreditCardInputFilter(),
});
This handle the case when typing and removing a digit.
(!) But this does not handle the case for a copy/paste of an entire string,
this one should be done in a different InputFilter class
Hope it helps !
I just did the next implementation and works well for me, even with pasting and typing new text in any position of the EditText.
Gist file
/**
* Text watcher for giving "#### #### #### ####" format to edit text.
* Created by epool on 3/14/16.
*/
public class CreditCardFormattingTextWatcher implements TextWatcher {
private static final String EMPTY_STRING = "";
private static final String WHITE_SPACE = " ";
private String lastSource = EMPTY_STRING;
#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) {
String source = s.toString();
if (!lastSource.equals(source)) {
source = source.replace(WHITE_SPACE, EMPTY_STRING);
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < source.length(); i++) {
if (i > 0 && i % 4 == 0) {
stringBuilder.append(WHITE_SPACE);
}
stringBuilder.append(source.charAt(i));
}
lastSource = stringBuilder.toString();
s.replace(0, s.length(), lastSource);
}
}
}
Usage: editText.addTextChangedListener(new CreditCardFormattingTextWatcher());
This implementation ensures correct placement of spacing chars, even if the user edits mid-string. Other characters that show up on the soft keyboard (such as dash) are also supported; that is, the user can't enter them. One improvement that could be made: this implementation doesn't allow for the deletion of spacing characters mid-string.
public class CreditCardTextWatcher implements TextWatcher {
public static final char SPACING_CHAR = '-'; // Using a Unicode character seems to stuff the logic up.
#Override
public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) { }
#Override
public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { }
#Override
public void afterTextChanged(final Editable s) {
if (s.length() > 0) {
// Any changes we make to s in here will cause this method to be run again. Thus we only make changes where they need to be made,
// otherwise we'll be in an infinite loop.
// Delete any spacing characters that are out of place.
for (int i=s.length()-1; i>=0; --i) {
if (s.charAt(i) == SPACING_CHAR // There is a spacing char at this position ,
&& (i+1 == s.length() // And it's either the last digit in the string (bad),
|| (i+1) % 5 != 0)) { // Or the position is not meant to contain a spacing char?
s.delete(i,i+1);
}
}
// Insert any spacing characters that are missing.
for (int i=14; i>=4; i-=5) {
if (i < s.length() && s.charAt(i) != SPACING_CHAR) {
s.insert(i, String.valueOf(SPACING_CHAR));
}
}
}
}
}
Works well with an appropriate PasswordTransformationMethod implementation to mask CC digits.
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())
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
After searching a lot and not getting any satisfactory answer to meet my needs, I ended up writing my own function.
Here is an example to format entered credit card details based on the type of card being entered. Currently it takes care of Visa, MasterCard and American Express for the purpose of formatting.
editTxtCardNumber.addTextChangedListener(new TextWatcher() {
private boolean spaceDeleted;
#Override
public void onTextChanged(CharSequence s, int arg1, int arg2,
int arg3) {
}
#Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
CharSequence charDeleted = s.subSequence(start, start + count);
spaceDeleted = " ".equals(charDeleted.toString());
}
#Override
public void afterTextChanged(Editable editable) {
if(editTxtCardNumber.getText().length() > 0 && editTxtCardNumber.getText().charAt(0) == '3') {
editTxtCardNumber.setFilters(new InputFilter[] { new InputFilter.LengthFilter(Constants.MAX_LENGTH_CARD_NUMBER_AMEX) });
editTxtCardNumber.removeTextChangedListener(this);
int cursorPosition = editTxtCardNumber.getSelectionStart();
String withSpaces = formatTextAmEx(editable);
editTxtCardNumber.setText(withSpaces);
editTxtCardNumber.setSelection(cursorPosition + (withSpaces.length() - editable.length()));
if (spaceDeleted) {
editTxtCardNumber.setSelection(editTxtCardNumber.getSelectionStart() - 1);
spaceDeleted = false;
}
editTxtCardNumber.addTextChangedListener(this);
} else if(editTxtCardNumber.getText().length() > 0
&& (editTxtCardNumber.getText().charAt(0) == '4' || editTxtCardNumber.getText().charAt(0) == '5')) {
editTxtCardNumber.setFilters(new InputFilter[] { new InputFilter.LengthFilter(Constants.MAX_LENGTH_CARD_NUMBER_VISA_MASTERCARD) });
editTxtCardNumber.removeTextChangedListener(this);
int cursorPosition = editTxtCardNumber.getSelectionStart();
String withSpaces = formatTextVisaMasterCard(editable);
editTxtCardNumber.setText(withSpaces);
editTxtCardNumber.setSelection(cursorPosition + (withSpaces.length() - editable.length()));
if (spaceDeleted) {
editTxtCardNumber.setSelection(editTxtCardNumber.getSelectionStart() - 1);
spaceDeleted = false;
}
editTxtCardNumber.addTextChangedListener(this);
} else {
editTxtCardNumber.setFilters(new InputFilter[] { new InputFilter.LengthFilter(Constants.MAX_LENGTH_CARD_NUMBER_VISA_MASTERCARD) });
editTxtCardNumber.removeTextChangedListener(this);
int cursorPosition = editTxtCardNumber.getSelectionStart();
String withSpaces = formatTextVisaMasterCard(editable);
editTxtCardNumber.setText(withSpaces);
editTxtCardNumber.setSelection(cursorPosition + (withSpaces.length() - editable.length()));
if (spaceDeleted) {
editTxtCardNumber.setSelection(editTxtCardNumber.getSelectionStart() - 1);
spaceDeleted = false;
}
editTxtCardNumber.addTextChangedListener(this);
}
}
});
private String formatTextVisaMasterCard(CharSequence text)
{
StringBuilder formatted = new StringBuilder();
int count = 0;
for (int i = 0; i < text.length(); ++i)
{
if (Character.isDigit(text.charAt(i)))
{
if (count % 4 == 0 && count > 0)
formatted.append(" ");
formatted.append(text.charAt(i));
++count;
}
}
return formatted.toString();
}
private String formatTextAmEx(CharSequence text)
{
StringBuilder formatted = new StringBuilder();
int count = 0;
for (int i = 0; i < text.length(); ++i)
{
if (Character.isDigit(text.charAt(i)))
{
if (count > 0 && ((count == 4) || (count == 10))) {
formatted.append(" ");
}
formatted.append(text.charAt(i));
++count;
}
}
return formatted.toString();
}
Other than formatting spaces, I also applied checks to make sure that card number doesn't exceed their maximum limit and user gets notified that he has entered all the digits by performing a change in font when the maximum limit is reached. Here is the function to perform the above mentioned operation.
public void checkCardNoEnteredCorrectly() {
if(editTxtCardNumber.getText().length() > 0 && editTxtCardNumber.getText().charAt(0) == '3') {
if(editTxtCardNumber.getText().length() == Constants.MAX_LENGTH_CARD_NUMBER_AMEX) {
editTxtCardNumber.setCompoundDrawablesWithIntrinsicBounds(getResources().getDrawable(R.drawable.amex), null, null, null);
} else {
editTxtCardNumber.setCompoundDrawablesWithIntrinsicBounds(getResources().getDrawable(R.drawable.amex), null, null, null);
}
} else if(editTxtCardNumber.getText().length() > 0 && editTxtCardNumber.getText().charAt(0) == '4') {
if(editTxtCardNumber.getText().length() == Constants.MAX_LENGTH_CARD_NUMBER_VISA_MASTERCARD) {
editTxtCardNumber.setCompoundDrawablesWithIntrinsicBounds(getResources().getDrawable(R.drawable.visa), null, null, null);
} else {
editTxtCardNumber.setCompoundDrawablesWithIntrinsicBounds(getResources().getDrawable(R.drawable.visa), null, null, null);
}
} else if(editTxtCardNumber.getText().length() > 0 && editTxtCardNumber.getText().charAt(0) == '5') {
if(editTxtCardNumber.getText().length() == Constants.MAX_LENGTH_CARD_NUMBER_VISA_MASTERCARD) {
editTxtCardNumber.setCompoundDrawablesWithIntrinsicBounds(getResources().getDrawable(R.drawable.master_card), null, null, null);
} else {
editTxtCardNumber.setCompoundDrawablesWithIntrinsicBounds(getResources().getDrawable(R.drawable.master_card), null, null, null);
}
} else {
editTxtCardNumber.setCompoundDrawablesWithIntrinsicBounds(getResources().getDrawable(R.drawable.credit_card_number), null, null, null);
}
}
Note: The declarations made in Constants.java is as follows:
public static final int MAX_LENGTH_CARD_NUMBER_VISA_MASTERCARD = 19;
public static final int MAX_LENGTH_CARD_NUMBER_AMEX = 17;
Hope it helps!
You may have figured it out already, but here is what I did. The only method I had to override was AfterTextChanged.
Check if the form of the credit card is already valid, base case to prevent infinite recursion
If the form is not valid, remove all whitespace, and copy over into another string, inserting white space where appropriate.
Then simply replace the editable with your new string.
If you need code for a particular step, feel free to ask.
And Preethi, the reason you can't delete spaces is because you can't change text in the onTextChanged callback. From the developer site:
public abstract void onTextChanged (CharSequence s, int start, int before, int count)
Added in API level 1
This method is called to notify you that, within s, the count characters beginning at start have just replaced old text that had length before. It is an error to attempt to make changes to s from this callback.
int keyDel;
String a;
String a0;
int isAppent = 0;
final String ch = " ";
private void initListner() {
txtCreditNumber.addTextChangedListener(new TextWatcher() {
#Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
boolean flag = true;
if (s.length() > 19) {
txtCreditNumber.setText(a0);
txtCreditNumber.setSelection(txtCreditNumber.getText().length());
return;
}
String eachBlock[] = s.toString().split(ch);
for(int i = 0; i < eachBlock.length; i++) {
if (eachBlock[i].length() > 4) {
flag = false;
}
}
if (a0.length() > s.toString().length()) {
keyDel = 1;
}
if (flag) {
if (keyDel == 0) {
if (((txtCreditNumber.getText().length() + 1) % 5) == 0) {
if (s.toString().split(ch).length <= 3) {
isAppent = 1;
txtCreditNumber.setText(s + ch);
isAppent = 0;
txtCreditNumber.setSelection(txtCreditNumber.getText().length());
a = txtCreditNumber.getText().toString();
return;
}
}
if (isAppent == 0) {
String str = s.toString();
if (str.lastIndexOf(ch) == str.length() - 1) {
str = str.substring(0, str.lastIndexOf(ch));
keyDel = 1;
txtCreditNumber.setText(str);
keyDel = 0;
txtCreditNumber.setSelection(txtCreditNumber.getText().length());
a = txtCreditNumber.getText().toString();
return;
}
}
}
else {
String str = s.toString();
if (str.length() > 0 && str.lastIndexOf(ch) == str.length() - 1) {
str = str.substring(0, str.lastIndexOf(ch));
keyDel = 1;
txtCreditNumber.setText(str);
keyDel = 0;
txtCreditNumber.setSelection(txtCreditNumber.getText().length());
a = txtCreditNumber.getText().toString();
return;
}
else {
a = txtCreditNumber.getText().toString();
keyDel = 0;
}
}
}
else {
String str = s.toString();
str = str.substring(0, str.length() - 1) + ch + str.substring(str.length() - 1, str.length());
a = str;
txtCreditNumber.setText(a);
txtCreditNumber.setSelection(txtCreditNumber.getText().length());
}
}
#Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
// TODO Auto-generated method stub
a0 = s.toString();
}
#Override
public void afterTextChanged(Editable s) {
}
});
}
Here is an example that use all the function appropriately to make a decision.
The code might be a bit longer, but it will be faster as it mainly use the function given values (start, before, count ...).
This example add "-" every 4 digits, and delete them as well, when user use backspace.
as well, make sure the cursor will be at the end.
public class TextWatcherImplement implements TextWatcher {
private EditText creditCard;
private String beforeText, currentText;
private boolean noAction, addStroke, dontAddChar, deleteStroke;
public TextWatcherImplement(EditText creditCard) {
// TODO Auto-generated constructor stub
this.creditCard = creditCard;
noAction = false;
addStroke = false;
dontAddChar = false;
deleteStroke = false;
}
/* here I save the previous string if the max character had achieved */
#Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
// TODO Auto-generated method stub
Log.i("TextWatcherImplement", "beforeTextChanged start==" + String.valueOf(start) + " count==" + String.valueOf(count) + " after==" + String.valueOf(after));
if (start >= 19)
beforeText = s.toString();
}
/* here I check were we add a character, or delete one.
if we add character and it is time to add a stroke, then I flag it -> addStroke
if we delete a character and it time to delete a stroke, I flag it -> deleteStroke
if we are in max character for the credit card, don't add char -> dontAddChar
*/
#Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
// TODO Auto-generated method stub
Log.i("TextWatcherImplement", "onTextChanged start==" + String.valueOf(start) + " before==" + String.valueOf(before) + " count==" + String.valueOf(count) + " noAction ==" + String.valueOf(noAction));
if ( (before < count) && !noAction ) {
if ( (start == 3) || (start == 8) || (start == 13) ) {
currentText = s.toString();
addStroke = true;
} else if (start >= 19) {
currentText = s.toString();
dontAddChar = true;
}
} else {
if ( (start == 4) || (start == 9) || (start == 14) ) { //(start == 5) || (start == 10) || (start == 15)
currentText = s.toString();
deleteStroke = true;
}
}
}
/* noAction flag is when we change the text, the interface is being called again.
the NoAction flag will prevent any action, and prevent a ongoing loop */
#Override
public void afterTextChanged(Editable stext) {
// TODO Auto-generated method stub
if (addStroke) {
Log.i("TextWatcherImplement", "afterTextChanged String == " + stext + " beforeText == " + beforeText + " currentText == " + currentText);
noAction = true;
addStroke = false;
creditCard.setText(currentText + "-");
} else if (dontAddChar) {
dontAddChar = false;
noAction = true;
creditCard.setText(beforeText);
} else if (deleteStroke) {
deleteStroke = false;
noAction = true;
currentText = currentText.substring(0, currentText.length() - 1);
creditCard.setText(currentText);
} else {
noAction = false;
creditCard.setSelection(creditCard.getText().length()); // set cursor at the end of the line.
}
}
}
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;
}
}
}
Here's a simple and easily customizable solution using the TextWatcher class. It may be assigned to your EditText using the addTextChangedListener() method.
new TextWatcher() {
/** Formats the Field to display user-friendly separation of the input values. */
#Override public final void afterTextChanged(final Editable pEditable) {
// Declare the separator.
final char lSeparator = '-';
// Declare the length of separated text. i.e. (XXXX-XXXX-XXXX)
final int lSeparationSize = 4;
// Declare the count; tracks the number of allowed characters in a row.
int lCount = 0;
// Iterate the Characters.
for(int i = 0; i < pEditable.length(); i++) {
// Fetch the current character.
final char c = pEditable.charAt(i);
// Is it a usual character. Here, we permit alphanumerics only.
final boolean lIsExpected = (Character.isDigit(c) || Character.isLetter(c)) && (c != lSeparator);
// Is the character expected?
if(lIsExpected) {
// Increase the count.
lCount++;
}
else {
// Is it a separator?
if(c == lSeparator) {
// Reset the count.
lCount = 0;
// Continue the iteration.
continue;
}
}
// Has the count been exceeded? Is there more text coming?
if(lCount >= (lSeparationSize + 1) && (i < pEditable.length())) {
// Reset the count.
lCount = 0;
// Insert the separator.
pEditable.insert(i, Character.toString(lSeparator));
// Increase the iteration count.
i++;
}
}
}
/** Unused overrides. */
#Override public final void beforeTextChanged(final CharSequence pCharSequence, final int pStart, final int pCount, final int pAfter) { }
#Override public final void onTextChanged(final CharSequence pCharSequence, final int pStart, final int pBefore, final int pCount) { }
}
Alternatively, here is a much cleaner implementation based on epool's implementation.
public final class TextGroupFormattingListener implements TextWatcher {
/* Member Variables. */
private final int mGroupLength;
private final String mSeparator;
private String mSource;
/** Constructor. */
public TextGroupFormattingListener(final String pSeparator, final int pGroupLength) {
// Initialize Member Variables.
this.mSeparator = pSeparator;
this.mGroupLength = pGroupLength;
this.mSource = "";
}
/** Formats the Field to display user-friendly separation of the input values. */
#Override public final void afterTextChanged(final Editable pEditable) {
// Fetch the Source.
String lSource = pEditable.toString();
// Has the text changed?
if (!this.getSource().equals(lSource)) {
// Remove all of the existing Separators.
lSource = lSource.replace(this.getSeparator(), "");
// Allocate a StringBuilder.
StringBuilder lStringBuilder = new StringBuilder();
// Iterate across the Source String, which contains the raw user input.
for(int i = 0; i < lSource.length(); i++) {
// Have we exceeded the GroupLength?
if(i > 0 && i % this.getGroupLength() == 0) {
// Append the separator.
lStringBuilder.append(this.getSeparator());
}
// Append the user's character data.
lStringBuilder.append(lSource.charAt(i));
}
// Track changes to the Source.
this.setSource(lStringBuilder.toString());
// Replace the contents of the Editable with this new String.
pEditable.replace(0, pEditable.length(), this.getSource());
}
}
/** Unused overrides. */
#Override public final void beforeTextChanged(final CharSequence pCharSequence, final int pStart, final int pCount, final int pAfter) { }
#Override public final void onTextChanged(final CharSequence pCharSequence, final int pStart, final int pBefore, final int pCount) { }
public final int getGroupLength() {
return this.mGroupLength;
}
public final String getSeparator() {
return this.mSeparator;
}
private final void setSource(final String pSource) {
this.mSource = pSource;
}
private final String getSource() {
return this.mSource;
}
}
None of above answers is perfect for me. I created one that solves the start-string/end-string/mid-string issues. Copy & Paste should also work fine. This supports Mastercard, Visa and Amex. You can change the separator. If you don't need payment method type just remove it. It is Kotlin though. The idea is simple. Everytime when text changed I remove all separators and re-added them base on the format. The solves the issue start-string/mid-string issues. Then the only problem is that you need to work out the the right text position after separators added.
fun addCreditCardNumberTxtWatcher(et: EditText, separator: Char, paymentMethodType: PaymentMethodType): TextWatcher {
val tw = object : TextWatcher {
var mBlock = false
override fun afterTextChanged(s: Editable) {
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
Logger.d("_debug", "s: $s, start: $start, count: $count, after $after")
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
if (mBlock)
return
var lastPos = et.selectionStart
val oldStr = et.text.toString().replace(separator.toString(), "", false)
var newFormattedStr = ""
if (before > 0) {
if (lastPos > 0 && et.text.toString()[lastPos - 1] == separator) lastPos--
}
Logger.d("_debug", "lastPos: $lastPos, s: $s, start: $start, before: $before, count $count")
mBlock = true
oldStr.forEachIndexed { i, c ->
when (paymentMethodType) {
PaymentMethodType.MASTERCARD, PaymentMethodType.VISA -> {
if (i > 0 && i % 4 == 0) {
newFormattedStr += separator
}
}
PaymentMethodType.AMERICAN_EXPRESS -> {
if (i == 4 || i == 10 || i == 15) {
newFormattedStr += separator
}
}
}
newFormattedStr += c
}
et.setText(newFormattedStr)
if (before == 0) {
if (et.text.toString()[lastPos - 1] == separator) lastPos++
}
et.setSelection(lastPos)
mBlock = false
}
}
et.addTextChangedListener(tw)
return tw
}
This solution was implemented for IBAN's but the principle is the same, I tried to correct all the main problems in the answers above, if you find an error feel free to say it, thank you.
Set the EditText and restrict the characters that can be used:
private void setEditTextIBAN(View view) {
editTextIBAN = (EditText) view.findViewById(R.id.client_iban);
editTextIBAN.setKeyListener(
DigitsKeyListener.getInstance("ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 "));
editTextIBAN.addTextChangedListener(new IBANTextWatcher());
}
This is the TextWatcher:
private class IBANTextWatcher implements TextWatcher {
// means divider position is every 5th symbol
private static final int DIVIDER_MODULO = 5;
private static final int GROUP_SIZE = DIVIDER_MODULO - 1;
private static final char DIVIDER = ' ';
private static final String STRING_DIVIDER = " ";
private String previousText = "";
private int deleteLength;
private int insertLength;
private int start;
private String regexIBAN = "(\\w{" + GROUP_SIZE + "}" + DIVIDER +
")*\\w{1," + GROUP_SIZE + "}";
private Pattern patternIBAN = Pattern.compile(regexIBAN);
#Override
public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) {
this.previousText = s.toString();
this.deleteLength = count;
this.insertLength = after;
this.start = start;
}
#Override
public void onTextChanged(final CharSequence s, final int start, final int before, final int count) {
}
#Override
public void afterTextChanged(final Editable s) {
String originalString = s.toString();
if (!previousText.equals(originalString) &&
!isInputCorrect(originalString)) {
String newString = previousText.substring(0, start);
int cursor = start;
if (deleteLength > 0 && s.length() > 0 &&
(previousText.charAt(start) == DIVIDER ||
start == s.length())) {
newString = previousText.substring(0, start - 1);
--cursor;
}
if (insertLength > 0) {
newString += originalString.substring(start, start + insertLength);
newString = buildCorrectInput(newString);
cursor = newString.length();
}
newString += previousText.substring(start + deleteLength);
s.replace(0, s.length(), buildCorrectInput(newString));
editTextIBAN.setSelection(cursor);
}
}
/**
* Check if String has the white spaces in the correct positions, meaning
* if we have the String "123456789" and there should exist a white space
* every 4 characters then the correct String should be "1234 5678 9".
*
* #param s String to be evaluated
* #return true if string s is written correctly
*/
private boolean isInputCorrect(String s) {
Matcher matcherDot = patternIBAN.matcher(s);
return matcherDot.matches();
}
/**
* Puts the white spaces in the correct positions,
* see the example in {#link IBANTextWatcher#isInputCorrect(String)}
* to understand the correct positions.
*
* #param s String to be corrected.
* #return String corrected.
*/
private String buildCorrectInput(String s) {
StringBuilder sbs = new StringBuilder(
s.replaceAll(STRING_DIVIDER, ""));
// Insert the divider in the correct positions
for (int i = GROUP_SIZE; i < sbs.length(); i += DIVIDER_MODULO) {
sbs.insert(i, DIVIDER);
}
return sbs.toString();
}
}
If anyone still looking for answer,
Try the format-edit-text library for auto-formatting text in one line of code. This library uses dash(es) to define the format of the input.
editText.setFormat("any (dash) format");
How to use
add format-edit-text library dependency in app/build.gradle
implementation 'com.androidwidgets:formatedittext:0.2.0'
Add FormatEditText view in activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:focusableInTouchMode="true"
android:focusable="true">
<com.androidwidgets.formatedittext.widgets.FormatEditText
android:id="#+id/edit_text_1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:imeOptions="actionSend"
android:inputType="number"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Set credit-card format to FormatEditText view in MainActivity.java
public class MainActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final FormatEditText editText1 = findViewById(R.id.edit_text_1);
editText1.setFormat("---- ---- ---- ----");
}
}
This will produce the below output
PS: Make sure parameter inputType is added to the FormatEditText view in the layout file.
android:inputType="number"
I know this question is a bit old but I need an implemantation of this for IBAN's and not satisfied with the given answers. So I wrote some code for this. But it takes the "pattern" and "divider" as parameters so it can be use for credit card numbers too.
This is the extended text watcher class.
import android.text.Editable;
import android.text.TextWatcher;
import android.widget.EditText;
public class IbanTextWatcher implements TextWatcher {
private int[] pattern;
private String divider;
private String before;
private EditText field;
private boolean dividerDeleted;
public IbanTextWatcher(int[] pattern, String divider, EditText field) {
this.divider = divider;
this.pattern = pattern;
this.field = field;
}
#Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
before = charSequence.toString();
if (!String.valueOf(charSequence).equals("") && charSequence.length() > i) {
if (String.valueOf(before.charAt(i)).equals(getDivider())) {
dividerDeleted = true;
} else {
dividerDeleted = false;
}
}
}
#Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
#Override
public void afterTextChanged(Editable editable) {
String input = editable.toString().replaceAll("\\s", "");
StringBuilder output = new StringBuilder();
boolean error = false;
int currentIndex = 0;
int cursorPosition = getField().getSelectionStart();
int lengthBefore;
int currentPatternMember = 0;
//prevent user to delete the divider
if (dividerDeleted && cursorPosition != getField().getText().length()) {
getField().setText(getBefore());
getField().setSelection(cursorPosition + 1);
return;
} else if (input.equals(getBefore().replaceAll("\\s", ""))) {
return;
}
for (int i = 0; i < getPattern().length; i++) {
error = false;
currentPatternMember = getPattern()[i];
try {
output.append(input.substring(currentIndex, currentIndex + currentPatternMember));
} catch (StringIndexOutOfBoundsException e) {
error = true;
}
if (!error) {
if (i != getPattern().length - 1) {
output.append(getDivider());
}
currentIndex += currentPatternMember;
} else {
break;
}
}
if (error) {
output.append(input.substring(currentIndex, input.length()));
}
cursorPosition = getField().getSelectionStart();
lengthBefore = getBefore().length();
getField().setText(output.toString());
if (cursorPosition != lengthBefore && cursorPosition != lengthBefore + 1) {
getField().setSelection(cursorPosition);
} else {
getField().setSelection(getField().getText().length());
}
}
public int[] getPattern() {
return pattern;
}
public String getDivider() {
return divider;
}
public String getBefore() {
return before;
}
public EditText getField() {
return field;
}
}
And this is how I use it:
int[] pattern = {2,4,4,4,4,4,2}; //
iban.addTextChangedListener(new IbanTextWatcher(pattern, " ", iban)); //here iban is my edittext field
By the way, I set the max length of the field in xml.
In your layout:
<android.support.design.widget.TextInputEditText
android:id="#+id/et_credit_card_number"
android:digits=" 1234567890"
android:inputType="number"
android:maxLength="19"/>
Here the TextWachter which sets a space on every 4 digits in a 16 number credit card.
class CreditCardFormatWatcher : TextWatcherAdapter() {
override fun afterTextChanged(s: Editable?) {
if (s == null || s.isEmpty()) return
s.forEachIndexed { index, c ->
val spaceIndex = index == 4 || index == 9 || index == 14
when {
!spaceIndex && !c.isDigit() -> s.delete(index, index + 1)
spaceIndex && !c.isWhitespace() -> s.insert(index, " ")
}
}
if (s.last().isWhitespace())
s.delete(s.length - 1, s.length)
}
}
private class TextWatcherIBAN implements TextWatcher {
#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) {
textInputEditText.removeTextChangedListener(this);
formatIBANEditText(textInputEditText);
textInputEditText.addTextChangedListener(this);
}
}
public void formatIBANEditText(TextInputEditText editText) {
String decimalAmount = editText.getText().toString();
int selection = editText.getSelectionEnd() == decimalAmount.length() ? -1 : editText.getSelectionEnd();
decimalAmount = formatIBAN(decimalAmount);
editText.setText(decimalAmount);
if (selection != -1) {
editText.setSelection(selection);
} else {
editText.setSelection(decimalAmount.length());
}
}
public String formatIBAN(String text) {
return formatterIBAN(new StringBuilder(text));
}
private String formatterIBAN(StringBuilder text) {
int group = text.toString().length() / 5;
int spaceCount = getSpaceCount(text);
if (spaceCount < group) {
return formatterIBAN(text.insert(4 + 5 * spaceCount, space));
} else {
return text.toString();
}
}
private int getSpaceCount(StringBuilder text) {
int spaceCount = 0;
for (int index = 0; index < text.length(); index++) {
if (text.charAt(index) == space.charAt(0)) {
spaceCount++;
}
}
return spaceCount;
}
textInputEditText.addTextChangedListener(new TextWatcherIBAN());
class XYZ : TextWatcher {
private val formatSymbols = DecimalFormatSymbols(Locale.getDefault())
private lateinit var formatter: DecimalFormat
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
.
.
formatSymbols.groupingSeparator = ' '
formatter = DecimalFormat("####,####", formatSymbols)
.
.
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
editText.addTextChangedListener(this)
}
override fun afterTextChanged(s: Editable?) {
if (editText.error != null) {
editText.error = null
}
editText.removeTextChangedListener(this)
try {
var originalString = s.toString()
if (originalString.contains(" ")) {
originalString = originalString.replace(" ", "", true)
}
val longVal: Long? = originalString.toLong()
val formattedString = formatter.format(longVal)
editText.setText(formattedString)
editText.setSelection(editText.text.length)
} catch (error: NumberFormatException) {
// Print Error Or Do Whatever you want.
}
editText.addTextChangedListener(this)
}
}
This is my implementation base on Igor Tyulkanov's idea, it has a small improvement that fix the cursor position problem
class CardNumbersInputWatcher(private val editText: EditText) : TextWatcher {
companion object {
private const val TOTAL_SYMBOLS = 19
private const val DIVIDER_DISTANCE = 4
private const val DIVIDER = ' '
}
override fun afterTextChanged(s: Editable) {
if (!isInputCorrect(s, TOTAL_SYMBOLS, DIVIDER_DISTANCE, DIVIDER)) {
val beforeCurPos = editText.selectionStart
val beforeLength = s.length
s.replace(0, s.length, buildCorrectString(s, TOTAL_SYMBOLS, DIVIDER_DISTANCE, DIVIDER))
if (beforeLength > TOTAL_SYMBOLS && beforeCurPos <= s.length && editText.selectionStart < beforeCurPos) {
editText.setSelection(beforeCurPos)
}
}
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
}
private fun isInputCorrect(s: Editable, totalSymbols: Int, dividerDistance: Int, divider: Char): Boolean {
if (s.length > totalSymbols) {
return false
}
return s.withIndex().all { (index, c) ->
if (index != 0 && ((index + 1) % (dividerDistance + 1) == 0)) {
// it should be divider
c == divider
} else {
c.isDigit()
}
}
}
private fun buildCorrectString(s: Editable, totalSymbols: Int, dividerDistance: Int, divider: Char): String {
return buildString {
for (c in s) {
if (length >= totalSymbols) break
if (!c.isDigit()) continue
if (length > 0 && ((length + 1) % (dividerDistance + 1)) == 0) append(divider)
append(c)
}
}
}
1. Copy and paste this class
class EditTextForCards #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = androidx.appcompat.R.attr.editTextStyle
) : AppCompatEditText(context, attrs, defStyleAttr) {
private var mCCPatterns = SparseArray<Pattern>()
private var mSeparator: Separator = Separator.NONE
private var mDrawableGravity: Gravity? = null/*Gravity.END*/
private var isValidCard: Boolean = false
private var mCurrentDrawableResId = Card.UNKNOWN.drawableRes
val textWithoutSeparator
get() = if (mSeparator == Separator.NONE) {
text.toString()
} else {
text.toString().replace(mSeparator.toRegex(), "")
}
val isCardValid: Boolean
get() = textWithoutSeparator.length > 12 && isValidCard
val cardType: Card
get() = Card.from(mCurrentDrawableResId)
enum class Separator(private val stringValue: String) {
NONE(""), SPACES(" "), DASHES("-");
override fun toString() = stringValue
internal fun toRegex() = stringValue.toRegex()
internal val length
get() = stringValue.length
}
enum class Gravity {
START, END, LEFT, RIGHT
}
enum class Card(internal val value: Int, #field:DrawableRes internal val drawableRes: Int) {
VISA(1, R.drawable.ic_visa),
MASTERCARD(2, R.drawable.ic_mastercard),
AMEX(4, R.drawable.amex),
DISCOVER(8, R.drawable.discover),
UNKNOWN(-1, R.drawable.ic_visa);
companion object {
internal fun from(#DrawableRes drawableRes: Int): Card {
for (card in values()) {
if (card.drawableRes == drawableRes) {
return card
}
}
return UNKNOWN
}
}
}
private val textWatcher = object : TextWatcher {
override fun afterTextChanged(s: Editable?) {}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(
text: CharSequence,
start: Int,
lengthBefore: Int,
lengthAfter: Int
) {
val textWithoutSeparator = textWithoutSeparator
var mDrawableResId = 0
for (i in 0 until mCCPatterns.size()) {
val key = mCCPatterns.keyAt(i)
val p = mCCPatterns.get(key)
val m = p.matcher(textWithoutSeparator)
isValidCard = m.find()
if (isValidCard) {
mDrawableResId = key
break
}
}
// if (mDrawableResId != 0 && mDrawableResId != mCurrentDrawableResId) {
// mCurrentDrawableResId = mDrawableResId
// } else if (mDrawableResId == 0) {
// mCurrentDrawableResId = Card.UNKNOWN.drawableRes
// }
// addDrawable()
addSeparators()
}
}
init {
setDisabledCards()
inputType = InputType.TYPE_CLASS_PHONE
setSeparator(Separator.NONE)
// setDrawableGravity(Gravity.END)
attrs?.let { applyAttributes(it) }
addTextChangedListener(textWatcher)
}
private fun applyAttributes(attrs: AttributeSet) {
val a = context.theme.obtainStyledAttributes(
attrs,
R.styleable.EditTextForCards,
0, 0
)
try {
setSeparator(
Separator.values()[a.getInt(
R.styleable.EditTextForCards_separator,
Separator.NONE.ordinal
)]
)
setDisabledCardsInternal(a.getInt(R.styleable.EditTextForCards_disabledCards, 0))
setDrawableGravity(
Gravity.values()[a.getInt(
R.styleable.EditTextForCards_drawableGravity,
Gravity.END.ordinal
)]
)
} finally {
a.recycle()
}
}
private fun addDrawable() {
var currentDrawable = ContextCompat.getDrawable(context, mCurrentDrawableResId)
if (currentDrawable != null && error.isNullOrEmpty()) {
currentDrawable = resize(currentDrawable)
when (mDrawableGravity) {
Gravity.START -> setDrawablesRelative(start = currentDrawable)
Gravity.RIGHT -> setDrawables(right = currentDrawable)
Gravity.LEFT -> setDrawables(left = currentDrawable)
else -> setDrawablesRelative(end = currentDrawable)
}
}
}
private fun addSeparators() {
val text = text.toString()
if (mSeparator != Separator.NONE) {
if (text.length > 4 && !text.matches("(?:[0-9]{4}$mSeparator)+[0-9]{1,4}".toRegex())) {
val sp = StringBuilder()
val caretPosition = selectionEnd
val segments = splitString(text.replace(mSeparator.toRegex(), ""))
for (segment in segments) {
sp.append(segment).append(mSeparator)
}
setText("")
append(sp.delete(sp.length - mSeparator.length, sp.length).toString())
if (caretPosition < text.length)
setSelection(caretPosition)
}
}
}
private fun removeSeparators() {
var text = text.toString()
text = text.replace(" ".toRegex(), "").replace("-".toRegex(), "")
setText("")
append(text)
}
private fun splitString(s: String): Array<String?> {
val arrayLength = ceil(s.length / 4.toDouble()).toInt()
val result = arrayOfNulls<String>(arrayLength)
var j = 0
val lastIndex = result.size - 1
for (i in 0 until lastIndex) {
result[i] = s.substring(j, j + 4)
j += 4
}
result[lastIndex] = s.substring(j)
return result
}
/*#Deprecated("Please use the method that accepts a Separator enum instead.", ReplaceWith("this.setSeparator(Separator.)"))
fun setSeparator(#IntRange(from = 0, to = 2) separator: Int) {
require(!(separator > 2 || separator < 0)) {
"The separator has to be one of the following:" +
"NO_SEPARATOR." +
"SPACES_SEPARATOR." +
"DASHES_SEPARATOR."
}
setSeparator(Separator.values()[separator])
}*/
/**
* Use this method to set the separator style.
* The default separator is [Separator.NONE].
*
* #param separator the style of the separator.
*/
fun setSeparator(separator: Separator) {
mSeparator = separator
if (mSeparator != Separator.NONE) {
filters = arrayOf<InputFilter>(InputFilter.LengthFilter(23))
keyListener = DigitsKeyListener.getInstance("0123456789$mSeparator")
addSeparators()
} else {
filters = arrayOf<InputFilter>(InputFilter.LengthFilter(19))
keyListener = DigitsKeyListener.getInstance("0123456789")
removeSeparators()
}
}
/**
* Use this method to set the location of the card drawable.
* The default gravity is [Gravity.END].
*
* #param gravity the drawable location.
*/
fun setDrawableGravity(gravity: Gravity) {
mDrawableGravity = gravity
addDrawable()
}
private fun setDisabledCardsInternal(disabledCards: Int) {
val cards = ArrayList<Card>()
if (containsFlag(disabledCards, Card.VISA.value)) {
cards.add(Card.VISA)
}
if (containsFlag(disabledCards, Card.MASTERCARD.value)) {
cards.add(Card.MASTERCARD)
}
/*if (containsFlag(disabledCards, Card.AMEX.value)) {
cards.add(Card.AMEX)
}
if (containsFlag(disabledCards, Card.DISCOVER.value)) {
cards.add(Card.DISCOVER)
}*/
setDisabledCards(*cards.toTypedArray())
}
#Deprecated(
"Please use the method that accepts an array of Cards instead.",
ReplaceWith("this.setDisabledCards(cards)")
)
fun setDisabledCards(disabledCards: Int) {
setDisabledCardsInternal(disabledCards)
}
/**
* Use this method to set which cards are disabled.
* By default all supported cards are enabled.
*
* #param cards the cards to be disabled.
*/
fun setDisabledCards(vararg cards: Card) {
var disabledCards = 0
for (card in cards) {
disabledCards = disabledCards or card.value
}
mCCPatterns.clear()
if (!containsFlag(disabledCards, Card.VISA.value)) {
mCCPatterns.put(Card.VISA.drawableRes, Pattern.compile("^4[0-9]{1,12}(?:[0-9]{6})?$"))
}
if (!containsFlag(disabledCards, Card.MASTERCARD.value)) {
mCCPatterns.put(Card.MASTERCARD.drawableRes, Pattern.compile("^5[1-5][0-9]{0,14}$"))
}
/*if (!containsFlag(disabledCards, Card.AMEX.value)) {
mCCPatterns.put(Card.AMEX.drawableRes, Pattern.compile("^3[47][0-9]{0,13}$"))
}
if (!containsFlag(disabledCards, Card.DISCOVER.value)) {
mCCPatterns.put(Card.DISCOVER.drawableRes, Pattern.compile("^6(?:011|5[0-9]{1,2})[0-9]{0,12}$"))
}*/
textWatcher.onTextChanged("", 0, 0, 0)
}
private fun containsFlag(flagSet: Int, flag: Int): Boolean {
return flagSet or flag == flagSet
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
var noDrawablesVisible = true
for (drawable in compoundDrawables) {
if (drawable != null) {
noDrawablesVisible = false
break
}
}
if (noDrawablesVisible) {
addDrawable()
}
}
private fun resize(image: Drawable) =
when (val height = measuredHeight - (paddingTop + paddingBottom)) {
in 1 until image.intrinsicHeight -> {
val bitmap = (image as BitmapDrawable).bitmap
val ratio = image.getIntrinsicWidth().toFloat() / image.intrinsicHeight.toFloat()
val resizedBitmap =
Bitmap.createScaledBitmap(bitmap, (height * ratio).toInt(), height, false)
resizedBitmap.density = Bitmap.DENSITY_NONE
BitmapDrawable(resources, resizedBitmap)
}
in Int.MIN_VALUE..0 -> null
else -> image
}
private fun setDrawablesRelative(
start: Drawable? = null,
top: Drawable? = null,
end: Drawable? = null,
bottom: Drawable? = null
) =
/*TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(this, start, top, end, bottom)*/
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(this, null, null, null, null)
private fun setDrawables(
left: Drawable? = null,
top: Drawable? = null,
right: Drawable? = null,
bottom: Drawable? = null
) =
/*setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom)*/
setCompoundDrawablesWithIntrinsicBounds(null, null, null, null)
companion object {
#Deprecated("This constant has been replace with an enum.", ReplaceWith("Separator.NONE"))
const val NO_SEPARATOR = 0
#Deprecated("This constant has been replace with an enum.", ReplaceWith("Separator.SPACES"))
const val SPACES_SEPARATOR = 1
#Deprecated("This constant has been replace with an enum.", ReplaceWith("Separator.DASHES"))
const val DASHES_SEPARATOR = 2
#Deprecated("This constant has been replace with an enum.", ReplaceWith("null"))
const val NONE = 0
#Deprecated("This constant has been replace with an enum.", ReplaceWith("Card.VISA"))
const val VISA = 1
#Deprecated("This constant has been replace with an enum.", ReplaceWith("Card.MASTERCARD"))
const val MASTERCARD = 2
#Deprecated("This constant has been replace with an enum.", ReplaceWith("Card.AMEX"))
const val AMEX = 4
#Deprecated("This constant has been replace with an enum.", ReplaceWith("Card.DISCOVER"))
const val DISCOVER = 8
}
}
2. paste this style
<declare-styleable name="EditTextForCards">
<attr name="separator" format="enum">
<enum name="no_separator" value="0" />
<enum name="spaces" value="1" />
<enum name="dashes" value="2" />
</attr>
<attr name="disabledCards">
<flag name="none" value="0" />
<flag name="visa" value="1" />
<flag name="mastercard" value="2" />
<flag name="amex" value="4" />
<flag name="discover" value="8" />
</attr>
<attr name="drawableGravity">
<enum name="start" value="0" />
<enum name="end" value="1" />
<enum name="left" value="2" />
<enum name="right" value="3" />
</attr>
</declare-styleable>
3. In your layout file, use it by
<EditTextForCards
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="#dimen/dp_5"
android:digits="0123456789 "
android:hint="#string/card_number"
android:padding="#dimen/dp_20"
android:textColor="#android:color/white"
android:textColorHint="#android:color/white"
android:textSize="#dimen/sp_16"
app:separator="spaces" />
benefits:
It works when you insert numbers one by one
It works when you paste all 16 character
It handles back press
It removes other characters automatically
in XML
android:digits="0123456789-"
android:maxLength="19"
android:inputType="number"
and the TextWatcher
class FourDigitCardFormatWatcher : TextWatcher {
private var current = ""
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
}
override fun afterTextChanged(s: Editable) {
if (s.toString() != current) {
val userInput = s.toString().replace(nonDigits,"")
if (userInput.length <= 16) {
current = userInput.chunked(4).joinToString("-")
s.filters = arrayOfNulls<InputFilter>(0)
}
s.replace(0, s.length, current, 0, current.length)
}
}
companion object {
private val nonDigits = Regex("[^\\d]")
}
}
import android.text.Editable;
import android.text.TextWatcher;
import android.widget.EditText;`
public class CreditCard implements TextWatcher
{
EditText editText;
public CreditCard(EditText editText)
{
this.editText = editText;
}
#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) {
try
{
editText.removeTextChangedListener(this);
String str = editText.getText().toString().replaceAll("-", "");
editText.setText(setDash(str));
editText.setSelection(editText.getText().toString().length());
editText.addTextChangedListener(this);
return;
}
catch (Exception ex)
{
ex.printStackTrace();
editText.addTextChangedListener(this);
}
}
public static String setDash(String value)
{
String str = "";
int j = 0;
for (int i = 0;i<value.length(); i++)
{
j++;
if (j == 5)
{
str = str+"-";
j = 1;
}
str = str + value.charAt(i);
}
return str;
}
public static String trimDashOfString(String string)
{
if (string.contains("-")) {
return string.replace("-", "");
} else {
return string;
}
}
}

Categories

Resources