Thursday, 18 October 2012

Recycling of views with Heterogeneous List Adapters

UPDATE: Issue now fixed in cleaner way, please see comments below.

I ran into an interesting 'quirk' or as I would call it a 'bug' in the way that Android recycles views in GridView (and presumably the same issue applies to ListView too).

Say you have an Adaptor which supplies two different views - one is a simple ImageView and one is a more complex layout inflated from xml. The android sdk docs and examples suggest that you can override getItemViewType() and getViewTypeCount() in order that android will sent the correct type of view to your getView() method for recycling. Like so:
private class ImageAdapter extends BaseAdapter {

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

    @Override
    public int getItemViewType(int position) {
        return getDirItem(position).isDir()? TYPE_DIR : TYPE_PIC;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup container) {
        if (getItemViewType(position) == TYPE_DIR) { // Folder
            if (convertView == null) { 
                LayoutInflater vi = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
                convertView = vi.inflate(R.layout.folder_grid_entry, container, false);
            }

            convertView.setLayoutParams(mImageViewLayoutParams);
                
            // Populate view here ...            

            return convertView;
        } else { // Pic
            ImageView imageView;
            if (convertView == null) { 
                imageView = new ImageView(mContext);
                imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
            } else {
                imageView = (ImageView)convertView;
            }

            imageView.setLayoutParams(mImageViewLayoutParams);

            // load the image here (in a background thread) ...

            return imageView;
        }
    }

However, this doesn't work. Android will occasionally send the wrong type of view in the convertView parameter to getView() resulting in some strange errors. It seems that the whole view type business is no guarantee that getView() will receive the right view type, at best Android treats view type as a 'hint'.

To be fair the Android docs here do say:

"You should check that this view is non-null and of an appropriate type before using. If it is not possible to convert this view to display the correct data, this method can create a new view."

However, in the very next sentence they go on to say:

"Heterogeneous lists can specify their number of view types, so that this View is always of the right type (see getViewTypeCount() and getItemViewType(int))

It appears the the documentation is wrong in this regard, the view is not always of the right type and it is essential to check this like so:

public View getView(int position, View convertView, ViewGroup container) {
    if (getItemViewType(position) == TYPE_DIR) { // Folder
        if ((convertView == null) || (convertView.getClass() != LinearLayout.class)) { 
            LayoutInflater vi = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView = vi.inflate(R.layout.folder_grid_entry, container, false);
        }

        convertView.setLayoutParams(mImageViewLayoutParams);
                
        // Populate view here ...            

        return convertView;
    } else { // Pic
        ImageView imageView;
        if ((convertView == null) || (convertView.getClass() != ImageView.class)) {
            imageView = new ImageView(mContext);
            imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
        } else {
            imageView = (ImageView)convertView;
        }

        imageView.setLayoutParams(mImageViewLayoutParams);

        // load the image here (in a background thread) ...

        return imageView;
    }
}

5 comments:

  1. You're a life saver! :D Thank you so much!!! I had this problem for a long long time!

    ReplyDelete
  2. Can you test it if you use LayoutInflater? I think your LayoutParam creates this bug. I just test it without LayoutParam and they're OK.

    ReplyDelete
    Replies
    1. Sorry I don't understand what you mean - I am using LayoutInflater as you can see.
      How can setLayoutParams() affect this issue?

      Please can you clarify what you mean?

      Delete
    2. Thanks to your hint I managed to fix the problem. No idea why this fixes it though!

      mImageViewLayoutParams was type GridView.LayoutParams. Makes sense because that is the type of the parent view right?
      However although this works fine for the ImageView, it turns out that it causes the strange behaviour observed in this post if you use it for the LinearLayout (same behaviour observed with RelativeLayout).

      The solution was to change the type of mImageViewLayoutParams to ViewGroup.LayoutParams.

      I have no idea why this should cause this problem and why this fix works!

      If anyone can enlighten me I would be very grateful.

      Delete
  3. I ran into this ClassCastException issue because I was changing the return value of getViewTypeCount at runtime. Obviously ListView doesn't like this.

    So its a good idea to always return the maximum number of possible viewTypes, even if not all are used at a specific time.

    ReplyDelete