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:
- xml version="1.0" encoding="utf-8"?>
- <LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="vertical"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent" >
- <TextView
- android:id="@+id/selection"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"/>
- <com.commonsware.android.fancylists.eight.CheckListView
- android:id="@android:id/list"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:drawSelectorOnTop="false"
- />
- 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:
- public class CheckListViewDemo extends ListActivity {
- TextView selection;
- String[] items={"lorem", "ipsum", "dolor", "sit", "amet",
- "consectetuer", "adipiscing", "elit", "morbi", "vel",
- "ligula", "vitae", "arcu", "aliquet", "mollis",
- "etiam", "vel", "erat", "placerat", "ante",
- "porttitor", "sodales", "pellentesque", "augue",
- "purus"};
-
- @Override
- public void onCreate(Bundle icicle) {
- super.onCreate(icicle);
- setContentView(R.layout.main);
-
- setListAdapter(new ArrayAdapter(this, android.R.layout.simple_list_item_1, items));
- selection=(TextView)findViewById(R.id.selection);
- }
-
- public void onListItemClick(ListView parent, View v, int position, long id) {
- selection.setText(items[position]);
- }
- }
public class CheckListViewDemo extends ListActivity { TextView selection; String[] items={"lorem", "ipsum", "dolor", "sit", "amet", "consectetuer", "adipiscing", "elit", "morbi", "vel", "ligula", "vitae", "arcu", "aliquet", "mollis", "etiam", "vel", "erat", "placerat", "ante", "porttitor", "sodales", "pellentesque", "augue", "purus"}; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); setListAdapter(new ArrayAdapter(this, android.R.layout.simple_list_item_1, items)); selection=(TextView)findViewById(R.id.selection); } public void onListItemClick(ListView parent, View v, int position, long id) { selection.setText(items[position]); } }
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:
- public class AdapterWrapper implements ListAdapter {
- ListAdapter delegate=null;
-
- public AdapterWrapper(ListAdapter delegate) {
- this.delegate=delegate;
- }
-
- public int getCount() {
- return(delegate.getCount());
- }
-
- public Object getItem(int position) {
- return(delegate.getItem(position));
- }
-
- public long getItemId(int position) {
- return(delegate.getItemId(position));
- }
-
- public int getNewSelectionForKey(int currentSelection, int keyCode, KeyEvent event) {
- return(delegate.getNewSelectionForKey(currentSelection, keyCode, event));
- }
-
- public View getView(int position, View convertView, ViewGroup parent) {
- return(delegate.getView(position, convertView, parent));
- }
-
- public void registerDataSetObserver(DataSetObserver observer) {
- delegate.registerDataSetObserver(observer);
- }
-
- public boolean stableIds() {
- return(delegate.stableIds());
- }
-
- public void unregisterDataSetObserver(DataSetObserver observer) {
- delegate.unregisterDataSetObserver(observer);
- }
-
- public boolean areAllItemsSelectable() {
- return(delegate.areAllItemsSelectable());
- }
-
- public boolean isSelectable(int position) {
- return(delegate. isSelectable(position));
- }
- }
public class AdapterWrapper implements ListAdapter { ListAdapter delegate=null; public AdapterWrapper(ListAdapter delegate) { this.delegate=delegate; } public int getCount() { return(delegate.getCount()); } public Object getItem(int position) { return(delegate.getItem(position)); } public long getItemId(int position) { return(delegate.getItemId(position)); } public int getNewSelectionForKey(int currentSelection, int keyCode, KeyEvent event) { return(delegate.getNewSelectionForKey(currentSelection, keyCode, event)); } public View getView(int position, View convertView, ViewGroup parent) { return(delegate.getView(position, convertView, parent)); } public void registerDataSetObserver(DataSetObserver observer) { delegate.registerDataSetObserver(observer); } public boolean stableIds() { return(delegate.stableIds()); } public void unregisterDataSetObserver(DataSetObserver observer) { delegate.unregisterDataSetObserver(observer); } public boolean areAllItemsSelectable() { return(delegate.areAllItemsSelectable()); } public boolean isSelectable(int position) { return(delegate. isSelectable(position)); } }
We can then subclass AdapterWrapper
to create CheckableWrapper
, overriding the default getView()
but otherwise allowing the delegated ListAdapter
to do the “real work”:
- public class CheckableWrapper extends AdapterWrapper {
- Context ctxt=null;
- boolean[] states=null;
-
- public CheckableWrapper(Context ctxt, ListAdapter delegate) {
- super(delegate);
-
- this.ctxt=ctxt;
- this.states=new boolean[delegate.getCount()];
-
- for (int i=0;i
- this.states[i]=false;
- }
- }
-
- public List getCheckedPositions() {
- List result=new ArrayList();
-
- for (int i=0;i
- if (states[i]) {
- result.add(new Integer(i));
- }
- }
-
- return(result);
- }
-
- public List getCheckedObjects() {
- List result=new ArrayList();
-
- for (int i=0;i
- if (states[i]) {
- result.add(delegate.getItem(i));
- }
- }
-
- return(result);
- }
-
- public View getView(int position, View convertView, ViewGroup parent) {
- ViewWrapper wrap=null;
- View row=convertView;
-
- if (convertView==null) {
- LinearLayout layout=new LinearLayout(ctxt);
- CheckBox cb=new CheckBox(ctxt);
- View guts=delegate.getView(position, null, parent);
-
- layout.setOrientation(LinearLayout.HORIZONTAL);
-
- cb.setLayoutParams(new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.WRAP_CONTENT,
- LinearLayout.LayoutParams.FILL_PARENT));
- guts.setLayoutParams(new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.FILL_PARENT,
- LinearLayout.LayoutParams.FILL_PARENT));
-
- cb.setOnCheckedChangeListener(
- new CheckBox.OnCheckedChangeListener() {
- public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
- states[(Integer)buttonView.getTag()]=isChecked;
- }
- });
-
- layout.addView(cb);
- layout.addView(guts);
-
- wrap=new ViewWrapper(layout);
- wrap.setGuts(guts);
- layout.setTag(wrap);
-
- cb.setTag(new Integer(position));
- cb.setChecked(states[position]);
-
- row=layout;
- }
- else {
- wrap=(ViewWrapper)convertView.getTag();
- wrap.setGuts(delegate.getView(position, wrap.getGuts(), parent));
- wrap.getCheckBox().setTag(new Integer(position));
- wrap.getCheckBox().setChecked(states[position]);
- }
-
- return(row);
- }
- }
public class CheckableWrapper extends AdapterWrapper { Context ctxt=null; boolean[] states=null; public CheckableWrapper(Context ctxt, ListAdapter delegate) { super(delegate); this.ctxt=ctxt; this.states=new boolean[delegate.getCount()]; for (int i=0;i The idea is that CheckableWrapper
is where most of our checklist logic resides. It puts the checkboxes on the rows and it tracks the checkboxes’ states as they are checked and unchecked. For the states, it has a boolean[]
sized to fit the number of rows that the delegate says are in the list.
CheckableWrapper
’s implementation of getView()
is reminiscent of the one from ChecklistDemo
, except that rather than use ViewInflate
, we need to manually construct a LinearLayout
to hold our CheckBox
and the “guts” (a.k.a., whatever view the delegate created that we are decorating with the checkbox). ViewInflate
is designed to construct a View
from raw widgets; in our case, we don’t know in advance what the rows will look like, other than that we need to add a checkbox to them. However, the rest is similar to the one from ChecklistDemo
, including using a ViewWrapper
(below), hooking onCheckedChanged()
to have the checkbox update the state, and so forth:
- class ViewWrapper {
- ViewGroup base;
- View guts=null;
- CheckBox cb=null;
-
- ViewWrapper(ViewGroup base) {
- this.base=base;
- }
-
- CheckBox getCheckBox() {
- if (cb==null) {
- cb=(CheckBox)base.getChildAt(0);
- }
-
- return(cb);
- }
-
- void setCheckBox(CheckBox cb) {
- this.cb=cb;
- }
-
- View getGuts() {
- if (guts==null) {
- guts=base.getChildAt(1);
- }
-
- return(guts);
- }
-
- void setGuts(View guts) {
- this.guts=guts;
- }
- }
class ViewWrapper { ViewGroup base; View guts=null; CheckBox cb=null; ViewWrapper(ViewGroup base) { this.base=base; } CheckBox getCheckBox() { if (cb==null) { cb=(CheckBox)base.getChildAt(0); } return(cb); } void setCheckBox(CheckBox cb) { this.cb=cb; } View getGuts() { if (guts==null) { guts=base.getChildAt(1); } return(guts); } void setGuts(View guts) { this.guts=guts; } }
CheckableWrapper
also has implementations of getCheckedPositions()
and getCheckedObjects()
that blend the state information with the delegate’s data to return the selections as indexes or objects.
With all that in place, CheckListView
is comparatively simple:
- public class CheckListView extends ListView {
- public CheckListView(Context context) {
- super(context);
- }
-
- public CheckListView(Context context, AttributeSet attrs, Map inflateParams) {
- super(context, attrs, inflateParams);
- }
-
- public CheckListView(Context context, AttributeSet attrs, Map inflateParams, int defStyle) {
- super(context, attrs, inflateParams, defStyle);
- }
-
- public void setAdapter(ListAdapter adapter) {
- super.setAdapter(new CheckableWrapper(getContext(), adapter));
- }
-
- public List getCheckedPositions() {
- return(((CheckableWrapper)getAdapter()).getCheckedPositions());
- }
-
- public List getCheckedObjects() {
- return(((CheckableWrapper)getAdapter()).getCheckedObjects());
- }
- }
public class CheckListView extends ListView { public CheckListView(Context context) { super(context); } public CheckListView(Context context, AttributeSet attrs, Map inflateParams) { super(context, attrs, inflateParams); } public CheckListView(Context context, AttributeSet attrs, Map inflateParams, int defStyle) { super(context, attrs, inflateParams, defStyle); } public void setAdapter(ListAdapter adapter) { super.setAdapter(new CheckableWrapper(getContext(), adapter)); } public List getCheckedPositions() { return(((CheckableWrapper)getAdapter()).getCheckedPositions()); } public List getCheckedObjects() { return(((CheckableWrapper)getAdapter()).getCheckedObjects()); } }
We simply subclass ListView
and override setAdapter()
so we can wrap the supplied ListAdapter
in our own CheckableWrapper
. We also surface the getCheckedPositions()
and getCheckedObjects()
to complete the encapsulation, so users of CheckListView
have no idea that there is a wrapper in use.
Visually, the results are similar to the ChecklistDemo
:
The difference is in reusability. We could package CheckListView in its own JAR and plop it into any Android project where we need it. So while CheckListView is somewhat complicated to write, we only have to write it once, and the rest of the application code is blissfully simple.
Of course, this CheckListView could use some more features, such as programmatically changing states (updating both the boolean[] and the actual CheckBox itself), allowing other application logic to be invoked when a CheckBox state is toggled (via some sort of callback), etc. These are left as exercises for the reader.
This concludes the Fancy ListView blog post series. After the next SDK is released, we will revisit these and other Building ‘Droids posts, to let you know what all has changed that affects the code samples you’ve seen.
Next time, we’ll talk about doing something in Android that Apple is doing its level best to prevent in the iPhone: on-device scripting.
0 comments:
Post a Comment