InputFilter on EditText cause repeating text

16.2k views Asked by At

I'm trying to implement an EditText that limits input to Capital chars only [A-Z0-9] with digits as well.

I started with the InputFilter method from some post.But here I am getting one problem on Samsung Galaxy Tab 2 but not in emulator or Nexus 4.

Problem is like this :

  1. When I type "A" the text shows as "A" its good
  2. Now when I type "B" so text should be "AB" but it gives me "AAB" this looks very Strange.

In short it repeats chars

Here's the code I'm working with this code :

public class DemoFilter implements InputFilter {

    public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart,
            int dend) {

        if (source.equals("")) { // for backspace
            return source;
        }
        if (source.toString().matches("[a-zA-Z0-9 ]*")) // put your constraints
                                                        // here
        {
            return source.toString().toUpperCase();
        }
        return "";
    }
}

XML file code :

<EditText
    android:id="@+id/et_licence_plate_1"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_weight="3"
    android:hint="0"
    android:imeOptions="actionNext"
    android:inputType="textNoSuggestions"
    android:maxLength="3"
    android:singleLine="true"
    android:textSize="18px" >
</EditText>

I'm totally stuck up on this one, so any help here would be greatly appreciated.

10

There are 10 answers

5
Kamil Seweryn On

The problem of characters duplication comes from InputFilter bad implementation. Rather return null if replacement should not change:

@Override
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
    boolean keepOriginal = true;
    StringBuilder sb = new StringBuilder(end - start);
    for (int i = start; i < end; i++) {
        char c = source.charAt(i);
        if (isCharAllowed(c)) // put your condition here
            sb.append(c);
        else
            keepOriginal = false;
    }
    if (keepOriginal)
        return null;
    else {
        if (source instanceof Spanned) {
            SpannableString sp = new SpannableString(sb);
            TextUtils.copySpansFrom((Spanned) source, start, end, null, sp, 0);
            return sp;
        } else {
            return sb;
        }           
    }
}

private boolean isCharAllowed(char c) {
    return Character.isUpperCase(c) || Character.isDigit(c);
}
0
pskink On

try this:

class CustomInputFilter implements InputFilter {
    StringBuilder sb = new StringBuilder();

    @Override
    public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
        Log.d(TAG, "filter " + source + " " + start + " " + end + " dest " + dest + " " + dstart + " " + dend);
        sb.setLength(0);
        for (int i = start; i < end; i++) {
            char c = source.charAt(i);
            if (Character.isUpperCase(c) || Character.isDigit(c) || c == ' ') {
                sb.append(c);
            } else
            if (Character.isLowerCase(c)) {
                sb.append(Character.toUpperCase(c));
            }
        }
        return sb;
    }
}

this also allows filtering when filter() method accepts multiple characters at once e.g. pasted text from a clipboard

2
CRUSADER On

InputFilters can be attached to Editable S to constrain the changes that can be made to them. Refer that it emphasises on changes made rather than whole text it contains..

Follow as mentioned below...

 public class DemoFilter implements InputFilter {

        public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart,
                int dend) {

            if (source.equals("")) { // for backspace
                return source;
            }
            if (source.toString().matches("[a-zA-Z0-9 ]*")) // put your constraints
                                                            // here
            {
               char[] ch = new char[end - start];

              TextUtils.getChars(source, start, end, ch, 0);

                // make the characters uppercase
                String retChar = new String(ch).toUpperCase();
                return retChar;
            }
            return "";
        }
    }
0
Ashwin On

I have found many bugs in the Android's InputFilter, I am not sure if those are bugs or intended to be so. But definitely it did not meet my requirements. So I chose to use TextWatcher instead of InputFilter

private String newStr = "";

myEditText.addTextChangedListener(new TextWatcher() {
        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            // Do nothing
        }

        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
            String str = s.toString();
            if (str.isEmpty()) {
                myEditText.append(newStr);
                newStr = "";
            } else if (!str.equals(newStr)) {
                // Replace the regex as per requirement
                newStr = str.replaceAll("[^A-Z0-9]", "");
                myEditText.setText("");
            }
        }

        @Override
        public void afterTextChanged(Editable s) {
            // Do nothing
        }
    });

The above code does not allow users to type any special symbol into your EditText. Only capital alphanumeric characters are allowed.

0
Avinash Jadaun On

recently i faced same problem reason of the problem is... if there is a no change in the input string then don't return source string return null, some device doesn't handle this properly that's why characters are repating.

in your code you are returning

return source.toString().toUpperCase();

don't return this , return null; in place of return source.toString().toUpperCase(); , but it will be a patch fix , it will not handle all scenarios , for all scenario you can use this code.

public class SpecialCharacterInputFilter implements InputFilter {

    private static final String PATTERN = "[^A-Za-z0-9]";
    // if you want to allow space use this pattern
    //private static final String PATTERN = "[^A-Za-z\\s]";

    @Override
    public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
        // Only keep characters that are letters and digits
        String str = source.toString();
        str = str.replaceAll(PATTERN, AppConstants.EMPTY_STRING);
        return str.length() == source.length() ? null : str;
    }
}

what is happening in this code , there is a regular expression by this we will find all characters except alphabets and digits , now it will replace all characters with empty string, then remaining string will have alphabets and digits.

0
Sam On

