laggy Listview with ImageView and Executor Framework

不问归期 提交于 2019-12-11 11:22:01

问题


I have a ListView with custom items, like this one:

The grey square is an ImageView. The data to fill the ListView comes from a database in the form of a Cursor. But the images are not directly stored in the database, but in the SDCard, the database only holds a String reference to them.

In the beginning I was decoding the Image into a Bitmap from the overriden CursorAdapter's bindView() callback method:

 Bitmap bmp = BitmapFactory.decodeFile(imageLocation);
 holder.imageHolder.setImageBitmap(bmp);

But the ListView Scrolling was very laggy. So i read about Executor framework and implemented it, replacing the previous code with the following:

 ImageView imageView = holder.imageHolder;
 asyncImageLoader.DisplayImage(imageLocation, imageView);

And creating the AsyncImageLoader class. Which creates, in its constructor, a thread pool with maximum of 5 worker threads to take care of the Runnables sent to the work queue. Then, when I call the DisplayImage() method from my custom CursorAdapter, it checks if the location String contains a url. If it does, an ImageLoader Runnable is sent to the thread pool's work queue. If the location contains "N/A", a default image is set to the ImageView.

When an available worker thread takes care of the ImageLoader Runnable, the image in the SDCard is decoded into a Bitmap, and a ImageDisplayer Runnable is sent to the Main Thread's message queue, to show the image in the UI:

public class AsyncImageLoader {

ExecutorService executorService;
Handler handler = new Handler();

public AsyncImageLoader() {
    this.executorService = Executors.newFixedThreadPool(5);
}

public void DisplayImage(String location, ImageView imageView) {
    if(!location.matches("N/A")) {
        queueImageDecoding(location, imageView);
    } else {
        imageView.setImageDrawable(imageView.getContext().getResources().getDrawable(R.drawable.not_available));
    }
}

private void queueImageDecoding(String location, ImageView imageView) {
    executorService.execute(new ImageLoader(location, imageView));
}

class ImageLoader implements Runnable {

    private String location;
    private ImageView imageView;

    public ImageLoader(String location, ImageView imageView) {
        this.location = location;
        this.imageView = imageView;
    }

    @Override
    public void run() {
        Bitmap bmp = BitmapFactory.decodeFile(location);
        handler.post(new ImageDisplayer(bmp, imageView));
    }

}

class ImageDisplayer implements Runnable {

    private Bitmap bitmap;
    private ImageView imageView;

    public ImageDisplayer(Bitmap bitmap, ImageView imageView) {
        this.bitmap =  bitmap;
        this.imageView = imageView;
    }

    @Override
    public void run() {
        if(bitmap != null) {
            imageView.setImageBitmap(bitmap);
        }
    }

}

}

The problem is that I am still getting the Laggy scrolling. If I get rid of the code inside ImageLoader.run() method, the scrolling is perfect. Isn't that code supposed to be processed in a worker thread? What am i missing here?

UPDATE

Since the Views in the ListView are reused when the scrolling happens, the Bitmaps returned from the worker thread, are set several times in a single ImageView. So the possible solutions are:

  • To avoid setting the old Bitmap when the ListView item has already been reused.
  • Or even better, cancel the task.

I am cancelling the tasks using a Future object. Which is stored in the holder tagged to the item View inside the custom CursorAdapter:

public class MyCustomAdapter extends CursorAdapter {

...
public AsyncImageLoader asyncImageLoader; 

private static class ViewHolder {
    ImageView imageHolder;
    TextView text1Holder;
    TextView text2Holder;
    TextView text3Holder;
    Button buttonHolder;
    Future<?> futureHolder;
}

public MyCustomAdapter(Context context, Cursor c, int flags) {
    ...
    this.asyncImageLoader = new AsyncImageLoader();
}

@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
    ...
    return view;
}

@Override
public void bindView(View view, Context context, Cursor cursor) {

    ViewHolder holder = (ViewHolder)view.getTag();

    String location = ...;
    ImageView imageView = holder.imageHolder;
    if(holder.futureHolder == null) {
        holder.futureHolder = asyncImageLoader.DisplayImage(location, imageView);
    } else {
        if(!holder.futureHolder.isDone()) 
            holder.futureHolder.cancel(true);
        holder.futureHolder = asyncImageLoader.DisplayImage(location, imageView);
    }
    ...
}   

}   

Each time an item view is reused, I check if the holder's future object isDone(). If it is not, I cancel the task with Future.cancel(true). But now, the problem is that the tasks complete too fast to be cancelled. if I put the worker thread to sleep, for let's say 1 second, then the task lasts long enough to be cancelled and the ListView scrolling works better. But i have to wait 1 second for the images to appear and I don't want that.

public class AsyncImageLoader {
    ....
    public Future<?> DisplayImage(String location, ImageView imageView) {
        if(!location.matches("N/A")) {
            return executorService.submit(new ImageLoader(location, imageView));
        } else {
            imageView.setImageDrawable(imageView.getContext().getResources().getDrawable(R.drawable.not_available));
            return null;
        }
    }

    class ImageLoader implements Runnable {

        private String location;
        private ImageView imageView;

        public ImageLoader(String location, ImageView imageView) {
            this.location = location;
            this.imageView = imageView;
        }

        @Override
        public void run() {
            boolean interrupted = false;
            try {
                if(!Thread.currentThread().isInterrupted()) {
                    Thread.sleep(1000);
                    Bitmap bmp = BitmapFactory.decodeFile(location);
                    handler.post(new ImageDisplayer(bmp, imageView));
                }
            } catch (InterruptedException consumed) {
                interrupted = true;
            } finally {
                if(interrupted)
                    Thread.currentThread().interrupt();
            }
        }

    }
    ...
}

The second solution would be to let the tasks complete, but prevent setting the old Bitmap when the ListView item has already been reused. But i can't figure out how to do it. Any suggestions?


回答1:


Ok, originally I get the images from a Web Service, and store them in the SDCard. From the samples I downloaded, I trusted the service was returning all the images with the same dimensions. WRONG! some of them are bigger than expected and were causing the lag when were set in the ImageView. I just had to scale them down. Load a Scaled Bitmap Version into Memory



来源:https://stackoverflow.com/questions/23050212/laggy-listview-with-imageview-and-executor-framework

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!