All View objects have getTag() and setTag() methods. These allow you to associate an arbitrary object with the widget. That holder pattern uses that “tag” to hold an object that, in turn, holds each of the child widgets of interest. By attaching that holder to the row View, every time we use the row, we already have access to the child widgets we care about, without having to call findViewById() again.
So, let’s take a look at one of these holder classes (taken from the FancyLists/ViewWrapper sample project at http://apress.com/):
class ViewWrapper {
View base;
TextView label = null;
ImageView icon = null;
ViewWrapper(View base) {
this.base = base;
}
TextView getLabel() {
if (label==null) {
label = (TextView)base.findViewById(R.id.label);
}
return(label);
}
ImageView getIcon() {
if (icon==null) {
icon = (ImageView)base.findViewById(R.id.icon);
}
return(icon);
}
}
ViewWrapper not only holds onto the child widgets, but also lazy-finds the child widgets. If you create a wrapper and never need a specific child, you never go through the findViewById() operation for it and never have to pay for those CPU cycles.
The holder pattern also allows us to do the following:
• Consolidate all our per-widget type casting in one place, rather than having to cast it everywhere we call findViewById()
• Perhaps track other information about the row, such as state information we are not yet ready to “flush” to the underlying model
Using ViewWrapper is a matter of creating an instance whenever we inflate a row and attaching said instance to the row View via setTag(), as shown in this rewrite of getView():
public class ViewWrapperDemo 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 IconicAdapter(this));
selection = (TextView)findViewById(R.id.selection);
}
private String getModel(int position) {
return(((IconicAdapter)getListAdapter()).getItem(position));
}
public void onListItemClick(ListView parent, View v,
int position, long id) {
selection.setText(getModel(position));
}
class IconicAdapter extends ArrayAdapter<String> {
Activity context;
IconicAdapter(Activity context) {
super(context, R.layout.row, items);
this.context = context;
}
public View getView(int position, View convertView,
ViewGroup parent) {
View row = convertView;
ViewWrapper wrapper = null;
if (row==null) {
LayoutInflater inflater = context.getLayoutInflater();
row = inflater.inflate(R.layout.row, null);
wrapper = new ViewWrapper(row);
row.setTag(wrapper);
} else {
wrapper = (ViewWrapper)row.getTag();
}
wrapper.getLabel().setText(getModel(position));
if (getModel(position).length() > 4) {
wrapper.getIcon().setImageResource(R.drawable.delete);
} else {
wrapper.getIcon().setImageResource(R.drawable.ok);
}
return(row);
}
}
}
Just as we check convertView to see if it is null in order to create the row Views as needed, we also pull out (or create) the corresponding row’s ViewWrapper. Then accessing the child widgets is merely a matter of calling their associated methods on the wrapper.
Making a List…
Lists with pretty icons next to them are all fine and well. But can we create ListView widgets whose rows contain interactive child widgets instead of just passive widgets like TextView and ImageView? For example, could we combine the RatingBar with text in order to allow people to scroll a list of, say, songs and rate them right inside the list?
There is good news and bad news.
The good news is that interactive widgets in rows work just fine. The bad news is that it is a little tricky, specifically when it comes to taking action when the interactive widget’s state changes (e.g., a value is typed into a field). We need to store that state somewhere, since our RatingBar widget will be recycled when the ListView is scrolled. We need to be able to set the RatingBar state based upon the actual word we are viewing as the RatingBar 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.