The problem with most the answers here is that they all mess up the cursor position.

  • If you simply replace text, your cursor ends up in the wrong place for the next typed character
  • If you think you handled that by putting their cursor back at the end, well then they can't add prefix text or middle text, they are always jumped back to the end on each typed character, it's a bad experience.

You have an easy way to handle this, and a more universal way to handle it.

The easy way

          <EditText
            android:id="@+id/itemNameEditText"
            android:text="@={viewModel.selectedCartItemModel.customName}"
            android:digits="abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
            android:inputType="textVisiblePassword"/>

DONE!

Visible password will fix the issue of double callbacks and problems like that. Problem with this solution is it removes your suggestions and autocompletes, and things like that. So if you can get away with this direction, PLEASE DO!!! It will eliminate so many headaches of trying to handle every possible issue of the hard way lol.

The Hard Way

The issue is related to the inputfilter callback structure being triggered by autocomplete. It is easy to reproduce. Just set your inputType = text, and then type abc@ you'll see it get called two times and if you can end up with abcabc instead of just abc if you were trying to ignore @ for example.

  1. First thing you have to handle is deleting to do this, you must return null to accept "" as that is triggered by delete.
  2. Second thing you have to handle is holding delete as that updates every so often, but can come in as a long string of characters, so you need to see if your text length shrunk before doing replacement text or you can end up duplicating your text while holding delete.
  3. Third thing you need to handle is the duplicate callback, by keeping track of the previous text change call to avoid getting it twice. Don't worry you can still type the same letters back to back, it won't prevent that.

Here is my example. It's not perfect, and still has some kinks to work out, but it's a good place to start.

The following example is using databinding, but you are welcome to just use the intentFilter without databinding if that's your style. Abbreviated UI for showing only the parts that matter.

In this example, I restrict to alpha, numeric, and spaces only. I was able to cause a semi-colon to show up once while pounding on the android keyboard like crazy. So there is still some tweaking I believe that may need done.

DISCLAIMER

--I have not tested with auto complete

--I have not tested with suggestions

--I have not tested with copy/paste

--This solution is a 90% there solution to help you, not a battle tested solution

XML FILE

<layout
    xmlns:bind="http://schemas.android.com/apk/res-auto"
    >

<EditText
     bind:allowAlphaNumericOnly="@{true}

OBJECT FILE

@JvmStatic
@BindingAdapter("allowAlphaNumericOnly")
fun restrictTextToAlphaNumericOnly(editText: EditText, value: Boolean) {
    val tagMap = HashMap<String, String>()
    val lastChange = "repeatCheck"
    val lastKnownSize = "handleHoldingDelete"

    if (value) {
        val filter = InputFilter { source, start, end, dest, dstart, dend ->
            val lastKnownChange = tagMap[lastChange]
            val lastKnownLength = tagMap[lastKnownSize]?.toInt()?: 0

            //handle delete
            if (source.isEmpty() || editText.text.length < lastKnownLength) {
                return@InputFilter null
            }

            //duplicate callback handling, Android OS issue
            if (source.toString() == lastKnownChange) {
                return@InputFilter ""
            }

            //handle characters that are not number, letter, or space
            val sb = StringBuilder()
            for (i in start until end) {
                if (Character.isLetter(source[i]) || Character.isSpaceChar(source[i]) || Character.isDigit(source[i])) {
                    sb.append(source[i])
                }
            }

            tagMap[lastChange] = source.toString()
            tagMap[lastKnownSize] = editText.text.length.toString()

            return@InputFilter sb.toString()
        }

        editText.filters = arrayOf(filter)
    }
}
1
elp On

Same for me, InputFilter duplicates characters. This is what I've used:

Kotlin version:

private fun replaceInvalidCharacters(value: String) = value.replace("[a-zA-Z0-9 ]*".toRegex(), "")

textView.addTextChangedListener(object : TextWatcher {
    override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}

    override fun afterTextChanged(s: Editable) {}

    override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
        val newValue = replaceInvalidCharacters(s.toString())
        if (newValue != s.toString()) {
            textView.setText(newValue)
            textView.setSelection(textView.text.length)
        }
    }
})

works well.

0
Karol Kulbaka On

I've met this problem few times before. Setting some kinds of inputTypes in xml propably is the source of problem. To resolve it without any additional logic in InputFilter or TextWatcher just set input type in code instead xml like this:

editText.setInputType(getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);

1
kassim On

I've run into the same issue, after fixing it with solutions posted here there was still a remaining issue with keyboards with autocomplete. One solution is to set the inputType as 'visiblePassword' but that's reducing functionality isn't it?

I was able to fix the solution by, when returning a non-null result in the filter() method, use the call

TextUtils.copySpansFrom((Spanned) source, start, newString.length(), null, newString, 0);

This copies the auto-complete spans into the new result and fixes the weird behaviour of repetition when selecting autocomplete suggestions.

0
Navon Developer On

The following solution also supports the option of an autocomplete keyboard

editTextFreeNote.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 newStr = s.toString();
            newStr = newStr.replaceAll( "[a-zA-Z0-9 ]*", "" );
            if(!s.toString().equals( newStr )) {
                editTextFreeNote.setText( newStr );
                editTextFreeNote.setSelection(editTextFreeNote.getText().length());
            }
        }

        @Override
        public void afterTextChanged(Editable s) {}
    } );