Decimal separator comma (',') with numberDecimal inputType in EditText

Viewed 138287

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?

24 Answers

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.

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.

Sample app screenshot

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
}

}

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.

Related