3

I'm working with lazy loading a Gallery of images (one class for the Gallery and Adapter, and another for the lazy loading part). The second class uses runOnUiThread to update the first class using its context, but it seems that it's calling the getView() method of the first class' adapter again. This means that getView() is being called twice for every image in the Gallery. Check out the code below.

The weird thing is that second getView() call is only being called on the selected image in the Gallery widget. If four images are shown at the same time, getView() will be called on those four images once, and three additional times on the selected image.

Any ideas why this is happening?

Here's the adapter

public class ImageAdapter extends BaseAdapter {

        public HorizontalImageLoader horImageLoader;

        public ImageAdapter() {
            horImageLoader = new HorizontalImageLoader(Main.this);
        }

        public int getCount() {
            return coverFileNames.size();
        }

        public Object getItem(int position) {
            return position;
        }

        public long getItemId(int position) {
            return position;
        }

        public View getView(int position, View convertView, ViewGroup parent) {

            ImageView imageView;

            if (convertView == null) {
                imageView = new ImageView(Main.this);
            } else {
                imageView = (ImageView) convertView;
            }

            // If I just use this, getView() is only called once per image (the correct way):
            // imageView.setImageResource(R.drawable.noposterxl);

            // If I just use this, getView() is only called once per
            // image, and additional times on the selected image (not correct):
            horImageLoader.DisplayImage(coverFileNames.get(position), imageView, position);

            return imageView;
        }

    }

Here's the HorizontalImageLoader class (based on this example)

public class HorizontalImageLoader {

    private Activity activity;
    private Map<ImageView, String> imageViews=Collections.synchronizedMap(new WeakHashMap<ImageView, String>());

    public HorizontalImageLoader(Activity activity) {
        this.activity = activity;
        photoLoaderThread.setPriority(Thread.NORM_PRIORITY-1);
    }

    public void DisplayImage(String fileUrl, ImageView imageView, final int pos) {
        imageViews.put(imageView, fileUrl);
        queuePhoto(fileUrl, activity, imageView, pos);
        imageView.setImageResource(R.drawable.noposterxl);
    }

    private void queuePhoto(String url, Activity activity, ImageView imageView, int position) {
        //This ImageView may be used for other images before. So there may be some old tasks in the queue. We need to discard them.
        photosQueue.Clean(imageView);
        PhotoToLoad p=new PhotoToLoad(url, imageView, position);
        synchronized(photosQueue.photosToLoad){
            photosQueue.photosToLoad.push(p);
            photosQueue.photosToLoad.notifyAll();
        }

        //start thread if it's not started yet
        if(photoLoaderThread.getState()==Thread.State.NEW)
            photoLoaderThread.start();
    }

    private Bitmap getBitmap(String fileUrl, int position) {

        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inPurgeable = true;

        Bitmap bm = BitmapFactory.decodeFile(fileUrl, options);

        return bm;
    }

    //Task for the queue
    private class PhotoToLoad
    {
        public String url;
        public ImageView imageView;
        public int pos;
        public PhotoToLoad(String u, ImageView i, int p){
            url=u;
            imageView=i;
            pos = p;
        }
    }

    PhotosQueue photosQueue=new PhotosQueue();

    public void stopThread()
    {
        photoLoaderThread.interrupt();
    }

    //stores list of photos to download
    class PhotosQueue
    {
        private Stack<PhotoToLoad> photosToLoad=new Stack<PhotoToLoad>();

        //removes all instances of this ImageView
        public void Clean(ImageView image)
        {
            try {
                for(int j=0 ;j<photosToLoad.size();){
                    if(photosToLoad.get(j).imageView==image)
                        photosToLoad.remove(j);
                    else
                        ++j;
                }
            } catch (Exception e) {
                // Do nothing
            }
        }
    }

    class PhotosLoader extends Thread {
        public void run() {
            try {
                while(true)
                {
                    //thread waits until there are any images to load in the queue
                    if(photosQueue.photosToLoad.size()==0)
                        synchronized(photosQueue.photosToLoad){
                            photosQueue.photosToLoad.wait();
                        }
                    if(photosQueue.photosToLoad.size()!=0)
                    {
                        PhotoToLoad photoToLoad;
                        synchronized(photosQueue.photosToLoad){
                            photoToLoad=photosQueue.photosToLoad.pop();
                        }
                        Bitmap bmp = getBitmap(photoToLoad.url, photoToLoad.pos);
                        String tag=imageViews.get(photoToLoad.imageView);

                        if(tag!=null && tag.equals(photoToLoad.url)){
                            BitmapDisplayer bd=new BitmapDisplayer(bmp, photoToLoad.imageView);
                            Activity a=(Activity)photoToLoad.imageView.getContext();
                            a.runOnUiThread(bd); // This seems to be causing the additional calls to getView()
                        }
                    }
                    if(Thread.interrupted())
                        break;
                }
            } catch (InterruptedException e) {
                //allow thread to exit
            }
        }
    }

