ListView Optimisations : Part 2 (Displaying Images in Your Lists)

This second post about listview optimisations will talk about the images in your lists. This post is an extension of the first, which one was about the viewholder :

I.    The ImageLoader

We will create a class which will do all the job (here is the skeleton):

1
2
3
4
5
6
public class ImageLoader {
  public ImageLoader(Context context) {
  }
  public void displayImage(String url, ImageView imageView) {
  }
}

And how to use it in your adapter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class MyAdapter extends BaseAdapter {

        private LayoutInflater mLayoutInflater;
        private List mData;
        private ImageLoader mImageLoader;

        public MyAdapter(Context context, List data){
            mData = data;
            mLayoutInflater = LayoutInflater.from(context);
            mImageLoader = new ImageLoader(context);
        }

        @Override
        public int getCount() {
            return mData == null ?  : mData.size();
        }

        @Override
        public MyPojo getItem(int position) {
            return mData.get(position);
        }

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

        @Override
        public View getView(int position, View view, ViewGroup parent) {

            View vi = view;
            ViewHolder holder = null;

            if (vi == null) {
                vi = mLayoutInflater.inflate(R.layout.item, parent, false);
                holder = new ViewHolder();

                holder.ivIcon = (ImageView) vi.findViewById(R.id.imageView_item_icon);
                vi.setTag(holder);
            } else {
                holder = (ViewHolder) vi.getTag();
            }

            MyPojo item = getItem(position);

            mImageLoader.displayImage(item.getIcon(), holder.icon);

            return vi;
        }

        static class ViewHolder{
            ImageView ivIcon;
        }
    }

A simple picture to understand the role of the ImageLoader:

And his code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
public class ImageLoader {

        private LruCache memoryCache;
        private FileCache fileCache;
        private Map imageViews = Collections.synchronizedMap(new WeakHashMap());

        private Drawable mStubDrawable;

        public ImageLoader(Context context) {
            fileCache = new FileCache(context);
            init(context);
        }

        private void init(Context context) {
            // Get memory class of this device, exceeding this amount will throw an
            // OutOfMemory exception.
            final int memClass = ((ActivityManager) context.getSystemService(
                    Context.ACTIVITY_SERVICE)).getMemoryClass();
            // 1/8 of the available mem
            final int cacheSize = 1024 * 1024 * memClass / 8;
            memoryCache = new LruCache(cacheSize);

            mStubDrawable = context.getResources().getDrawable(R.drawable.default_icon);
        }

        public void displayImage(String url, ImageView imageView) {
            imageViews.put(imageView, url);
            Bitmap bitmap = null;
            if (url != null && url.length() > )
                bitmap = (Bitmap) memoryCache.get(url);
            if (bitmap != null) {
                //the image is in the LRU Cache, we can use it directly
                imageView.setImageBitmap(bitmap);
            } else {
                //the image is not in the LRU Cache
                //set a default drawable a search the image
                imageView.setImageDrawable(mStubDrawable);
                if (url != null && url.length() > )
                    queuePhoto(url, imageView);
            }
        }

        private void queuePhoto(String url, ImageView imageView) {
                new LoadBitmapTask().execute(url, imageView);
        }

        /**
         * Search for the image in the device, then in the web
         * @param url
         * @return
         */
        private Bitmap getBitmap(String url) {
            Bitmap ret = null;
            //from SD cache
            File f = fileCache.getFile(url);
            if (f.exists()) {
                ret = decodeFile(f);
                if (ret != null)
                    return ret;
            }

            //from web
            try {
                //your requester will fetch the bitmap from the web and store it in the phone using the fileCache
                ret = MyRequester.getBitmapFromWebAndStoreItInThePhone(url);    // your own requester here
                return ret;
            } catch ([Exception][8] ex) {
                ex.printStackTrace();
                return null;
            }
        }

        //decodes image and scales it to reduce memory consumption
        private Bitmap decodeFile(File f) {
            Bitmap ret = null;
            try {
                [FileInputStream][9] is = new [FileInputStream][9](f);
                ret = BitmapFactory.decodeStream(is, null, null);
            } catch ([FileNotFoundException][10] e) {
                e.printStackTrace();
            } catch ([Exception][8] e) {
                e.printStackTrace();
            }
            return ret;
        }

        private class PhotoToLoad {
            public String url;
            public ImageView imageView;

            public PhotoToLoad(String u, ImageView i) {
                url = u;
                imageView = i;
            }
        }

        private boolean imageViewReused(PhotoToLoad photoToLoad) {
            //tag used here
            String tag = imageViews.get(photoToLoad.imageView);
            if (tag == null || !tag.equals(photoToLoad.url))
                return true;
            return false;
        }

