RSS

Search Engine

Saturday, May 22, 2010

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:

  1. xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout
  3. xmlns:android="http://schemas.android.com/apk/res/android"
  4. android:orientation="vertical"
  5. android:layout_width="fill_parent"
  6. android:layout_height="fill_parent" >
  7. <TextView
  8. android:id="@+id/selection"
  9. android:layout_width="fill_parent"
  10. android:layout_height="wrap_content"/>
  11. <ListView
  12. android:id="@android:id/list"
  13. android:layout_width="fill_parent"
  14. android:layout_height="fill_parent"
  15. android:drawSelectorOnTop="false"
  16. android:listSelector="@drawable/green"
  17. />
  18. LinearLayout>

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:

  1. <resources>
  2. <drawable name="green">#f0f0drawable>
  3. resources>

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:

  1. xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:layout_width="fill_parent"
  4. android:layout_height="wrap_content"
  5. android:orientation="horizontal"
  6. >
  7. <ImageView
  8. android:id="@+id/ffff"
  9. android:layout_width="81px"
  10. android:layout_height="41px"
  11. />
  12. <ImageView
  13. android:id="@+id/icon"
  14. android:layout_width="22px"
  15. android:paddingLeft="2px"
  16. android:paddingRight="2px"
  17. android:paddingTop="2px"
  18. android:layout_height="wrap_content"
  19. android:src="@drawable/ok"
  20. />
  21. <TextView
  22. android:id="@+id/label"
  23. android:layout_width="fill_parent"
  24. android:layout_height="fill_parent"
  25. android:layout_gravity="center_vertical"
  26. android:textSize="25sp"
  27. />
  28. LinearLayout>

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:

  1. package com.commonsware.android.fancylists.seven;

  2. import android.view.View;
  3. import android.widget.ImageView;
  4. import android.widget.TextView;

  5. class ViewWrapper {
  6. View base;
  7. TextView label=null;
  8. ImageView icon=null;
  9. ImageView ffff=null;

  10. ViewWrapper(View base) {
  11. this.base=base;
  12. }

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

  17. return(label);
  18. }

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

  23. return(icon);
  24. }

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

  29. return(ffff);
  30. }
  31. }

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:

  1. public void onItemSelected(AdapterView parent, View v, int position, long id) {
  2. if (lastFinger!=null) {
  3. lastFinger.setImageDrawable(null);
  4. }

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

  6. lastFinger=wrapper.getFlyingFickleFingerOfFate();
  7. lastFinger.setImageResource(R.drawable.ffff);
  8. }

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.

  1. public void onNothingSelected(AdapterView parent) {
  2. if (lastFinger!=null) {
  3. lastFinger.setImageDrawable(null);
  4. lastFinger=null;
  5. }
  6. }

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.

0 comments:

Post a Comment