|
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:

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:

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:
- public
class ChecklistDemo 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);
-
- ArrayList list=new
ArrayList();
-
-
for (String s : items) {
-
list.add(new RowModel(s));
- }
-
- setListAdapter(new
CheckAdapter(this, list));
- selection=(TextView)findViewById(R.id.selection);
- }
-
- private
RowModel getModel(int position) {
-
return(((CheckAdapter)getListAdapter()).getItem(position));
- }
-
- public
void onListItemClick(ListView parent, View
v, int position, long
id) {
- selection.setText(getModel(position).toString());
- }
-
- class CheckAdapter
extends ArrayAdapter {
- Activity context;
-
- CheckAdapter(Activity
context, ArrayList list) {
-
super(context, R.layout.row, list);
-
-
this.context=context;
- }
-
-
public View getView(int
position, View convertView, ViewGroup parent) {
-
View row=convertView;
-
ViewWrapper wrapper;
-
CheckBox cb;
-
-
if (row==null)
{
-
ViewInflate inflater=context.getViewInflate();
-
-
row=inflater.inflate(R.layout.row, null,
null);
-
wrapper=new ViewWrapper(row);
-
row.setTag(wrapper);
-
cb=wrapper.getCheckBox();
-
-
CompoundButton.OnCheckedChangeListener l=new
CompoundButton.OnCheckedChangeListener() {
-
public void onCheckedChanged(CompoundButton
buttonView, boolean isChecked) {
-
Integer myPosition=(Integer)buttonView.getTag();
-
RowModel model=getModel(myPosition);
-
-
model.isChecked=isChecked;
-
buttonView.setText(model.toString());
-
}
-
};
-
-
cb.setOnCheckedChangeListener(l);
-
}
-
else {
-
wrapper=(ViewWrapper)row.getTag();
-
cb=wrapper.getCheckBox();
-
}
-
-
RowModel model=getModel(position);
-
-
cb.setTag(new Integer(position));
-
cb.setText(model.toString());
-
cb.setChecked(model.isChecked);
-
-
return(row);
- }
- }
-
- class RowModel
{
- String label;
-
boolean isChecked=false;
-
- RowModel(String label)
{
-
this.label=label;
- }
-
-
public String toString() {
-
if (isChecked) {
-
return(label.toUpperCase());
-
}
-
-
return(label);
- }
- }
- }
public class ChecklistDemo 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);
ArrayList list=new ArrayList();
for (String s : items) {
list.add(new RowModel(s));
}
setListAdapter(new CheckAdapter(this, list));
selection=(TextView)findViewById(R.id.selection);
}
private RowModel getModel(int position) {
return(((CheckAdapter)getListAdapter()).getItem(position));
}
public void onListItemClick(ListView parent, View v, int position, long id) {
selection.setText(getModel(position).toString());
}
class CheckAdapter extends ArrayAdapter {
Activity context;
CheckAdapter(Activity context, ArrayList list) {
super(context, R.layout.row, list);
this.context=context;
}
public View getView(int position, View convertView, ViewGroup parent) {
View row=convertView;
ViewWrapper wrapper;
CheckBox cb;
if (row==null) {
ViewInflate inflater=context.getViewInflate();
row=inflater.inflate(R.layout.row, null, null);
wrapper=new ViewWrapper(row);
row.setTag(wrapper);
cb=wrapper.getCheckBox();
CompoundButton.OnCheckedChangeListener l=new CompoundButton.OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
Integer myPosition=(Integer)buttonView.getTag();
RowModel model=getModel(myPosition);
model.isChecked=isChecked;
buttonView.setText(model.toString());
}
};
cb.setOnCheckedChangeListener(l);
}
else {
wrapper=(ViewWrapper)row.getTag();
cb=wrapper.getCheckBox();
}
RowModel model=getModel(position);
cb.setTag(new Integer(position));
cb.setText(model.toString());
cb.setChecked(model.isChecked);
return(row);
}
}
class RowModel {
String label;
boolean isChecked=false;
RowModel(String label) {
this.label=label;
}
public String toString() {
if (isChecked) {
return(label.toUpperCase());
}
return(label);
}
}
}
Specifically, here is what’s new:
-
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.
-
Utility methods like onListItemClick() had to
be updated to reflect the change from a pure-String model to use
a RowModel.
-
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.
-
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:
- <?xml
version="1.0"
encoding="utf-8"?>
- <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal"
- >
- <CheckBox
-
android:id="@+id/check"
-
android:layout_width="wrap_content"
-
android:layout_height="wrap_content"
-
android:text=""
/>
- </LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<CheckBox
android:id="@+id/check"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="" />
</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:
- class ViewWrapper {
- View base;
- CheckBox cb=null;
-
- ViewWrapper(View base) {
-
this.base=base;
- }
-
- CheckBox getCheckBox() {
-
if (cb==null)
{
-
cb=(CheckBox)base.findViewById(R.id.check);
- }
-
-
return(cb);
- }
- }
class ViewWrapper {
View base;
CheckBox cb=null;
ViewWrapper(View base) {
this.base=base;
}
CheckBox getCheckBox() {
if (cb==null) {
cb=(CheckBox)base.findViewById(R.id.check);
}
return(cb);
}
}
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.
Source: http://androidguys.com/2008/07/25/fancy-listviews-part-four/
 |
It can't be a matter of effeminacy; they're meant to be unisex. Those who first favoured the wool-lined sheepskin boot were not only men, but men of a macho stripe befitting footwear seemingly named after the main ugg boots for sale conversational gambit of the caveman: first world war aviators and sheep-shearers in rural Australia.