        class LoadBitmapTask extends AsyncTask {
            private PhotoToLoad mPhoto;

            @Override
            protected TransitionDrawable doInBackground([Object][11]... params) {
                mPhoto = new PhotoToLoad((String) params[], (ImageView) params[1]);

                if (imageViewReused(mPhoto))
                    return null;
                Bitmap bmp = getBitmap(mPhoto.url);
                if (bmp == null)
                    return null;
                memoryCache.put(mPhoto.url, bmp);

                // TransitionDrawable let you to make a crossfade animation between 2 drawables
                // It increase the sensation of smoothness
                TransitionDrawable td = null;
                if (bmp != null) {
                    Drawable[] drawables = new Drawable[2];
                    drawables[] = mStubDrawable;
                    drawables[1] = new BitmapDrawable(Application.getRessources(), bmp);
                    td = new TransitionDrawable(drawables);
                    td.setCrossFadeEnabled(true); //important if you have transparent bitmaps
                }

                return td;
            }

            @Override
            protected void onPostExecute(TransitionDrawable td) {
                if (imageViewReused(mPhoto)) {
                    //imageview reused, just return
                    return;
                }
                if (td != null) {
                    // bitmap found, display it !
                    mPhoto.imageView.setImageBitmap(drawable);
                    mPhoto.imageView.setVisibility(View.VISIBLE);

                    //a little crossfade
                    td.startTransition(200);
                } else {
                    //bitmap not found, display the default drawable
                    mPhoto.imageView.setImageDrawable(mStubDrawable);
                }
            }
        }
    }

FileCache:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class FileCache {
    private File cacheDir;

    public FileCache(Context context) {
        this(context, );
    }

    public FileCache(Context context, long evt) {
        //Find the dir to save cached images
        cacheDir = context.getCacheDir();
        if (!cacheDir.exists())
            cacheDir.mkdirs();
    }

    public File getFile(String url) {
        return new File(cacheDir, String.valueOf(url.hashCode()));
    }

    public void clear() {
        File[] files = cacheDir.listFiles();
        if (files == null)
            return;
        for (File f : files)
            f.delete();
    }
}

Note 1: The crossfade is done with a TransitionDrawable, which give the feeling that your list scroll smoother!

Note 2: Try to avoid the setImageResource(ind resId) as much as possible and create a default drawable when you init your imageLoader. Indeed, according to the DOC,

This does Bitmap reading and decoding on the UI thread, which can cause a latency hiccup. If that’s a concern, consider using setImageDrawable(Drawable) or setImageBitmap(Bitmap) and BitmapFactory instead.

Note 3 : We use the fileCache here to get bitmaps, but we store the bitmaps thanks to our requester. You can do this very easily using an InputStream and an OutputStream

II. The BlockingImageView

When you scroll now your list, you can notice some lags when images are displayed. This is due to the setImageDrawable(Drawable d) wich can call a requestLayout (which one can take some time).

There is a way to block the requestLayout, pretty simple. I found it thanks to Jorim Jaggy, on this google post. But look at the source code of the ImageView first:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
     * Sets a drawable as the content of this ImageView.
     * 
     * @param drawable The drawable to set
     */
    public void setImageDrawable(Drawable drawable) {
        if (mDrawable != drawable) {
            mResource = ;
            mUri = null;

            int oldWidth = mDrawableWidth;
            int oldHeight = mDrawableHeight;

            updateDrawable(drawable);

            if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
                requestLayout();    //HERE
            }
            invalidate();
        }
    }

If you know that the dimension of all your images is the same, then you can block the requestlayout, by using a custom ImageView :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class BlockingImageView extends ImageView {
    private boolean mBlockLayout;

    public BlockingImageView(Context context) {
        super(context);
    }

    public BlockingImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public BlockingImageView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    public void requestLayout() {
        if (!mBlockLayout) {
            super.requestLayout();
        }
    }

    @Override
    public void setImageResource(int resId) {
        mBlockLayout = true;
        super.setImageResource(resId);
        mBlockLayout = false;
    }

    @Override
    public void setImageURI(Uri uri) {
        mBlockLayout = true;
        super.setImageURI(uri);
        mBlockLayout = false;
    }

    @Override
    public void setImageDrawable(Drawable drawable) {
        mBlockLayout = true;
        super.setImageDrawable(drawable);
        mBlockLayout = false;
    }
}

Don’t forget to modify the xml of your elements to add your custom ImageView.

You should now have a smoothy listView with images.

Hope you enjoyed, do not hesitate to leave comments and share it!

Comments