December 27, 2014

Fancy ListViews, Part Five

In one of our earlier posts in this Fancy ListViews series, Michal asked “could you make also a short tutorial on changing the background image and text color of the selection in ListView?”

Of course, we at AndroidGuys love fan mail! So, let’s talk about changing the way selections look in ListViews. As with many things in Android development, there’s the simple answer, the realization that the simple answer isn’t so simple, and some workarounds.

In theory, changing the selection bar is a matter of setting the android:listSelector property on the ListView widget in the layout XML, or using the equivalent setSelector() methods on the ListView object itself. In practice, well, let’s say there are issues…

Let’s first take a simple case: we want to change the color of the selection bar to something else, such as green. In the layout XML, the change is a single line:

[sourcecode language=’xml’]

xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
android:id="@+id/selection"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>

android:id="@android:id/list"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:drawSelectorOnTop="false"
android:listSelector="@drawable/green"
/> [/sourcecode]

All we did was add android:listSelector="@drawable/green". Of course, we need to define that Drawable. In this case, since we’re settling for a solid color, a PaintDrawable defined in res/values/colors.xml will suffice:

[sourcecode language=’xml’]

#f0f0

[/sourcecode]

That change alone will give us a ListView where the selection bar is green.

That’s nice and all, but suppose we want to do something more elaborate, something more exciting, something that speaks to the way the world is today.

In other words, rather than have list items be solely highlighted with a bar, we want to have them be pointed to by the Flying Fickle Finger of Fate.

(NOTE: any resemblance of this Flying Fickle Finger of Fate to the original is purely coincidental and terribly amusing)

For the Flying Fickle Finger of Fate (or FFFF for short), we’ll use a suitable icon, courtesy of OpenClipArt.org:

Flying Fickle Finger of Fate

The goal is to have the currently-selected list entry be pointed to by the finger. Sounds easy, right?

After all, since android:listSelector takes a reference to a Drawable resource, all we need to do is drop a suitably-sized PNG of FFFF into res/drawable, point to it from android:listSelector, set aside some whitespace on the left of our row layout to accommodate the icon, and it “just works”. Right?

Right?

Well, not exactly.

Android will attempt to stretch whatever PNG drawable you use as the list selector, so it takes up the full width of the ListView. In fact, I suspect it really would like you to use the so-called “Nine Patch” style PNG, where you use an outer border of pixels to provide instructions to Android for how to stretch the PNG.

I tried that and got…unpleasant results. Whether that was the result of my error in coding, my error in understanding how to make PNG-based list selectors work, or a bug in the M5 SDK, is still under investigation.

But the Flying Fickle Finger of Fate will not be denied. So, let’s take a look at how you “manually” adjust the way selected items looks. This also covers Michal’s second request — to see how to adjust other properties of the selected row besides the background image.

Working from the code first demonstrated in a previous post, let’s make a few changes.

First, we add an ImageView to our rows:

[sourcecode language=’xml’]

android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
android:id="@+id/ffff"
android:layout_width="81px"
android:layout_height="41px"
/>
android:id="@+id/icon"
android:layout_width="22px"
android:paddingLeft="2px"
android:paddingRight="2px"
android:paddingTop="2px"
android:layout_height="wrap_content"
android:src="@drawable/ok"
/>
android:id="@+id/label"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_gravity="center_vertical"
android:textSize="25sp"
/>
[/sourcecode]

Note how we neglected to provide the android:src attribute in the layout XML. Without a source, the ImageView simply paints nothing, showing the background. But, since we specified a size in pixels, it will take up that amount of space. In this case, we chose a pixel size that matches with an FFFF icon we prepared.

Next, we update our ViewWrapper to give us access to the FFFF:

[sourcecode language=’java’]
package com.commonsware.android.fancylists.seven;

import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;

class ViewWrapper {
View base;
TextView label=null;
ImageView icon=null;
ImageView ffff=null;

ViewWrapper(View base) {
this.base=base;
}

TextView getLabel() {
if (label==null) {
label=(TextView)base.findViewById(R.id.label);
}

return(label);
}

ImageView getIcon() {
if (icon==null) {
icon=(ImageView)base.findViewById(R.id.icon);
}

return(icon);
}

ImageView getFlyingFickleFingerOfFate() {
if (ffff==null) {
ffff=(ImageView)base.findViewById(R.id.ffff);
}

return(ffff);
}
}
[/sourcecode]

Next, we add an OnItemSelectedListener to the ListView. As one might expect, this gets invoked every time an item is selected in the list, or when nothing is selected. There are two separate callback methods: onItemSelected() and onNothingSelected(). Our mission in these callbacks is to draw the FFFF on the selected row and to remove any previous FFFF from the previous selection.

In onItemSelected(), we see if we already have fingered a row (lastFinger!=null); if so, we null out the image Drawable, causing that ImageView to return to its original background-only state. We then get the current row’s ViewWrapper, get our FFFF placeholder, and have it actually draw the FFFF:

