EditText filtering – overcomplicated and poorly documented

Suppose you have an EditText in your application. That’s fairly common, in most of applications you need some kind of text input from your users. Now let’s also assume that you want to restrict characters the user can type or limit the length. You think about filtering user’s input.

You have two options here:

  • TextWatcher protocol
  • InputFilter objects

And this itself is a poor API design. Why have two? Why not one, well thought-over delegate protocol that tells the programer when EditText content is about to be changed and let them discard or modify these changes? What do you need two options for? This creates confusion, makes developers think “what should I choose? which is better?” and makes the API more complicated than it should be (remember Occam’s razor?).

But that is just the beginning of nonsense. Let’s see what happens when you decide to implement this filtering using, say, InputFilter. After a quick glance at the documentation you know you have to override this method in your InputFilter class implementation:

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

Since: API Level 1

This method is called when the buffer is going to replace the range dstart … dend of dest with the new text from the range start … end of source. Return the CharSequence that you would like to have placed there instead, including an empty string if appropriate, or null to accept the original replacement. Be careful to not to reject 0-length replacements, as this is what happens when you delete text. Also beware that you should not attempt to make any changes to dest from this method; you may only examine it for context. Note: If source is an instance of Spanned or Spannable, the span objects in the source should be copied into the filtered result (i.e. the non-null return value). copySpansFrom(Spanned, int, int, Class, Spannable, int) can be used for convenience.

You may think about implementing this method in a straightforward way – create some kind of StringBuilder or buffer from source, remove characters that you do not want in your EditText and return that buffer as a String.

Then you realize that little note in the documentation: “If source is an instance of Spanned or Spannable(…)”. Your straightforward approach won’t work. Modern versions of Android (>=4.0 I guess) introduce dictionary suggestions displayed above the keyboard. If the word user is typing is being displayed above the keyboard your source parameter is an instance of SpannableStringBuilder class (which implements Spanned interface), otherwise it appears to be just a simple String.

So from the documentation you know you may get a Spanned or Spannable, which you have to copy. You want to actually filter your input so you decide to use a SpannableStringBuilder as your buffer.

Here is what might be your first attempt on this task:

if (source instanceof Spanned) {
    SpannableStringBuilder sourceCopy = new SpannableStringBuilder();
    TextUtils.copySpansFrom((Spanned)source, start, end, source.getClass(), sourceCopy, 0);
    /* ... do some filtering on your sourceCopy ... */
    return sourceCopy;
}

Looks nice, but doesn’t work. It turns out that copySpansFrom doesn’t copy the actual text. The docs for SpannableStringBuilder say that you can copy both text and spans using constructor. You give it a chance:

if (source instanceof Spanned) {
    SpannableStringBuilder sourceCopy = new SpannableStringBuilder(source, start, end);
    /* ... do some filtering on your sourceCopy ... */
    return sourceCopy;
}

And guess what? Yes, it copies the text, but now when user types a single letter the EditText field appends all the letters of the word he is currently typing to the end of the text!

You tried you best. You read the docs carefully, tried to implement filtering the way it should be, but failed. It is not you who failed however. You simply stumbled upon one of the many FAILs of Android API. Now you are on your own, poorly written Android API documentation won’t help you anymore.

It turns out that you can modify the source in place if it is a SpannableStringBuffer (code sample here), but the only way to come to this solution is by experimentation and guessing. And this is not how development process for a major mobile platform should work…

Leave a comment