This is similar, but a somewhat different problem than the question I asked here: When my InputFilter source comes in as spannable, the source does not remove the characters I filter out
In my example, I'm making an input filter to validate Canadian zip codes.
I have an abstract here:
abstract class MyInputFilter : InputFilter {
protected abstract fun String.isValid(): Boolean
private fun getFinalResultOfChange(source: CharSequence, start: Int, end: Int, dest: Spanned, dstart: Int, dend: Int): CharSequence {
return dest.replaceRange(dstart, dend, source.subSequence(start, end))
}
private fun getNoChangeResult(source: CharSequence, start: Int, end: Int, dest: Spanned, dstart: Int, dend: Int): CharSequence {
val initialSubSequence = dest.subSequence(dstart, dend)
return try {
if (source is Spanned) {
val spannable = SpannableString(initialSubSequence)
TextUtils.copySpansFrom(source, start, end, null, spannable, 0)
spannable
} else {
initialSubSequence
}
} catch (e: Exception) {
initialSubSequence
}
}
override fun filter(source: CharSequence?, start: Int, end: Int, dest: Spanned?, dstart: Int, dend: Int): CharSequence? {
if (source == null || dest == null) {
return null
}
val input = getFinalResultOfChange(source, start, end, dest, dstart, dend).toString()
return if (input.isValid()) {
null // Allow the edit to proceed unchanged.
} else {
getNoChangeResult(source, start, end, dest, dstart, dend)
}
}
}
which I inherit and override isValid for all of my input filters. This filter is... weird, but came about based on the question linked above, and somewhat solved the problem I had.
This is naive due to my intent to be deliberate, but here is the CAN filter:
private val patterns = listOf(
"^(?!.*[DFIOQUdfioqu])[A-VXYa-vxy]$",
"^(?!.*[DFIOQUdfioqu])[A-VXYa-vxy][0-9]$",
"^(?!.*[DFIOQUdfioqu])[A-VXYa-vxy][0-9][A-Za-z]$",
"^(?!.*[DFIOQUdfioqu])[A-VXYa-vxy][0-9][A-Za-z][0-9]$",
"^(?!.*[DFIOQUdfioqu])[A-VXYa-vxy][0-9][A-Za-z][0-9][A-Za-z]$",
"^(?!.*[DFIOQUdfioqu])[A-VXYa-vxy][0-9][A-Za-z][0-9][A-Za-z][0-9]$"
)
override fun String.isValid(): Boolean {
return when (length) {
0 -> true
1, 2, 3, 4, 5, 6 -> this.matches(Regex(patterns[length - 1]))
else -> false
}
}
Now, I have a new problem. If the very first character entered is an invalid letter, the keyboard keeps loading the characters into a 'word' and trying to resubmit them. So, if I type
D
nothing gets entered, correctly. But, if I type
DA
still nothing is entered, and my phone is trying to 'auto suggest' the word "da". Unless I either click the suggested word or backspace to empty, the field WILL not accept any input of any kind, but the keyboard will continue to 'queue' a word with all of the letters I've typed. If I input a bad character in ANY OTHER position, as long as the first character has been accepted, it works. The phone does not start 'queueing' a word into the keyboard, and I can immediately enter the correct character.
Also of importance: Numbers do not cause the problem. The keyboard does not try to start 'building' a word from a number, so if I type
1A
then "A" is entered into the field with no problems.
I can't find nearly anyone else having these problems with input filters and spannable vs not spannable CharSequence input, but the keyboard is building a word and trying to submit the whole thing in a way that I don't understand. I've tried it on the GBoard on a OnePlus 6T, as well as the default samsung keyboard of a Samsung Tab E, so it doesn't feel like an individual keyboard bug.
The inputType numberDecimal in EditText uses the dot . as decimal separator. In Europe it's common to use a comma , instead. Even though my locale is set as german the decimal separator is still the .
Is there a way to get the comma as decimal separator?
A workaround (until Google fix this bug) is to use an EditText with android:inputType="numberDecimal" and android:digits="0123456789.,".
Then add a TextChangedListener to the EditText with the following afterTextChanged:
public void afterTextChanged(Editable s) {
double doubleValue = 0;
if (s != null) {
try {
doubleValue = Double.parseDouble(s.toString().replace(',', '.'));
} catch (NumberFormatException e) {
//Error
}
}
//Do something with doubleValue
}
A variation on the 'digit' solutions offered here:
char separator = DecimalFormatSymbols.getInstance().getDecimalSeparator();
input.setKeyListener(DigitsKeyListener.getInstance("0123456789" + separator));
Taking into account the locale separator.
Following Code Currency Mask for EditText ($ 123,125.155)
Xml Layout
<EditText
android:inputType="numberDecimal"
android:layout_height="wrap_content"
android:layout_width="200dp"
android:digits="0123456789.,$" />
Code
EditText testFilter=...
testFilter.addTextChangedListener( new TextWatcher() {
boolean isEdiging;
#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) {
if(isEdiging) return;
isEdiging = true;
String str = s.toString().replaceAll( "[^\\d]", "" );
double s1 = Double.parseDouble(str);
NumberFormat nf2 = NumberFormat.getInstance(Locale.ENGLISH);
((DecimalFormat)nf2).applyPattern("$ ###,###.###");
s.replace(0, s.length(), nf2.format(s1));
isEdiging = false;
}
});
This is a known bug in the Android SDK.
The only workaround is to create your own soft keyboard. You can find an example of implementation here.
you could use the following for different locales
private void localeDecimalInput(final EditText editText){
DecimalFormat decFormat = (DecimalFormat) DecimalFormat.getInstance(Locale.getDefault());
DecimalFormatSymbols symbols=decFormat.getDecimalFormatSymbols();
final String defaultSeperator=Character.toString(symbols.getDecimalSeparator());
editText.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) {
}
#Override
public void afterTextChanged(Editable editable) {
if(editable.toString().contains(defaultSeperator))
editText.setKeyListener(DigitsKeyListener.getInstance("0123456789"));
else
editText.setKeyListener(DigitsKeyListener.getInstance("0123456789" + defaultSeperator));
}
});
}
You can use the following workaround to also include comma as a valid input:-
Through XML:
<EditText
android:inputType="number"
android:digits="0123456789.," />
Programmatically:
EditText input = new EditText(THE_CONTEXT);
input.setKeyListener(DigitsKeyListener.getInstance("0123456789.,"));
In this way Android system will show the numbers' keyboard and allow the input of comma. Hope this answers the question :)
Martins answer won't work if you are instantiating the EditText programmatically. I went ahead and modified the included DigitsKeyListener class from API 14 to allow for both comma and period as decimal separator.
To use this, call setKeyListener() on the EditText, e.g.
// Don't allow for signed input (minus), but allow for decimal points
editText.setKeyListener( new MyDigitsKeyListener( false, true ) );
However, you still have to use Martin's trick in the TextChangedListener where you replace commas with periods
import android.text.InputType;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.method.NumberKeyListener;
import android.view.KeyEvent;
class MyDigitsKeyListener extends NumberKeyListener {
/**
* The characters that are used.
*
* #see KeyEvent#getMatch
* #see #getAcceptedChars
*/
private static final char[][] CHARACTERS = new char[][] {
new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' },
new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-' },
new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', ',' },
new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '.', ',' },
};
private char[] mAccepted;
private boolean mSign;
private boolean mDecimal;
private static final int SIGN = 1;
private static final int DECIMAL = 2;
private static MyDigitsKeyListener[] sInstance = new MyDigitsKeyListener[4];
#Override
protected char[] getAcceptedChars() {
return mAccepted;
}
/**
* Allocates a DigitsKeyListener that accepts the digits 0 through 9.
*/
public MyDigitsKeyListener() {
this(false, false);
}
/**
* Allocates a DigitsKeyListener that accepts the digits 0 through 9,
* plus the minus sign (only at the beginning) and/or decimal point
* (only one per field) if specified.
*/
public MyDigitsKeyListener(boolean sign, boolean decimal) {
mSign = sign;
mDecimal = decimal;
int kind = (sign ? SIGN : 0) | (decimal ? DECIMAL : 0);
mAccepted = CHARACTERS[kind];
}
/**
* Returns a DigitsKeyListener that accepts the digits 0 through 9.
*/
public static MyDigitsKeyListener getInstance() {
return getInstance(false, false);
}
/**
* Returns a DigitsKeyListener that accepts the digits 0 through 9,
* plus the minus sign (only at the beginning) and/or decimal point
* (only one per field) if specified.
*/
public static MyDigitsKeyListener getInstance(boolean sign, boolean decimal) {
int kind = (sign ? SIGN : 0) | (decimal ? DECIMAL : 0);
if (sInstance[kind] != null)
return sInstance[kind];
sInstance[kind] = new MyDigitsKeyListener(sign, decimal);
return sInstance[kind];
}
/**
* Returns a DigitsKeyListener that accepts only the characters
* that appear in the specified String. Note that not all characters
* may be available on every keyboard.
*/
public static MyDigitsKeyListener getInstance(String accepted) {
// TODO: do we need a cache of these to avoid allocating?
MyDigitsKeyListener dim = new MyDigitsKeyListener();
dim.mAccepted = new char[accepted.length()];
accepted.getChars(0, accepted.length(), dim.mAccepted, 0);
return dim;
}
public int getInputType() {
int contentType = InputType.TYPE_CLASS_NUMBER;
if (mSign) {
contentType |= InputType.TYPE_NUMBER_FLAG_SIGNED;
}
if (mDecimal) {
contentType |= InputType.TYPE_NUMBER_FLAG_DECIMAL;
}
return contentType;
}
#Override
public CharSequence filter(CharSequence source, int start, int end,
Spanned dest, int dstart, int dend) {
CharSequence out = super.filter(source, start, end, dest, dstart, dend);
if (mSign == false && mDecimal == false) {
return out;
}
if (out != null) {
source = out;
start = 0;
end = out.length();
}
int sign = -1;
int decimal = -1;
int dlen = dest.length();
/*
* Find out if the existing text has '-' or '.' characters.
*/
for (int i = 0; i < dstart; i++) {
char c = dest.charAt(i);
if (c == '-') {
sign = i;
} else if (c == '.' || c == ',') {
decimal = i;
}
}
for (int i = dend; i < dlen; i++) {
char c = dest.charAt(i);
if (c == '-') {
return ""; // Nothing can be inserted in front of a '-'.
} else if (c == '.' || c == ',') {
decimal = i;
}
}
/*
* If it does, we must strip them out from the source.
* In addition, '-' must be the very first character,
* and nothing can be inserted before an existing '-'.
* Go in reverse order so the offsets are stable.
*/
SpannableStringBuilder stripped = null;
for (int i = end - 1; i >= start; i--) {
char c = source.charAt(i);
boolean strip = false;
if (c == '-') {
if (i != start || dstart != 0) {
strip = true;
} else if (sign >= 0) {
strip = true;
} else {
sign = i;
}
} else if (c == '.' || c == ',') {
if (decimal >= 0) {
strip = true;
} else {
decimal = i;
}
}
if (strip) {
if (end == start + 1) {
return ""; // Only one character, and it was stripped.
}
if (stripped == null) {
stripped = new SpannableStringBuilder(source, start, end);
}
stripped.delete(i - start, i + 1 - start);
}
}
if (stripped != null) {
return stripped;
} else if (out != null) {
return out;
} else {
return null;
}
}
}
IMHO the best approach for this problem is to just use the InputFilter. A nice gist is here DecimalDigitsInputFilter. Then you can just:
editText.setInputType(TYPE_NUMBER_FLAG_DECIMAL | TYPE_NUMBER_FLAG_SIGNED | TYPE_CLASS_NUMBER)
editText.setKeyListener(DigitsKeyListener.getInstance("0123456789,.-"))
editText.setFilters(new InputFilter[] {new DecimalDigitsInputFilter(5,2)});
For Mono(Droid) solutions:
decimal decimalValue = decimal.Parse(input.Text.Replace(",", ".") , CultureInfo.InvariantCulture);
You could do the following:
DecimalFormatSymbols d = DecimalFormatSymbols.getInstance(Locale.getDefault());
input.setFilters(new InputFilter[] { new DecimalDigitsInputFilter(5, 2) });
input.setKeyListener(DigitsKeyListener.getInstance("0123456789" + d.getDecimalSeparator()));
And then you could use an input filter:
public class DecimalDigitsInputFilter implements InputFilter {
Pattern mPattern;
public DecimalDigitsInputFilter(int digitsBeforeZero, int digitsAfterZero) {
DecimalFormatSymbols d = new DecimalFormatSymbols(Locale.getDefault());
String s = "\\" + d.getDecimalSeparator();
mPattern = Pattern.compile("[0-9]{0," + (digitsBeforeZero - 1) + "}+((" + s + "[0-9]{0," + (digitsAfterZero - 1) + "})?)||(" + s + ")?");
}
#Override
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
Matcher matcher = mPattern.matcher(dest);
if (!matcher.matches())
return "";
return null;
}
}
to get localize your input use:
char sep = DecimalFormatSymbols.getInstance().getDecimalSeparator();
and then add:
textEdit.setKeyListener(DigitsKeyListener.getInstance("0123456789" + sep));
than don't forget to replace "," with "." so Float or Double can parse it without errors.
I can confirm that the fixes proposed do not work on Samsung IMEs (at least on S6 and S9) and maybe LG. They still show a dot as decimal separator regardless of locale. Switching to Google's IME fixes this but is hardly an option for most developers.
It also has not been fixed in Oreo for these keyboards since it is a fix that Samsung and/or LG have to do and then to push even to their ancient handsets.
I have instead forked the number-keyboard project and added a mode where it behaves like an IME: fork. See the project sample for details. This has worked quite well for me and is similar to many of the "PIN entry" fake IMEs you see in banking apps.
All the other posts here had major holes in them, so here's a solution that will:
Enforce commas or periods based on region, will not let you type the opposite one.
If the EditText starts with some value, it replaces the correct separator as needed.
In the XML:
<EditText
...
android:inputType="numberDecimal"
... />
Class variable:
private boolean isDecimalSeparatorComma = false;
In onCreate, find the separator used in the current locale:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
NumberFormat nf = NumberFormat.getInstance();
if (nf instanceof DecimalFormat) {
DecimalFormatSymbols sym = ((DecimalFormat) nf).getDecimalFormatSymbols();
char decSeparator = sym.getDecimalSeparator();
isDecimalSeparatorComma = Character.toString(decSeparator).equals(",");
}
}
Also onCreate, Use this to update it if you're loading in a current value:
// Replace editText with commas or periods as needed for viewing
String editTextValue = getEditTextValue(); // load your current value
if (editTextValue.contains(".") && isDecimalSeparatorComma) {
editTextValue = editTextValue.replaceAll("\\.",",");
} else if (editTextValue.contains(",") && !isDecimalSeparatorComma) {
editTextValue = editTextValue.replaceAll(",",".");
}
setEditTextValue(editTextValue); // override your current value
Also onCreate, Add the Listeners
editText.addTextChangedListener(editTextWatcher);
if (isDecimalSeparatorComma) {
editText.setKeyListener(DigitsKeyListener.getInstance("0123456789,"));
} else {
editText.setKeyListener(DigitsKeyListener.getInstance("0123456789."));
}
editTextWatcher
TextWatcher editTextWatcher = 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) { }
#Override
public void afterTextChanged(Editable s) {
String editTextValue = s.toString();
// Count up the number of commas and periods
Pattern pattern = Pattern.compile("[,.]");
Matcher matcher = pattern.matcher(editTextValue);
int count = 0;
while (matcher.find()) {
count++;
}
// Don't let it put more than one comma or period
if (count > 1) {
s.delete(s.length()-1, s.length());
} else {
// If there is a comma or period at the end the value hasn't changed so don't update
if (!editTextValue.endsWith(",") && !editTextValue.endsWith(".")) {
doSomething()
}
}
}
};
doSomething() example, convert to standard period for data manipulation
private void doSomething() {
try {
String editTextStr = editText.getText().toString();
if (isDecimalSeparatorComma) {
editTextStr = editTextStr.replaceAll(",",".");
}
float editTextFloatValue = editTextStr.isEmpty() ?
0.0f :
Float.valueOf(editTextStr);
... use editTextFloatValue
} catch (NumberFormatException e) {
Log.e(TAG, "Error converting String to Double");
}
}
My fix for KOTLIN
I came across the same bug, which i fixed with:
val separator = DecimalFormatSymbols.getInstance().decimalSeparator
mEditText.keyListener = DigitsKeyListener.getInstance("0123456789$separator")
and this works quite fine. !BUT! on Samsung Keyboards, the separator is NOT shown, so you cannot type in decimal numbers.
so i had to fix this issue with checking, if Samsung Keyboard is used:
val x = Settings.Secure.getString(getContentResolver(), Settings.Secure.DEFAULT_INPUT_METHOD);
if (x.toLowerCase().contains("samsung")) {}
But then you still have the "." as decimal separator. Therefore you have to replace the dot with comma, if the separator is comma:
val separator: Char = DecimalFormatSymbols.getInstance().decimalSeparator
if (separator == ',') {
mEditText.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
override fun afterTextChanged(s: Editable?) {
if (!s.isNullOrEmpty()) {
if (s.toString().contains(".")) {
val replaced = s.toString().replace('.', separator)
mEditText.setText(replaced)
mEditText.setSelection(replaced.length)
}
}
}
})
}
But then you have to check that nobody types more "," in the EditTextfield. This can be done with a Regex.
My whole solution:
val x = Settings.Secure.getString(getContentResolver(), Settings.Secure.DEFAULT_INPUT_METHOD);
if (x.toLowerCase().contains("samsung")) {
val Number_REGEX: Pattern = Pattern.compile("^([1-9])*([.,]{1}[0-9]{0,10})?$")
val separator: Char = DecimalFormatSymbols.getInstance().decimalSeparator
if (separator == ',') {
mEditText.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
override fun afterTextChanged(s: Editable?) {
if (!s.isNullOrEmpty()) {
val matcherMail = Number_REGEX.matcher(s.toString())
if (!matcherMail.matches()) {
val length: Int = s.length
s.delete(length - 1, length);
} else {
if (s.toString().contains(".")) {
val replaced = s.toString().replace('.', separator)
mEditText.setText(replaced)
mEditText.setSelection(replaced.length)
}
}
}
}
})
}
} else {
val separator = DecimalFormatSymbols.getInstance().decimalSeparator
mEditText.keyListener = DigitsKeyListener.getInstance("0123456789$separator")
}
xml file:
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/tEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Input"
android:inputType="numberDecimal"
android:imeOptions="actionDone"/>
If you want to use the number, make sure to get the right format:
val x = NumberFormat.getInstance().parse(mEditText.text.toString()).toDouble()
I have a solution that allows the user to enter both the dot and the comma (if available on the keyboard), but only displays the locale default separator. In addition it will not allow the user to enter more than 1 separator. No issues with references to EditText or infinite loops. It is a combination of several answers in this thread suited to my needs.
As with the accepted answer, configure the EditText accordingly:
android:inputType="numberDecimal"
android:digits="0123456789.,"
Then set a custom TextWatcher on the EditText:
myEditText.addTextChangedListener(FlexibleDecimalSeparatorTextWatcher())
And include the custom TextWatcher:
import android.text.Editable
import android.text.SpannableStringBuilder
import android.text.TextWatcher
import android.widget.EditText
import java.text.DecimalFormatSymbols
/**
* The [FlexibleDecimalSeparatorTextWatcher] allows the user to input both the comma (,) and dot (.) as a decimal separator,
* and will then automatically convert each entered separator into the locale default separator.
* If the user were to enter multiple separators - every separator but the first will be removed.
*
* To provide comma and dot support, set the [EditText] inputType to 'numberDecimal' and its digits to '0123456789.,'.
*/
class FlexibleDecimalSeparatorTextWatcher : TextWatcher {
companion object {
private val DECIMAL_SEPARATORS = listOf('.', ',')
private val LOCALE_DEFAULT_DECIMAL_SEPARATOR = DecimalFormatSymbols.getInstance().decimalSeparator
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
if (s != null) {
val textWithConvertedSeparators = convertSeparatorsToLocaleDefault(s.toString())
val textWithoutMultipleSeparators = removeAdditionalSeparators(textWithConvertedSeparators)
// Make the change if required. This only triggers one additional afterTextChanged call if there were changes.
if(s.toString() != textWithoutMultipleSeparators) {
s.replace(0, s.length, SpannableStringBuilder(textWithoutMultipleSeparators))
}
}
}
/**
* This function converts all entered separators (in [DECIMAL_SEPARATORS]) to the [LOCALE_DEFAULT_DECIMAL_SEPARATOR].
*/
private fun convertSeparatorsToLocaleDefault(original: String): String {
var result = original
DECIMAL_SEPARATORS.forEach { separator ->
if (separator != LOCALE_DEFAULT_DECIMAL_SEPARATOR && result.contains(separator)) {
result = result.replace(separator, LOCALE_DEFAULT_DECIMAL_SEPARATOR)
}
}
return result
}
/**
* Strip out all separators but the first.
* In this function we assume all separators are already converted to the locale default.
*/
private fun removeAdditionalSeparators(original: String): String {
var result = original
var separatorCount = result.count { c -> c == LOCALE_DEFAULT_DECIMAL_SEPARATOR }
if(separatorCount > 1) {
// We will reverse the text so we can keep stripping the last (first in reverse) separator off.
var textReversed = result.reversed()
val separatorRegex = Regex.fromLiteral(LOCALE_DEFAULT_DECIMAL_SEPARATOR.toString())
while (separatorCount > 1) {
textReversed = textReversed.replaceFirst(separatorRegex, "")
separatorCount--
}
// And finally we reverse it back to the original order.
result = textReversed.reversed()
}
return result
}
}
Android has a built in number formatter.
You can add this to your EditText to allow decimals and commas:
android:inputType="numberDecimal" and android:digits="0123456789.,"
Then somewhere in your code, either when user clicks save or after text is entered (use a listener).
// Format the number to the appropriate double
try {
Number formatted = NumberFormat.getInstance().parse(editText.getText().toString());
cost = formatted.doubleValue();
} catch (ParseException e) {
System.out.println("Error parsing cost string " + editText.getText().toString());
cost = 0.0;
}
I decided to change comma to dot only while editing. Here is my tricky and relative simple workaround:
editText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
#Override
public void onFocusChange(View v, boolean hasFocus) {
EditText editText = (EditText) v;
String text = editText.getText().toString();
if (hasFocus) {
editText.setText(text.replace(",", "."));
} else {
if (!text.isEmpty()) {
Double doubleValue = Double.valueOf(text.replace(",", "."));
editText.setText(someDecimalFormatter.format(doubleValue));
}
}
}
});
someDecimalFormatter will use comma or dot depends on Locale
I don't know why your answers are so complicated. If there's a bug in SDK you must override it or go around.
I have chosen the second way to solve that problem. If you format your string as Locale.ENGLISH and then put it to the EditText (even as an empty string). Example:
String.format(Locale.ENGLISH,"%.6f", yourFloatNumber);
Chasing that solution your result are compatible with the shown keyboard. Then float and double numbers work in typical for programming languages manner with dot instead of comma.
My solution is:
In main activity:
char separator =DecimalFormatSymbols.getInstance().getDecimalSeparator();
textViewPitchDeadZone.setKeyListener(DigitsKeyListener.getInstance("0123456789" + separator));
In xml file:
android:imeOptions="flagNoFullscreen"
android:inputType="numberDecimal"
and I took the double in the editText as a String.
It's more than 8 years passed and I am surprised, this issue isn't fixed yet...
I struggled with this simple issue since the most upvoted answer by #Martin lets typing multiple separators, i.e. user can type in "12,,,,,,12,1,,21,2,"
Also, the second concern is that on some devices comma is not shown on the numerical keyboard (or requires multiple pressing of a dot button)
Here is my workaround solution, which solves the mentioned problems and lets user typing '.' and ',', but in EditText he will see the only decimal separator which corresponds to current locale:
editText.apply { addTextChangedListener(DoubleTextChangedListener(this)) }
And the text watcher:
open class DoubleTextChangedListener(private val et: EditText) : TextWatcher {
init {
et.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
et.keyListener = DigitsKeyListener.getInstance("0123456789.,")
}
private val separator = DecimalFormatSymbols.getInstance().decimalSeparator
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
//empty
}
#CallSuper
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
et.run {
removeTextChangedListener(this#DoubleTextChangedListener)
val formatted = toLocalizedDecimal(s.toString(), separator)
setText(formatted)
setSelection(formatted.length)
addTextChangedListener(this#DoubleTextChangedListener)
}
}
override fun afterTextChanged(s: Editable?) {
// empty
}
/**
* Formats input to a decimal. Leaves the only separator (or none), which matches [separator].
* Examples:
* 1. [s]="12.12", [separator]=',' -> result= "12,12"
* 2. [s]="12.12", [separator]='.' -> result= "12.12"
* 4. [s]="12,12", [separator]='.' -> result= "12.12"
* 5. [s]="12,12,,..,,,,,34..,", [separator]=',' -> result= "12,1234"
* 6. [s]="12.12,,..,,,,,34..,", [separator]='.' -> result= "12.1234"
* 7. [s]="5" -> result= "5"
*/
private fun toLocalizedDecimal(s: String, separator: Char): String {
val cleared = s.replace(",", ".")
val splitted = cleared.split('.').filter { it.isNotBlank() }
return when (splitted.size) {
0 -> s
1 -> cleared.replace('.', separator).replaceAfter(separator, "")
2 -> splitted.joinToString(separator.toString())
else -> splitted[0]
.plus(separator)
.plus(splitted.subList(1, splitted.size - 1).joinToString(""))
}
}
}
Simple solution, make a custom control. (this is made in Xamarin android but should port easily to java)
public class EditTextDecimalNumber:EditText
{
readonly string _numberFormatDecimalSeparator;
public EditTextDecimalNumber(Context context, IAttributeSet attrs) : base(context, attrs)
{
InputType = InputTypes.NumberFlagDecimal;
TextChanged += EditTextDecimalNumber_TextChanged;
_numberFormatDecimalSeparator = System.Threading.Thread.CurrentThread.CurrentUICulture.NumberFormat.NumberDecimalSeparator;
KeyListener = DigitsKeyListener.GetInstance($"0123456789{_numberFormatDecimalSeparator}");
}
private void EditTextDecimalNumber_TextChanged(object sender, TextChangedEventArgs e)
{
int noOfOccurence = this.Text.Count(x => x.ToString() == _numberFormatDecimalSeparator);
if (noOfOccurence >=2)
{
int lastIndexOf = this.Text.LastIndexOf(_numberFormatDecimalSeparator,StringComparison.CurrentCulture);
if (lastIndexOf!=-1)
{
this.Text = this.Text.Substring(0, lastIndexOf);
this.SetSelection(this.Text.Length);
}
}
}
}
You could use inputType="phone", however in that case you would have to deal with multiple , or . being present, so additional validation would be necessary.
I had to come up with a work around on my side that it consisted of a mix of different answers.
This will allow for "," or "." for up to one decimal
This is my editText:
val separator: Char = DecimalFormatSymbols.getInstance().decimalSeparator
editTextBox.filters = arrayOf<InputFilter>(DecimalDigitsInputFilter(5, 1, separator))
editTextBox.keyListener = DigitsKeyListener.getInstance("0123456789$separator")
and my class to handle specific regex:
class DecimalDigitsInputFilter(
digitsBeforeZero: Int,
digitsAfterZero: Int,
separator: Char
) : InputFilter {
private val mPattern: Pattern =
Pattern.compile("[0-9]{0," + (digitsBeforeZero - 1) + "}+((\\$separator[0-9]{0," + (digitsAfterZero - 1) + "})?)||(\\$separator)?")
override fun filter(source: CharSequence, start: Int, end: Int, dest: Spanned, dstart: Int, dend: Int): CharSequence? {
val matcher = mPattern.matcher(dest)
return if (!matcher.matches()) "" else null
}
}
I think this solution is less complex than the others written here:
<EditText
android:inputType="numberDecimal"
android:digits="0123456789," />
This way when you press the '.' in the soft keyboard nothing happens; only numbers and comma are allowed.