In our last episode, we took a closer look at the ViewHolder
/ViewWrapper
pattern for making ListView
s 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 CheckBox
es.
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 CheckBox
es, many of the same concepts hold true if you have a ListView
whose rows hold Button
s, 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);
- }
- }
- }
Specifically, here is what’s new:
-
While we are still using
String[] items
as the list of nonsense words, rather than pour thatString
array straight into anArrayAdapter
, we turn it into a list ofRowModel
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 aCursor
, 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 aRowModel
. -
The
ArrayAdapter
subclass (CheckAdapter
), ingetView()
, looks to see ifconvertView
is null. If so, we create a new row by inflating a simple layout (see below) and also attach aViewWrapper
(also below). For the row’s checkbox, we add an anonymousonCheckedChanged()
listener that looks at the row’s tag (getTag()
) and converts that into an Integer, representing the position within theArrayAdapter
that this row is displaying. Using that, the checkbox can get the actualRowModel
for the row and update the model based upon the new state of the checkbox. It also updates the text of theCheckBox
when checked to match the checkbox state. -
We always make sure that the
CheckBox
has the proper contents and has a tag (viasetTag()
) 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>
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 ImageView
s 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);
- }
- }
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