    PhotosLoader photoLoaderThread=new PhotosLoader();

    //Used to display bitmap in the UI thread
    class BitmapDisplayer implements Runnable
    {
        Bitmap bitmap;
        ImageView imageView;
        public BitmapDisplayer(Bitmap b, ImageView i){
            bitmap=b;
            imageView=i;
        }
        public void run()
        {
            if(bitmap!=null)
                imageView.setImageBitmap(bitmap);
            else
                imageView.setImageResource(R.drawable.noposterxl);
        }
    }

}

UPDATED AGAIN

If I do the following in my getView() method, this is what LogCat says:

Code:

Log.d("POSITION", "Current position: " + position);
horImageLoader.DisplayImage(coverFileNames.get(position), imageView, position);

LogCat:

09-03 13:59:11.920: DEBUG/POSITION(15278): Current position: 1
09-03 13:59:11.960: DEBUG/POSITION(15278): Current position: 2
09-03 13:59:11.960: DEBUG/POSITION(15278): Current position: 3
09-03 13:59:11.960: DEBUG/POSITION(15278): Current position: 0
09-03 13:59:12.110: DEBUG/POSITION(15278): Current position: 1
09-03 13:59:12.240: DEBUG/POSITION(15278): Current position: 1
09-03 13:59:12.300: DEBUG/POSITION(15278): Current position: 1

(note the additional three log entries with "Current position: 1" at the bottom)

If I do this, then here's what LogCat says:

Code:

Log.d("POSITION", "Current position: " + position);
imageView.setImageResource(R.drawable.noposterxl);

LogCat:

09-03 14:02:47.340: DEBUG/POSITION(15412): Current position: 1
09-03 14:02:47.360: DEBUG/POSITION(15412): Current position: 2
09-03 14:02:47.370: DEBUG/POSITION(15412): Current position: 3
09-03 14:02:47.370: DEBUG/POSITION(15412): Current position: 0

(note that this is returning the correct result - only one call per image)

PS. My Gallery is set to select index 1 first, that's why position 1 is called first.

Michell Bak
  • 13,182
  • 11
  • 64
  • 121
  • Can you show some relevant code. – Ron Sep 03 '11 at 09:49
  • Try commenting out the line a.runOnUiThread(bd) and show us what the logcat is. Also try commenting out imageView.setImageBitmap(bitmap) and post the logcat too. – Sebastian Nowak Sep 03 '11 at 12:36
  • This is the result: 09-03 14:02:47.340: DEBUG/POSITION(15412): Current position: 1 09-03 14:02:47.360: DEBUG/POSITION(15412): Current position: 2 09-03 14:02:47.370: DEBUG/POSITION(15412): Current position: 3 09-03 14:02:47.370: DEBUG/POSITION(15412): Current position: 0 – Michell Bak Sep 03 '11 at 14:12
  • Just so you don't think I didn't try what you said: I did try it, it was just the same result as when setting an image resource, so I just copied that :) – Michell Bak Sep 03 '11 at 15:17

5 Answers5

2

Execution:

  1. public View getView(int position, View convertView, ViewGroup parent)
  2. DisplayImage(coverFileNames.get(position), imageView, position)

Bear in mind that if getView is contained in the execution of DisplayImage, You will have an infinite loop.

Since there is no infinite loop, then one of two scenarios can be true:

  1. DisplayImage executes something only ONCE and this something is trigerring getView ONCE
  2. something else is trigerring getView.

2 is not true, since the only statement executed in getView is DisplayImage

1 is true, DisplayImage executes one of the flavors of setImageDrawable once.

According to the source code of setImageDrawable View.requestLayout() and View.invalidate are called.

  • View.requestLayout() will call ViewParent Interface's requestLayout() which according to the docs:

    Called when something has changed which has invalidated the layout of a child of this view parent. This will schedule a layout pass of the view tree.

  • View.invalidate will call ViewParent Interface's invalidateChild which might cause an invalidation on the ViewParent itself.


Therefore, it is safe to say that setting the image of an ImageView will cause an invalidation to itself. (Which is really the case and does not need to be proven anyway)

Now Since this ImageView is a child of an Adapter interface, it is state of art that the Adapter's getView is called with the position of the exact view that contains this ImageView


Finally, Worry Not because getView in this case is equivelant to invalidate() and you surely want invalidate() to be called so that your images show up.

This is normal.

1 MORE THING

