I'm displaying a list of contacts (name + picture) using the ListView
. In order to make the initial load fast, I only load the names first, and defer picture loading. Now, whenever my background thread finishes loading a picture, it schedules my adapter's notifyDataSetChanged()
to be called on the UI thread. Unfortunately, when this happens the ListView
does not re-render (i.e. call getView()
for) the items that are already on-screen. Because of this, the user doesn't see the newly-loaded picture, unless they scroll away and back to the same set of items, so that the views get recycled. Some relevant bits of code:
private final Map<Long, Bitmap> avatars = new HashMap<Long, Bitmap>();
// this is called *on the UI thread* by the background thread
@Override
public void onAvatarLoaded(long contactId, Bitmap avatar) {
avatars.put(requestCode, avatar);
notifyDataSetChanged();
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// snip...
final Bitmap avatar = avatars.get(contact.id);
if (avatar != null) {
tag.avatar.setImageBitmap(avatar);
tag.avatar.setVisibility(View.VISIBLE);
tag.defaultAvatar.setVisibility(View.GONE);
} else {
tag.avatar.setVisibility(View.GONE);
tag.defaultAvatar.setVisibility(View.VISIBLE);
if (!avatars.containsKey(contact.id)) {
avatars.put(contact.id, null);
// schedule the picture to be loaded
avatarLoader.addContact(contact.id, contact.id);
}
}
}
AFAICT, if you assume that notifyDataSetChanged()
causes the on-screen items to be re-created, my code is correct. However, it seems that is not true, or maybe I'm missing something. How can I make this work smoothly?
Here I go answering my own question with a hackaround that I've settled on. Apparently, notifyDataSetChanged()
is only to be used if you are adding / removing items. If you are updating information about items that are already displayed, you might end up with visible items not updating their visual appearance (getView()
not being called on your adapter).
Furthermore, calling invalidateViews()
on the ListView
doesn't seem to work as advertised. I still get the same glitchy behavior with getView()
not being called to update on-screen items.
At first I thought the issue was caused by the frequency at which I called notifyDataSetChanged()
/ invalidateViews()
(very fast, due to updates coming from different sources). So I've tried throttling calls to these methods, but still to no avail.
I'm still not 100% sure this is the platform's fault, but the fact that my hackaround works seems to suggest so. So, without further ado, my hackaround consists in extending the ListView
to refresh visible items. Note that this only works if you're properly using the convertView
in your adapter and never returning a new View
when a convertView
was passed. For obvious reasons:
public class ProperListView extends ListView {
private static final String TAG = ProperListView.class.getName();
@SuppressWarnings("unused")
public ProperListView(Context context) {
super(context);
}
@SuppressWarnings("unused")
public ProperListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@SuppressWarnings("unused")
public ProperListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
class AdapterDataSetObserver extends DataSetObserver {
@Override
public void onChanged() {
super.onChanged();
refreshVisibleViews();
}
@Override
public void onInvalidated() {
super.onInvalidated();
refreshVisibleViews();
}
}
private DataSetObserver mDataSetObserver = new AdapterDataSetObserver();
private Adapter mAdapter;
@Override
public void setAdapter(ListAdapter adapter) {
super.setAdapter(adapter);
if (mAdapter != null) {
mAdapter.unregisterDataSetObserver(mDataSetObserver);
}
mAdapter = adapter;
mAdapter.registerDataSetObserver(mDataSetObserver);
}
void refreshVisibleViews() {
if (mAdapter != null) {
for (int i = getFirstVisiblePosition(); i <= getLastVisiblePosition(); i ++) {
final int dataPosition = i - getHeaderViewsCount();
final int childPosition = i - getFirstVisiblePosition();
if (dataPosition >= 0 && dataPosition < mAdapter.getCount()
&& getChildAt(childPosition) != null) {
Log.v(TAG, "Refreshing view (data=" + dataPosition + ",child=" + childPosition + ")");
mAdapter.getView(dataPosition, getChildAt(childPosition), this);
}
}
}
}
}
Add the following line to onResume()
listview.setAdapter(listview.getAdapter());
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With