The following is the first few sections of a chapter from The Busy Coder's Guide to Android Development, plus headings for the remaining major sections, to give you an idea about the content of the chapter.


Advanced ListViews

The humble ListView is the backbone of many an Android application. On phone-sized screens, the screen may be dominated by a single ListView, to allow the user to choose something to examine in more detail (e.g., pick a contact). On larger screens, the ListView may be shown side-by-side with the details of the selected item, to minimize the “pogo stick” effect seen on phones as users bounce back and forth between the list and the details.

While we have covered the basics of ListView in the core chapters of this book, there is a lot more that you can do if you so choose, to make your lists that much more interesting — this chapter will cover some of these techniques.

Prerequisites

Understanding this chapter requires that you have read the core chapters, particularly the one on Adapter and AdapterView.

Multiple Row Types, and Self Inflation

When we originally looked at ListView, we had all of our rows come from a common layout. Hence, while the data in each row would vary, the row structure itself would be consistent for all rows. This is very easy to set up, but it is not always what you want. Sometimes, you want a mix of row structures, such as header rows versus detail rows, or detail rows that vary a bit in structure based on the data:

ListView with Row Structure Mix (image courtesy of Google)
Figure 474: ListView with Row Structure Mix (image courtesy of Google)

Here, we see some header rows (e.g., “SINGLE LINE LIST”) along with detail rows. While the detail rows visually vary a bit, they might still be all inflated from the same layout, simply making some pieces (second line of text, thumbnail, etc.) visible or invisible as needed. However, the header rows are sufficiently visually distinct that they really ought to come from separate layouts.

The good news is that Android supports multiple row types. However, this comes at a cost: you will need to handle the row creation yourself, rather than chaining to the superclass.

Our sample project, Selection/HeaderDetailList will demonstrate this, along with showing how you can create your own custom adapter straight from BaseAdapter, for data models that do not quite line up with what Android supports natively.

Our Data Model and Planned UI

The HeaderDetailList project is based on the ViewHolderDemo project from the chapter on ListView. However, this time, we have our list of 25 Latin words broken down into five groups of five, as seen in the HeaderDetailList activity:

  private static final 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" } };

We want to display a header row for each batch:

HeaderDetailList, on Android 4.0.3
Figure 475: HeaderDetailList, on Android 4.0.3

The Basic BaseAdapter

Once again, we have a custom ListAdapter named IconicAdapter. However, this time, instead of inheriting from ArrayAdapter, or even CursorAdapter, we are inheriting from BaseAdapter. As the name suggests, BaseAdapter is a basic implementation of the ListAdapter interface, with stock implementations of many of the ListAdapter methods. However, BaseAdapter is abstract, and so there are a few methods that we need to implement:

    @Override
    public int getCount() {
      int count=0;

      for (String[] batch : items) {
        count+=1 + batch.length;
      }

      return(count);
    }

    @Override
    public Object getItem(int position) {
      int offset=position;
      int batchIndex=0;

      for (String[] batch : items) {
        if (offset == 0) {
          return(Integer.valueOf(batchIndex));
        }

        offset--;

        if (offset < batch.length) {
          return(batch[offset]);
        }

        offset-=batch.length;
        batchIndex++;
      }

      throw new IllegalArgumentException("Invalid position: "
          + String.valueOf(position));
    }

    @Override
    public long getItemId(int position) {
      return(position);
    }

Requesting Multiple Row Types

The methods listed above are the abstract ones that you have no choice but to implement yourself. Anything else on the ListAdapter interface that you wish to override you can, to replace the stub implementation supplied by BaseAdapter.

If you wish to have more than one type of row, there are two such methods that you will wish to override:

    @Override
    public int getViewTypeCount() {
      return(2);
    }

    @Override
    public int getItemViewType(int position) {
      if (getItem(position) instanceof Integer) {
        return(0);
      }

      return(1);
    }

The reason for supplying this information is for row recycling. The View that is passed into getView() is either null or a row that we had previously created that has scrolled off the screen. By passing us this now-unused View, Android is asking us to reuse it if possible. By specifying the row type for each position, Android will ensure that it hands us the right type of row for recycling — we will not be passed in a header row to recycle when we need to be returning a detail row, for example.

Creating and Recycling the Rows

Our getView() implementation, then, needs to have two key enhancements over previous versions:

  1. We need to create the rows ourselves, particularly using the appropriate layout for the required row type (header or detail)
  2. We need to recycle the rows when they are provided, as this has a major impact on the scrolling speed of our ListView

To help simplify the logic, we will have getView() focus on the detail rows, with a separate getHeaderView() to create/recycle and populate the header rows. Our getView() determines up front whether the row required is a header and, if so, delegates the work to getHeaderView():

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
      if (getItemViewType(position) == 0) {
        return(getHeaderView(position, convertView, parent));
      }

      View row=convertView;

      if (row == null) {
        row=getLayoutInflater().inflate(R.layout.row, parent, false);
      }

      ViewHolder holder=(ViewHolder)row.getTag();

      if (holder == null) {
        holder=new ViewHolder(row);
        row.setTag(holder);
      }

      String word=(String)getItem(position);

      if (word.length() > 4) {
        holder.icon.setImageResource(R.drawable.delete);
      }
      else {
        holder.icon.setImageResource(R.drawable.ok);
      }

      holder.label.setText(word);
      holder.size.setText(String.format(getString(R.string.size_template),
                                        word.length()));

      return(row);
    }

Assuming that we are to create a detail row, we then check to see if we were passed in a non-null View. If we were passed in null, we cannot recycle that row, so we have to inflate a new one via a call to inflate() on a LayoutInflater we get via getLayoutInflater(). But, if we were passed in an actual View to recycle, we can skip this step.

From here, the getView() implementation is largely the way it was before, including dealing with the ViewHolder. The only change of significance is that we have to manage the label TextView ourselves — before, we chained to the superclass and let ArrayAdapter handle that. So our ViewHolder now has a label data member with our label TextView, and we fill it in along with the size and icon. Also, we use getItem() to retrieve our Latin word, so it can find the right word for the given position out of our various word batches.

Our getHeaderView() does much the same thing, except it uses getItem() to retrieve our batch index, and we use that for constructing our header:

    private View getHeaderView(int position, View convertView,
                               ViewGroup parent) {
      View row=convertView;

      if (row == null) {
        row=getLayoutInflater().inflate(R.layout.header, parent, false);
      }

      Integer batchIndex=(Integer)getItem(position);
      TextView label=(TextView)row.findViewById(R.id.label);

      label.setText(String.format(getString(R.string.batch),
                                  1 + batchIndex.intValue()));

      return(row);
    }

Choice Modes and the Activated Style

The preview of this section is in the process of being translated from its native Klingon.

Custom Mutable Row Contents

The preview of this section is off trying to sweet-talk the Khaleesi into providing us with a dragon.

From Head To Toe

The preview of this section will not appear here for a while, due to a time machine mishap.

Enter RecyclerView

The preview of this section was fed to a gremlin, after midnight.