The bad performance of your application is not due to calling of the getView!

You should implement your DisplayImage correctly. Your class do not take into account any optimization. It is not the responsibility of the BaseAdapter.

Advice:

  1. I believe one big bottleneck is your queuePhoto function which is always calling the Clean function! You are cleaning all the time!
  2. Your key is an ImageView! and you are comparing ImageViews all the time in a loop

Just try to enhance your code and optimize it. You should account for getView being called multiple of times.

Sherif elKhatib
  • 45,786
  • 16
  • 89
  • 106
  • It is not normal in this case. It's not invalidating all the Imageviews twice, it's invalidating all once and the selected ImageView a lot of unneccessary times. This causes really annoying lag, and performance issues because it needs to load images an unnecessary amount of times. – Michell Bak Sep 06 '11 at 09:32
  • ok I added some edits at the answer. the lag is your fault not the adapter's fault – Sherif elKhatib Sep 06 '11 at 10:30
  • Commenting out the Clean function changes nothing. It is still calling getView() too many times on the selected ImageView. – Michell Bak Sep 06 '11 at 10:47
2

BitmapDisplayer is a Runnable that you run on the UI thread, and it calls methods on ImageView. These methods happen to generate layout requests, which will eventually cause getView() to be invoked.

Romain Guy
  • 97,993
  • 18
  • 219
  • 200
1

I can see no problem in the code. I think its normal for adapterview to call getview when a item is selcted. Here is a link to another similar question that will explain further.

custom listview adapter getView method being called multiple times, and in no coherent order

Update

Comment out the setImageResource line in DisplayImage and see if the number of calls to getView, for selected item, reduces to 2.

public void DisplayImage(String fileUrl, ImageView imageView, final int pos) {
    imageViews.put(imageView, fileUrl);
    queuePhoto(fileUrl, activity, imageView, pos);
 //   imageView.setImageResource(R.drawable.noposterxl); // coment this line
}
Community
  • 1
  • 1
Ron
  • 24,175
  • 8
  • 56
  • 97
  • The weird thing is that getview() is only being called when runOnUiThread is called. And what's even more weird is that it doesn't call getView() on all the images, only multiple times on the selected one. – Michell Bak Sep 03 '11 at 11:46
  • If I just create the ImageViews normally in the getView() method, the method will only be called the correct number of times. If I use the lazy loading class, it'll be called multiple times, which is slower and wrong. – Michell Bak Sep 03 '11 at 11:50
  • Sadly, no. It's still calling getView() the same exact number of times :-( – Michell Bak Sep 04 '11 at 09:57
1

This is not Weird issue, its normal to call getView method several times before showing the activity, this all about validating the views.

to test it try to create normal list with just text, you will see every view is called twice.

Mohammad Ersan
  • 12,304
  • 8
  • 54
  • 77
  • No, every view is called once if I do it any other way. I've even showed you that in the question. My Gallery widget is not set to wrap content, so there's no need for it to call the items several times. – Michell Bak Sep 06 '11 at 06:29
  • but i've tested it before, as i said, mmm, build a custom `ListView` then override validation methods and print in it how many time validation is called. – Mohammad Ersan Sep 06 '11 at 06:45
  • I'm not using a ListView, as stated several times in the original question. And I've also tested it a lot of times, and it works just fine with everything else, except this class for lazy loading. – Michell Bak Sep 06 '11 at 08:17
  • whatever you are using, gallery is similar to listView – Mohammad Ersan Sep 06 '11 at 08:27
  • Please explain why what you're saying doesn't happen then. I've got multiple Gallery widgets that are working beautifully and that don't call getView() on the selected item multiple times, but they're not using lazy load. The issue only occurs when using that lazy load class, and my issue is that I don't see what's causing it (besides the runOnUiThread call). Sorry, but what you're saying in this case is just inaccurate. – Michell Bak Sep 06 '11 at 08:38
  • am not inventing from my mind, am telling you what i experienced through apps am working on, on off them is using gallery with lazy and none lazy content, calling adapter.NotifyDataChanged() *may* cause this. – Mohammad Ersan Sep 06 '11 at 09:17
  • Would it be possible for you to send me your code to lazy load a gallery? – Michell Bak Sep 06 '11 at 09:33
  • Ah, it's OK. I've been working on my own lazy loading method, and think I've found something that works now :) Thanks a lot for your help. – Michell Bak Sep 06 '11 at 11:08
0

I think it is the problem with listview height. If you provide some fixed height instead of wrap_content or fill_parent for listview height then I think it will work. Once check that.

Ron
  • 24,175
  • 8
  • 56
  • 97
harish
  • 255
  • 1
  • 3
  • 13