RSS

Search Engine

Saturday, May 22, 2010

Fancy ListViews, Part Four

In our last episode, we took a closer look at the ViewHolder/ViewWrapper pattern for making ListViews that much more efficient to render. Today, we switch gears, and take a look at having interactive elements in ListView rows. Specifically, we’ll look at a crude implementation of a checklist: a ListView of CheckBoxes.

The Android M5 SDK lacks any sort of checklist component. Rumors abound that the next SDK will. So, if you’re reading this, and you’re looking for a checklist, and a newer SDK is available, check the SDK — you are probably better off using the SDK’s built-in checklist than the techniques I am showing here.

That being said, while this ListView uses CheckBoxes, many of the same concepts hold true if you have a ListView whose rows hold Buttons, or perhaps an EditView.

A checklist widget is designed to allow users to easily multi-select from a list, particularly in cases where multiple selections are the norm (versus some list where multiple selections are possible but unlikely). The list contains one checkbox per row, and the user can check off those of interest:

A simple checklist view

For today’s demo, we’ll use the same basic classes as our previous demo — we’re showing a list of nonsense words, in this case as checkboxes. When the user checks a word, though, the word is put in all caps:

The same checklist, with an item checked

It’s not the most sophisticated demo on the planet, but it will keep the extraneous logic to a minimum, so we can focus on the key topics of interest.

What gets tricky with checklists is taking action when the checkbox state changes (e.g., an unchecked box is checked by the user). We need to store that state somewhere, since our CheckBox widget will be recycled when the ListView is scrolled. We need to be able to set the CheckBox state based upon the actual word we are viewing as the CheckBox is recycled, and we need to save the state when it changes so it can be restored when this particular row is scrolled back into view.

What makes this interesting is that, by default, the CheckBox has absolutely no idea what model in the ArrayAdapter it is looking at. After all, the CheckBox is just a widget, used in a row of a ListView. We need to teach the rows which model they are presently displaying, so when their checkbox is checked, they know which model’s state to modify.

So, with all that in mind, let’s look at some code. Here is the activity class, with some significant changes from the previous one:

  1. public class ChecklistDemo extends ListActivity {
  2. TextView selection;
  3. String[] items={"lorem", "ipsum", "dolor", "sit", "amet",
  4. "consectetuer", "adipiscing", "elit", "morbi", "vel",
  5. "ligula", "vitae", "arcu", "aliquet", "mollis",
  6. "etiam", "vel", "erat", "placerat", "ante",
  7. "porttitor", "sodales", "pellentesque", "augue",
  8. "purus"};

  9. @Override
  10. public void onCreate(Bundle icicle) {
  11. super.onCreate(icicle);
  12. setContentView(R.layout.main);

  13. ArrayList list=new ArrayList();

  14. for (String s : items) {
  15. list.add(new RowModel(s));
  16. }

  17. setListAdapter(new CheckAdapter(this, list));
  18. selection=(TextView)findViewById(R.id.selection);
  19. }

  20. private RowModel getModel(int position) {
  21. return(((CheckAdapter)getListAdapter()).getItem(position));
  22. }

  23. public void onListItemClick(ListView parent, View v, int position, long id) {
  24. selection.setText(getModel(position).toString());
  25. }

  26. class CheckAdapter extends ArrayAdapter {
  27. Activity context;

  28. CheckAdapter(Activity context, ArrayList list) {
  29. super(context, R.layout.row, list);

  30. this.context=context;
  31. }

  32. public View getView(int position, View convertView, ViewGroup parent) {
  33. View row=convertView;
  34. ViewWrapper wrapper;
  35. CheckBox cb;

  36. if (row==null) {
  37. ViewInflate inflater=context.getViewInflate();

  38. row=inflater.inflate(R.layout.row, null, null);
  39. wrapper=new ViewWrapper(row);
  40. row.setTag(wrapper);
  41. cb=wrapper.getCheckBox();

  42. CompoundButton.OnCheckedChangeListener l=new CompoundButton.OnCheckedChangeListener() {
  43. public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
  44. Integer myPosition=(Integer)buttonView.getTag();
  45. RowModel model=getModel(myPosition);

  46. model.isChecked=isChecked;
  47. buttonView.setText(model.toString());
  48. }
  49. };

  50. cb.setOnCheckedChangeListener(l);
  51. }
  52. else {
  53. wrapper=(ViewWrapper)row.getTag();
  54. cb=wrapper.getCheckBox();
  55. }

  56. RowModel model=getModel(position);

  57. cb.setTag(new Integer(position));
  58. cb.setText(model.toString());
  59. cb.setChecked(model.isChecked);

  60. return(row);
  61. }
  62. }

  63. class RowModel {
  64. String label;
  65. boolean isChecked=false;

  66. RowModel(String label) {
  67. this.label=label;
  68. }

  69. public String toString() {
  70. if (isChecked) {
  71. return(label.toUpperCase());
  72. }

  73. return(label);
  74. }
  75. }
  76. }

Specifically, here is what’s new:

  1. While we are still using String[] items as the list of nonsense words, rather than pour that String array straight into an ArrayAdapter, we turn it into a list of RowModel objects. RowModel is this demo’s poor excuse for a mutable model: it holds the nonsense word plus the current checked state. In a real system, these might be objects populated from a Cursor, and the properties would have more business meaning.

  2. Utility methods like onListItemClick() had to be updated to reflect the change from a pure-String model to use a RowModel.

  3. The ArrayAdapter subclass (CheckAdapter), in getView(), looks to see if convertView is null. If so, we create a new row by inflating a simple layout (see below) and also attach a ViewWrapper (also below). For the row’s checkbox, we add an anonymous onCheckedChanged() listener that looks at the row’s tag (getTag()) and converts that into an Integer, representing the position within the ArrayAdapter that this row is displaying. Using that, the checkbox can get the actual RowModel for the row and update the model based upon the new state of the checkbox. It also updates the text of the CheckBox when checked to match the checkbox state.

  4. We always make sure that the CheckBox has the proper contents and has a tag (via setTag()) pointing to the position in the adapter the row is displaying.

The row layout is very simple: just a CheckBox inside a LinearLayout:

  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. <CheckBox
  8. android:id="@+id/check"
  9. android:layout_width="wrap_content"
  10. android:layout_height="wrap_content"
  11. android:text="" />
  12. LinearLayout>

Arguably, the LinearLayout is superfluous, but I left it in to remind you that the rows could be more complicated than just a CheckBox — you might have some ImageViews with icons depicting various bits of information about the row, for example.

The ViewWrapper is similarly simple, just extracting the CheckBox out of the row View:

  1. class ViewWrapper {
  2. View base;
  3. CheckBox cb=null;

  4. ViewWrapper(View base) {
  5. this.base=base;
  6. }

  7. CheckBox getCheckBox() {
  8. if (cb==null) {
  9. cb=(CheckBox)base.findViewById(R.id.check);
  10. }

  11. return(cb);
  12. }
  13. }

This is a fairly cumbersome bit of code. No doubt it can be simplified directly, such as by directly holding the RowModel in the tag versus an Integer pointing inside the ArrayAdapter. In addition, in a later episode of this blog post series, we’ll see how you can wrap much of the complexity up into a CheckList custom widget, so you do not have to keep repeating this code every place you want a checklist.

0 comments:

Post a Comment