问题
I have a custom adapter which extends ArrayAdapter, it implements the view holder patterns to show data (text + image) from a web service.
for lazy loading of the images I use the async tasks pattern from the advanced training in the Android's developers site,
I also use disk + ram bitmap cache.
when there is additional data to be retrieved I add a footer view that clicking on it retrieves additional data from the web service and add it to the adapter.
the problem is that when this new data is being added, some of the visible images are changing and immediately changing back, which result in a weird flickering.
other than that everything is working fine and the scrolling is smooth.
as far as I understand those image changes are happening when the visible views are being refreshed when new data is added.
is there a way to bypass this unwanted behavior ?
this is the class doing the downloading and managing the async tasks
public class ImageDownloader {
private ImageCache mCache;
private int reqWidth;
private int reqHeight;
public void download(String url, ImageView imageView, ImageCache imageCache, int reqHeight, int reqWidth) {
mCache = imageCache;
this.reqHeight = reqHeight;
this.reqWidth = reqWidth;
if (cancelPotentialDownload(url, imageView)) {
BitmapDownloaderTask task = new BitmapDownloaderTask(imageView);
DownloadedDrawable downloadedDrawable = new DownloadedDrawable(task);
imageView.setImageDrawable(downloadedDrawable);
task.execute(url);
}
}
private class BitmapDownloaderTask extends AsyncTask<String, Void, Bitmap> {
private String url;
private WeakReference<ImageView> imageViewReference;
public BitmapDownloaderTask(ImageView imageView) {
imageViewReference = new WeakReference<ImageView>(imageView);
}
@Override
protected Bitmap doInBackground(String... strings) {
Bitmap bitmap = null;
try {
bitmap = mCache.getBitmapFromURL(strings[0], reqWidth, reqHeight);
} catch (IOException e) {
}
return bitmap;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
if (isCancelled()) bitmap = null;
if (imageViewReference != null) {
ImageView imageView = imageViewReference.get();
BitmapDownloaderTask bitmapDownloaderTask = getBitmapDownloaderTask(imageView);
if (imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}
private static class DownloadedDrawable extends ColorDrawable {
private WeakReference<BitmapDownloaderTask> bitmapDownloaderTaskReference;
public DownloadedDrawable(BitmapDownloaderTask bitmapDownloaderTask) {
bitmapDownloaderTaskReference = new WeakReference<BitmapDownloaderTask>(bitmapDownloaderTask);
}
public BitmapDownloaderTask getBitmapDownloaderTask() {
return bitmapDownloaderTaskReference.get();
}
}
private static BitmapDownloaderTask getBitmapDownloaderTask(ImageView imageView) {
if (imageView != null) {
Drawable drawable = imageView.getDrawable();
if (drawable instanceof DownloadedDrawable) {
DownloadedDrawable downloadedDrawable = (DownloadedDrawable)drawable;
return downloadedDrawable.getBitmapDownloaderTask();
}
}
return null;
}
private static boolean cancelPotentialDownload(String url, ImageView imageView) {
BitmapDownloaderTask bitmapDownloaderTask = getBitmapDownloaderTask(imageView);
if (bitmapDownloaderTask != null) {
String bitmapUrl = bitmapDownloaderTask.url;
if ((bitmapUrl == null) || (!bitmapUrl.equals(url))) {
bitmapDownloaderTask.cancel(true);
} else {
return false;
}
}
return true;
}
}
this is the adapter:
private class PlaceAdapter extends ArrayAdapter { final int viewResourceId;
public PlaceAdapter(Context context, int textViewResourceId, List<PlaceModel> objects) {
super(context, textViewResourceId, objects);
viewResourceId = textViewResourceId;
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
LayoutInflater inflater = getLayoutInflater();
convertView = inflater.inflate(viewResourceId, null);
holder = new ViewHolder(convertView);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
PlaceModel place = getItem(position);
holder.name.setText(place.getName());
holder.address.setText(place.getVicinity());
holder.position = position;
if (place.getIcon() != null) {
String url = mImageViewUrls.get(holder.image);
if (url == null || (url != null && !url.equals(place.getIcon()))) {
mDownloader.download(place.getIcon(), holder.image, mCache, 100, 100);
mImageViewUrls.put(holder.image, place.getIcon());
}
} else {
holder.image.setImageBitmap(null);
mImageViewUrls.remove(holder.image);
}
return convertView;
}
}
private class ViewHolder {
public final ImageView image;
public final TextView name;
public final TextView address;
public int position;
public ViewHolder(View row) {
image = (ImageView) row.findViewById(R.id.placeRow_imageView);
name = (TextView) row.findViewById(R.id.placeRow_placeName);
address = (TextView) row.findViewById(R.id.placeRow_placeAddress);
}
}
mImageViewUrls
is a WeakHashMap<ImageView, String>
that maps between an ImageView
and a url, so redundant async tasks invocations can be reduced by checking if the ImageView
is already showing the required image. without this implementation, flickering is happening in all visible images on data change. with this, it happens only with some images.
EDIT: I tried to eliminate possible causes of this issue, first I tried to completely bypass the cache implementation and download each bitmap from the network, then I tried wrapping my adapter with CommonsWare's Endless adapter and with both I got the same result.. so this leaves only the ImageDownloader class and my adapter as possible causes.. I'm completely lost on this.
回答1:
The reason for this flicker is that, in listview list items are reused. When re-used, the imageviews in the list item retains the old image reference which is displayed first. Later on once new image is downloaded, it starts to show. this causes the flickering behavior. To avoid this flickering issue, always clear the old image reference from the imageview when it is getting reused.
In your case, add holder.image.setImageBitmap(null); after holder = (ViewHolder) convertView.getTag();
So, your getView() method will look like:
@Override public View getView(final int position, View convertView, ViewGroup parent) {
...
if (convertView == null) {
LayoutInflater inflater = getLayoutInflater();
convertView = inflater.inflate(viewResourceId, null);
holder = new ViewHolder(convertView);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
holder.image.setImageBitmap(null)
}
...
return convertView;
}
回答2:
Edit : I had similar issue, the images used to change rapidly. This happens because
getView() method of List adapter is called several times. every time getView() is called, you try to download and set the image on the row. By the time image is downloaded from the network, row in the list which requested for that image might have moved out of visible portion of screen but the adapter tries to reuse that row and this leads to setting previously requested image on new row at that position(previously requested row-position).
Try this approach in the adapter, set the requested URL on the ImageView with setTag :
if(item.get_referenceImage().length()!=0){
//with view, hold the url. This is used to verify that right image is loaded on download completion
holder.refImageView.setTag(item.get_referenceImage());
imageManager.loadImage(new MetaItem(item.get_referenceImage(), holder.refImageView));
}else{
//no image, set default
Log.d(TAG, "No images found for the view setting default image");
holder.refImageView.setTag(null); //this is req, otherwise image cache will retain previous image
holder.refImageView.setImageResource(R.drawable.blank_96_1382x);
}
in cache, before loading the bitmap, check if right image is loaded:
String url = (String) m_imageViewRef.get().getTag();
if(url != null && url.compareTo(m_metaItem.m_url) == 0 ){
m_metaItem.m_imageViewRef.get().setImageBitmap(result);
}
PS: MetaItem is just a POJO to hold reference to imageView and image's URL
回答3:
The solution is to not reload your image when it did not change.
In your adapters getView() do:
// schedule rendering:
final String path = ... (set path here);
if (holder.lastImageUrl == null || !holder.lastImageUrl.equals(path)
|| holder.headerImageView.getDrawable() == null) {
// refresh image
imageLoader.displayImage(imageUri, imageAware);
} else {
// do nothing, image did not change and does not need to be updated
}
on success (add a ImageLoadingListener) you set holder.lastImageUrl = path, on fail and cancel you set holder.lastImageUrl to null so that it will reload next time.
回答4:
You might be calling adapter.notifyDatasetChanged()
which causes listview
to reload its listitems.
try not calling adapter.notifyDatasetChanged()
your listview should automatically update when you scroll.
but this can cause ArrayIndexOutOfBounds
exception while scrolling.
来源:https://stackoverflow.com/questions/13322575/android-visible-listview-images-flicker-when-adding-data-to-arrayadapter