Monday, 29 April 2013

Loading Image

Hey everyone!
Recently while working on a project I was asked to lazy load a list of images (bitmaps) into a ListView. What made the task even trickier was the fact that each image had to be streamed from a given URL. I thought about downloading all of the images at the application’s startup Activity, and then dumping them into some kind of external SD card as a cache, but I was told that the images were subject to frequent change and that true caching would be difficult. The result is how I chose to implement the feature, and I thought I’d share it with everyone.
Now there’s no guarantee that the way I’m doing this is the “correct” or optimal way – in fact if you DO know the best way to lazy load images from URLs then please share – but the solution below works and uses a handful of Android classes and concepts, all of which I’ll note as I walk you through the solution.
Before I move onto the code, let’s conceptually think about how we’re going to make this feature work. In this example, let’s say we’re making an HTTP request to some external server that returns a list of Students. Each Student has a name and an image URL which is to be streamed, converted into a Bitmap, and then displayed in a ListView. Getting the list of Student objects is the easy part – but what’s the next step? One option is to loop through each Student, grab each Student’s image URL, convert the URL into a Bitmap, and then allow each Student to hold a reference to their Bitmap; at which point we can load the list like we normally would.
A very feasible solution – but depending on the size of your Student list. Let’s assume that each image takes between 0.5 to 1 seconds to load. Now, if your database only has 5-10 Students in it, then maybe this iterative solution may work. However, consider a database with 100s of Students – clearly this iterative solution won’t hold up in that case. What’s the better solution? Wouldn’t it be nice if we could parallelize the loading process?
That’s step one – thinking of a way to parallelize the process. Step two is to do all this loading and processing on separate background threads. This way the user can interact with the application, even while the images are loading. The last step, step three, is to make sure that each background thread can somehow communicate with the original ListAdapter and make sure that the list gets updated each time an image is successfully streamed.
With that, here’s the code.
First, let’s start with a basic Student object. Each Student has a picture that needs to be loaded, and each student holds a reference to both their image’s URL as well as their Bitmap (to be downloaded):
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
public class Student {
 
    private String name;
 
    private String imgUrl;
 
    private Bitmap image;
 
    private StudentAdapter sta;
 
