ExpandableListView group indicator bounds nonsense

Digging deeper into ExpandableListView I came across another weird API design issue. This one is about ExpandableListView group indicator images.

These images are displayed on the left side of a group row in the list. As the name suggests they indicate if the group is expanded or collapsed. Obviously there can be two different images here and Android supports it. You can write an XML drawable selector (setGroupIndicator (Drawable groupIndicator)) with two states: state_empty for collapsed and state_expanded for an expanded group.

Well, what is also obvious to me is the fact that two images can have two different sizes, right? Apparently it is not so obvious to people who designed this part of Android API. You can have two different images for two different group states, but… both images will be stretched to fit the size you set by ExpandableListView‘s setIndicatorBounds(int left, int right) method.

What options do you have? You can set indicator bounds (via setIndicatorBounds(int left, int right)) to match wider one of your two images and compensate for differences using this kind of selector:

<selector xmlns:android="http://schemas.android.com/apk/res/android" >

    <item android:state_empty="true">
        <layer-list>
        	<item
			    android:left="0dp"
			    android:right="20dp"
			    android:top="10dp"
			    android:bottom="10dp"
			    android:drawable="@drawable/collapsed_image">
 			</item>
 		</layer-list>
    </item>

    <item android:state_expanded="true">
        <layer-list>
        	<item
			    android:left="0dp"
			    android:right="0dp"
			    android:top="10dp"
			    android:bottom="10dp"
			    android:drawable="@drawable/expanded_image">
 			</item>
 		</layer-list>
    </item>

</selector>

In above example the expanded image is a wider one, so we compensate 20dp in android:right coordinate of the collapsed image. As you can see there are also some sample top and bottom margins for both images.

Second option is to convert your images to a 9-patches, carefully selecting stretchable and non-stretchable parts. As it deals with image editing by hand I consider this solution impractical.

You can also set your indicator to @null in selector items and put your indicator views inside XML row layouts. You will have to write a custom ExpandableListAdapter and display/hide indicator views after inflating these from your row layout in getGroupView implementation.

I haven’t tested it yet, but this looks like the best solution. Ditch this useless feature of group indicators and do it your way.

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…