|
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>
<?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<delegate.getCount();i++)
{
-
this.states[i]=false;
- }
- }
-
- public List getCheckedPositions()
{
- List result=new
ArrayList();
-
-
for (int i=0;i<delegate.getCount();i++)
{
-
if (states[i]) {
-
result.add(new Integer(i));
-
}
- }
-
-
return(result);
- }
-
- public List getCheckedObjects()
{
- List result=new
ArrayList();
-
-
for (int i=0;i<delegate.getCount();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<delegate.getCount();i++) {
this.states[i]=false;
}
}
public List getCheckedPositions() {
List result=new ArrayList();
for (int i=0;i<delegate.getCount();i++) {
if (states[i]) {
result.add(new Integer(i));
}
}
return(result);
}
public List getCheckedObjects() {
List result=new ArrayList();
for (int i=0;i<delegate.getCount();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);
}
}
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.
Source: http://androidguys.com/2008/07/31/fancy-listviews-part-six-custom-widget/
 |