    public Student(String name, String imgUrl) {
                this.name = name;
                this.imgUrl = imgUrl;
 
                // TO BE LOADED LATER - OR CAN SET TO A DEFAULT IMAGE
                this.image = null;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public String getImgUrl() {
        return imgUrl;
    }
 
    public void setImgUrl(String imgUrl) {
        this.imgUrl = imgUrl;
    }
 
        public Bitmap getImage() {
        return image;
    }
 
    public StudentAdapter getAdapter() {
        return sta;
    }
 
    public void setAdapter(StudentAdapter sta) {
        this.sta = sta;
    }
 
    public void loadImage(StudentAdapter sta) {
        // HOLD A REFERENCE TO THE ADAPTER
        this.sta = sta;
        if (imgUrl != null && !imgUrl.equals("")) {
            new ImageLoadTask().execute(imgUrl);
        }
    }
 
    // ASYNC TASK TO AVOID CHOKING UP UI THREAD
    private class ImageLoadTask extends AsyncTask<String, String, Bitmap> {
 
        @Override
        protected void onPreExecute() {
            Log.i("ImageLoadTask", "Loading image...");
        }
 
        // param[0] is img url
        protected Bitmap doInBackground(String... param) {
            Log.i("ImageLoadTask", "Attempting to load image URL: " + param[0]);
            try {
                Bitmap b = ImageService.getBitmapFromURLWithScale(param[0]);
                return b;
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
 
        protected void onProgressUpdate(String... progress) {
            // NO OP
        }
 
        protected void onPostExecute(Bitmap ret) {
            if (ret != null) {
                Log.i("ImageLoadTask", "Successfully loaded " + name + " image");
                image = ret;
                if (sta != null) {
                    // WHEN IMAGE IS LOADED NOTIFY THE ADAPTER
                    sta.notifyDataSetChanged();
                }
            } else {
                Log.e("ImageLoadTask", "Failed to load " + name + " image");
            }
        }
    }
 
}
Alright so what’s going on here? Again, the Student object we have here is very simple – it has only a name, image URL, and Bitmap image associated with it. For now, the image will be null (or a default image can be used) but we choose to hold a reference to it so that once it is loaded, we can “cache” it and only have to load it once.
Then, let’s note two things. First, we see that each Student has a loadImage() method which initiates an AsyncTask that we created called ImageLoadTask. By containing the loading process within the Student object, we’ve found a natural way to address step one and parallelize the process. Now, each Student can independently kick off their image loading processes and independently handle the resulting Bitmap. Why do we use an AsyncTask? Well that’s to address step two above and make sure that each process is loaded in a background thread. By doing this, we guarantee that each Student manages their own image Bitmap, and without disturbing the main UI thread!
Furthermore, we see that each Student object holds a reference to a StudentAdapter, whose code will be shown next. This is our solution to step three from above. By holding a reference to the Adapter, we can make sure that once the Student’s image is loaded (through the ImageLoadTask), the Student can then tell the adapter to update itself using the Adapter’s notifyDataSetChanged() method.
Let’s take a quick look at how the StudentAdapter is defined:
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
public class StudentAdapter extends BaseAdapter {
 
    private LayoutInflater mInflater;
 
    private List items = new ArrayList();
 
    public StudentAdapter(Context context, List items) {
        mInflater = LayoutInflater.from(context);
        this.items = items;
    }
 
    public int getCount() {
        return items.size();
    }
 
    public Student getItem(int position) {
        return items.get(position);
    }
 
    public long getItemId(int position) {
        return position;
    }
 
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder;
        Student s = items.get(position);
        if (convertView == null) {
            convertView = mInflater.inflate(R.layout.row_layout, null);
            holder = new ViewHolder();
            holder.name = (TextView) convertView.findViewById(R.id.name);
            holder.image = (ImageView) convertView.findViewById(R.id.image);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }
        holder.name.setText(s.getName());
        if (s.getImage() != null) {
            holder.pic.setImageBitmap(s.getImage());
        } else {
                // MY DEFAULT IMAGE
            holder.pic.setImageResource(R.drawable.generic_profile_man);
        }
        return convertView;
    }
 
    static class ViewHolder {
        TextView name;
 
        ImageView pic;
    }
 
}
Pretty simple really and nothing much to say. Really you just have to note that in the getView() method, if the Student’s image is null (implying that the Student’s image hasn’t been loaded yet) then I set the image to be a default generic profile picture. And so let’s bring this all together by looking at how we’d set this up in an Activity class:
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
public class StudentListActivity extends ListActivity {
 
        private List students;
 
        private StudentAdapter sta;
 
        @Override
        protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.list);
 
                // GET YOUR STUDENTS
                students = //...
 
                // CREATE BASE ADAPTER
                sta = new StudentAdapter(StudentListActivity.this, students);
 
                // SET AS CURRENT LIST
                setListAdapter(sta);
 
                for (Student s : students) {
                        // START LOADING IMAGES FOR EACH STUDENT
                        s.loadImage(sta);
                }
        }
 
}
And voila! That’s it! We look through our list of Students, and for each Student we call their loadImage() method, making sure we pass in the instantiated ListAdapter. Then, each loadImage() method will kick off an AsyncTask that will stream the image, cache it as a Bitmap, and then notify the ListAdapter to update itself.
How this is going to look at the end will be a list that the user can scroll through, where each row is loading itself behind the scenes, and displaying its image as soon as the loading is done. Hopefully this all makes sense – and again happy to hear other solutions as well! Best of luck and as usual, happy coding.



--------------------------------------------------------------------------------------
Hey everyone,
This post is for anyone who has ever wanted to load an image from a given URL and turn it into a Bitmap which you can then use to set as an ImageView’s background, or upload as a Contact’s photo, etc.
So here it is,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static Bitmap getBitmapFromURL(String src) {
    try {
        URL url = new URL(src);
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setDoInput(true);
        connection.connect();
        InputStream input = connection.getInputStream();
        Bitmap myBitmap = BitmapFactory.decodeStream(input);
        return myBitmap;
    } catch (IOException e) {
        e.printStackTrace();
        return null;
    }
}
Nothing too complicated – simply create a URL object using the “src” string (i.e. String src = “http://thinkandroid.wordpress.com&#8221;), connect to it, and use Android’s BitmapFactory class to decode the input stream. The result should be your desired Bitmap object!
- jwei

No comments:

Post a Comment