[sourcecode language=’java’]
public void onItemSelected(AdapterView parent, View v, int position, long id) {
if (lastFinger!=null) {
lastFinger.setImageDrawable(null);
}

ViewWrapper wrapper=(ViewWrapper)v.getTag();

lastFinger=wrapper.getFlyingFickleFingerOfFate();
lastFinger.setImageResource(R.drawable.ffff);
}
[/sourcecode]

In onNothingSelected(), we simply null out the previous finger (if there was one), both in terms of the image displayed and in terms of the lastFinger variable.

[sourcecode language=’java’]
public void onNothingSelected(AdapterView parent) {
if (lastFinger!=null) {
lastFinger.setImageDrawable(null);
lastFinger=null;
}
}
[/sourcecode]

The result is almost what we were after, complete with the green bar discussed earlier:

Green selection bar with Flying Fickle Finger of Fate

There are two flaws in this implementation as seen in the M5 SDK emulator. First, we get a FFFF icon on the first row when the activity starts, even though the row is not selected. That is because onItemSelected() is inexplicably called without that row being selected. Second, if you click on a row, you will get the green bar without the FFFF. In fact, the FFFF is cleared from the previous selection. Apparently, the current version of Android treats clicks and selections as being different from a callback standpoint, but the same from a display standpoint (i.e., uses android:listSelector). We will revisit all of these issues sometime after the next SDK release and see how the ListView behaves at that time.

While this demo shows activating or deactivating an icon in the row, you could do whatever you want to the View that makes up the row. So, if Michal wanted to change the text color of the selected row, that’s merely a matter of getting one’s hands on the desired TextView (perhaps via a wrapper or holder), and then call setTextColor().

Of course, setTextColor() is remarkably more complicated than one might expect, worthy of a future Building ‘Droids post in its own right.

Next time, in our final episode of the Fancy ListViews series, we will go back to the checklist scenario from before and wrap up the checkable-row logic into a CheckListView widget that one could use as a drop-in replacement for ListView. Until then, always remember: ask not to whom the Flying Fickle Finger of Fate points, it points to thee.



  • lostInTransit

    Hi Thanks for the tut. I am trying to create a ListView with a white background. Combining all your previous tutorial, this is what I came up with to have a ListView with white background and black separator <ListView android:id="@android:id/list" android:layout_width="fill_parent" android:layout_height="100dip" android:layout_weight="200" android:scrollingCache="false" android:divider="@drawable/black" android:dividerHeight="2px" android:background="#ffffffff"/> black is defined as a resource and has its value set to #00000000. Now the background comes up to be white. But I can't get the separator to show up. There is no divider! Can you please help me out! Thanks.

  • lostInTransit

    Sorry for the typo, black is defined as #ff000000.

  • Søren Juul

    Could someone fix the pictures in these guides?

    • http://streich.myopenid.com/ mark

      Yes, the images refer to androidguys.net, but they should be androidguys.com 

  • http://commonsware.com Mark Murphy

    A more up-to-date copy of the material from this blog post series can be found in this free excerpt from The Busy Coder’s Guide to Android Development.

  • steven_d

    Hi,

    thanks for this tutorial. I have one question: How can i update the ListView.
    I dont have a fixed number of strings in my array. I test it with:
    my_adapter.notifyDataSetChanged();
    But without success.

    Thanks, Stefan

  • http://www.neilkirk.co.uk Neil

    Hey there

    Thanks for the tutorial. I am having an issue though. I have set the ListView and drawable exactly as you have it here, but when I click the listview, the entire list changes to the background color. Not just the item I clicked… any ideas as to why this is?

    Thanks again
    Neil

  • Bob

    for God sake! please fix the source code on these ListViews posts!

  • Masoud

    Thanks for your great post of Fancy ListViews.
    Can you help me to change the font of ListView? I know we have to change TextView font, but how?
    Thanks

  • roostr

    This post would be great if it was readable…

  • http://twitter.com/Killerdackel @Killerdackel

    Argh the content of the posts is great but the formatting makes them unreadable…

  • http://mfarhan133.wordpress.com farhan

    http://mfarhan133.wordpress.com/2010/10/14/list-view-tutorial-for-android/

    This a simple android list view tutorial, you can also apply onClick listener,, complete source code is available

  • Android

    Same solution but with full source coding is provided on this site http://android-codes-examples.blogspot.com/2011/03/customized-listview-items-selection.html

    here it is mentioned clearly about where to provide the xml as background in the listview, so that we can easy grape it

  • Guest1

    can you add some image as to know how the output looks like..

  • Tom Hicks

    I know this is a few years too late but this http://i.imgur.com/Ici1P.png is a 9 patch of that finger that will work. Maybe it’ll help somebody else who lands here from google. The top and left borders select the area to repeat when stretching and the bottom and right borders select where content will be displayed.