RSS

Search Engine

Saturday, May 22, 2010

Fancy ListViews, Part Six: Custom Widget

In this, the last and longest of our Fancy ListView posts, we’ll cover what it takes to wrap up the logic from the ChecklistDemo from a previous post and turn it into a reusable CheckListView that can serve as a drop-in replacement for ListView.

Before I go much further, though, please bear in mind that the next version of the Android SDK may have a similar component built into the framework. If so, I heartily encourage you to use the official one, for ease of long-term maintenance, and so your application is that much smaller. But, until then, or if you want to use the techniques shown here for some other custom ListView subclass, read on!

What we’d really like is to be able to create a layout like this:

  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. <com.commonsware.android.fancylists.eight.CheckListView
  12. android:id="@android:id/list"
  13. android:layout_width="fill_parent"
  14. android:layout_height="fill_parent"
  15. android:drawSelectorOnTop="false"
  16. />
  17. LinearLayout>

where, in our code, almost all of the logic that might have referred to a ListView before “just works” with the CheckListView we put in the layout:

  1. public class CheckListViewDemo 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. setListAdapter(new ArrayAdapter(this, android.R.layout.simple_list_item_1, items));
  14. selection=(TextView)findViewById(R.id.selection);
  15. }

  16. public void onListItemClick(ListView parent, View v, int position, long id) {
  17. selection.setText(items[position]);
  18. }
  19. }

The CheckListView might offer some additional methods, such as getCheckedPositions() to get a list of position indexes that were checked, or getCheckedObjects() to get the actual objects that were checked.

Where things get a wee bit challenging is when you stop and realize that, in all our previous work with fancy ListViews, never were we actually changing the ListView itself. All our work was with the adapters, overriding getView() and inflating our own rows, and whatnot.

So if we want CheckListView to take in any ordinary ListAdapter and “just work”, putting checkboxes on the rows as needed, we are going to need to do some fancy footwork. Specifically, we are going to need to wrap the “raw” ListAdapter in some other ListAdapter that knows how to put the checkboxes on the rows and track the state of those checkboxes.

First, we need to establish the pattern of one ListAdapter augmenting another. Here is the code for AdapterWrapper, which takes a ListAdapter and delegates all of the interface’s methods to the delegate:

  1. public class AdapterWrapper implements ListAdapter {
  2. ListAdapter delegate=null;

  3. public AdapterWrapper(ListAdapter delegate) {
  4. this.delegate=delegate;
  5. }

  6. public int getCount() {
  7. return(delegate.getCount());
  8. }

  9. public Object getItem(int position) {
  10. return(delegate.getItem(position));
  11. }

  12. public long getItemId(int position) {
  13. return(delegate.getItemId(position));
  14. }

  15. public int getNewSelectionForKey(int currentSelection, int keyCode, KeyEvent event) {
  16. return(delegate.getNewSelectionForKey(currentSelection, keyCode, event));
  17. }

  18. public View getView(int position, View convertView, ViewGroup parent) {
  19. return(delegate.getView(position, convertView, parent));
  20. }

  21. public void registerDataSetObserver(DataSetObserver observer) {
  22. delegate.registerDataSetObserver(observer);
  23. }

  24. public boolean stableIds() {
  25. return(delegate.stableIds());
  26. }

  27. public void unregisterDataSetObserver(DataSetObserver observer) {
  28. delegate.unregisterDataSetObserver(observer);
  29. }

  30. public boolean areAllItemsSelectable() {
  31. return(delegate.areAllItemsSelectable());
  32. }

  33. public boolean isSelectable(int position) {
  34. return(delegate. isSelectable(position));
  35. }
  36. }

We can then subclass AdapterWrapper to create CheckableWrapper, overriding the default getView() but otherwise allowing the delegated ListAdapter to do the “real work”:

  1. public class CheckableWrapper extends AdapterWrapper {
  2. Context ctxt=null;
  3. boolean[] states=null;

  4. public CheckableWrapper(Context ctxt, ListAdapter delegate) {
  5. super(delegate);

  6. this.ctxt=ctxt;
  7. this.states=new boolean[delegate.getCount()];

  8. for (int i=0;i
  9. this.states[i]=false;
  10. }
  11. }

  12. public List getCheckedPositions() {
  13. List result=new ArrayList();

  14. for (int i=0;i
  15. if (states[i]) {
  16. result.add(new Integer(i));
  17. }
  18. }

  19. return(result);
  20. }

  21. public List getCheckedObjects() {
  22. List result=new ArrayList();

  23. for (int i=0;i
  24. if (states[i]) {
  25. result.add(delegate.getItem(i));
  26. }
  27. }

  28. return(result);
  29. }

  30. public View getView(int position, View convertView, ViewGroup parent) {
  31. ViewWrapper wrap=null;
  32. View row=convertView;

  33. if (convertView==null) {
  34. LinearLayout layout=new LinearLayout(ctxt);
  35. CheckBox cb=new CheckBox(ctxt);
  36. View guts=delegate.getView(position, null, parent);

  37. layout.setOrientation(LinearLayout.HORIZONTAL);

  38. cb.setLayoutParams(new LinearLayout.LayoutParams(
  39. LinearLayout.LayoutParams.WRAP_CONTENT,
  40. LinearLayout.LayoutParams.FILL_PARENT));
  41. guts.setLayoutParams(new LinearLayout.LayoutParams(
  42. LinearLayout.LayoutParams.FILL_PARENT,
  43. LinearLayout.LayoutParams.FILL_PARENT));

  44. cb.setOnCheckedChangeListener(
  45. new CheckBox.OnCheckedChangeListener() {
  46. public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
  47. states[(Integer)buttonView.getTag()]=isChecked;
  48. }
  49. });

  50. layout.addView(cb);
  51. layout.addView(guts);

  52. wrap=new ViewWrapper(layout);
  53. wrap.setGuts(guts);
  54. layout.setTag(wrap);

  55. cb.setTag(new Integer(position));
  56. cb.setChecked(states[position]);

  57. row=layout;
  58. }
  59. else {
  60. wrap=(ViewWrapper)convertView.getTag();
  61. wrap.setGuts(delegate.getView(position, wrap.getGuts(), parent));
  62. wrap.getCheckBox().setTag(new Integer(position));
  63. wrap.getCheckBox().setChecked(states[position]);
  64. }

  65. return(row);
  66. }
  67. }

0 comments:

Post a Comment