Paul
Paul

Reputation: 1734

Android listview images shift around

I've implemented the below code to fill my listview with various images if available and it works but the problem is when other elements on screen are touched or you scroll fast images shift around. Any help would be much appreciated...

import java.lang.ref.WeakReference;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.view.View;
import android.widget.ImageView;
import android.widget.TableLayout;

public class ImageDownloader {

    public void download(String url, ImageView imageView, TableLayout imageTable) {
        if (cancelPotentialDownload(url, imageView)) {
        BitmapDownloaderTask task = new BitmapDownloaderTask(imageView, imageTable);
        DownloadedDrawable downloadedDrawable = new DownloadedDrawable(task);
        imageView.setImageDrawable(downloadedDrawable);
        task.execute(url);
        }
    }

    class BitmapDownloaderTask extends AsyncTask<String, Void, Bitmap> {
        String url;
        private final WeakReference<ImageView> imageViewReference;
        private final WeakReference<TableLayout> imageTableReference;

        public BitmapDownloaderTask(ImageView imageView, TableLayout imageTable) {
            imageViewReference = new WeakReference<ImageView>(imageView);
            imageTableReference = new WeakReference<TableLayout>(imageTable);
        }

          @Override
          protected Bitmap doInBackground(String... params) {
                 BitmapFactory.Options o = new BitmapFactory.Options();
                  o.inJustDecodeBounds = true;
                  BitmapFactory.decodeFile(params[0], o);
                  final int REQUIRED_SIZE=70;

                  //Find the correct scale value. It should be the power of 2.
                  int width_tmp=o.outWidth, height_tmp=o.outHeight;
                  int scale=4;
                  while(true){
                      if(width_tmp/2<REQUIRED_SIZE || height_tmp/2<REQUIRED_SIZE)
                          break;
                      width_tmp/=2;
                      height_tmp/=2;
                      scale++;
                  }
                  //Decode with inSampleSize
                  BitmapFactory.Options o2 = new BitmapFactory.Options();
                  o2.inSampleSize=scale;       
                  return BitmapFactory.decodeFile(params[0], o2);
          }

          @Override
          protected void onPostExecute(Bitmap result) {
                if (isCancelled()) {
                    result = null;
                }

                if (imageViewReference != null) {
                    ImageView imageView = imageViewReference.get();
                    TableLayout imageTable = imageTableReference.get();
                    BitmapDownloaderTask bitmapDownloaderTask = ImageDownloader.getBitmapDownloaderTask(imageView);
                    // Change bitmap only if this process is still associated with it
                    if (this == bitmapDownloaderTask) {
                          imageView.setImageBitmap(result);
                          imageView.setVisibility(View.VISIBLE);
                          imageTable.setVisibility(View.VISIBLE);
                    }              
                }
            }
    }

    static class DownloadedDrawable extends ColorDrawable {
        private final WeakReference<BitmapDownloaderTask> bitmapDownloaderTaskReference;

        public DownloadedDrawable(BitmapDownloaderTask bitmapDownloaderTask) {
            super(Color.BLACK);
            bitmapDownloaderTaskReference =
                new WeakReference<BitmapDownloaderTask>(bitmapDownloaderTask);
        }

        public BitmapDownloaderTask getBitmapDownloaderTask() {
            return bitmapDownloaderTaskReference.get();
        }
    }

    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 {
                // The same URL is already being downloaded.
                return false;
            }
        }
        return true;
    }

    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;
    }
}

Upvotes: 2

Views: 1500

Answers (2)

KeenMonk
KeenMonk

Reputation: 432

As Loic mentioned, the basic issue is that ListView recycles the View objects used to render list rows. Because of this, the ImageView reference that you pass to the download() method can get re-used for a different "position" in the list, which leads to the behaviour you are observing.

The way I solved this was to have the Download task take the List Adapter rather than the ImageView as a parameter, and have it call notifyDataSetChanged() on the adapter when the download is complete. The Download task adds the image drawable to a cache, and the getView() implementation of your Adapter can simply get the Drawable from the cache to set the ImageView.

I've described the approach and posted the code here: http://mobrite.com/lazy-loading-images-in-a-listview/

Upvotes: 1

Lo&#239;c Faure-Lacroix
Lo&#239;c Faure-Lacroix

Reputation: 13600

Okay you're problem is quite simple and complicated at the same time. In short, ListView recycles views. If you use recycled views it will set the bitmap to an existing or new imageView. in other words when your bitmap is downloaded, it will be set to an image view that might not correspond to the item you're showing.

In my application, I sets a tag to my imageview. When the download is completed, I do a listview.findViewByTag(tag) if the view isn't found I just don't set the bitmap. if it is found I set it.

In getView, if the Bitmap exists in memory I set the bitmap to my image view. If the bitmap isn't in memory I start a background job and I set a tag to my view for that url.

That job will load a bitmap from disk if present or from internet.

Once the bitmap is present, I call a callback on my activity with a requestCode, a tag and bitmap.

In your case a tag and a bitmap should be enough.

You then have to do findViewByTag on your listview. If you find a view, you set the bitmap, if you don't find a view, forget it, when getView will be called once that items shows up, the bitmap should be present in memory or on disk.

So you cannot expect the views in your listview to be the ones you had when you first showed your items. I'd also say that if you start the background thread, you should set your image drawable to null so you won't see bitmaps of recycled views.

Upvotes: 1

Related Questions