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:
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:
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; } }
You're a life saver! :D Thank you so much!!! I had this problem for a long long time!
ReplyDeleteCan you test it if you use LayoutInflater? I think your LayoutParam creates this bug. I just test it without LayoutParam and they're OK.
ReplyDeleteSorry I don't understand what you mean - I am using LayoutInflater as you can see.
DeleteHow can setLayoutParams() affect this issue?
Please can you clarify what you mean?
Thanks to your hint I managed to fix the problem. No idea why this fixes it though!
DeletemImageViewLayoutParams 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.
I ran into this ClassCastException issue because I was changing the return value of getViewTypeCount at runtime. Obviously ListView doesn't like this.
ReplyDeleteSo its a good idea to always return the maximum number of possible viewTypes, even if not all are used at a specific time.