Jun 242011
 

The Android ProgressBar is a useful UI component that most developers will quickly find need of. Displaying progress, even an indeterminate “loading” indicator, provides crucial feedback to a user, eliminating frustration and confusion while your app is churning away in the background.

Today I’ll demonstrate using an indeterminate ProgressBar within a ListView while loading web content. I’ll be modifying the TweetView project from my previous post.

Note: You can find the full project with the ProgressBar feature in the “progress_bar” branch of the TweetView Github project.

Let’s get started. Recall that the previous iteration of the TweetView app displayed a stock Android icon image by default while the Twitter avatars were loaded for each item in the ListView. Of course, this is not ideal, as it gives the user no indication of what is going on, and new images suddenly replacing the icon may be confusing. A better user experience would be to place a loading image in each ListView item until the proper avatar is retrieved and displayed. To avoid the complication of trying determine progress of an image download (especially since our avatar images are not very large, and will download fairly quickly), I’ll use an indeterminate ProgressBar:

Step 1: Insert a ProgressBar UI component into our listitem.xml layout file.

We’ll put the ProgressBar element in the same spot we intend the avatar to occupy. This way, we can just set the visibility of the avatar ImageView to “gone” until the image is ready, and then switch the ImageView to “visible” and the ProgressBar to “gone” when we display the image. Here’s the updated layout file:



android:layout_height="wrap_content"
android:gravity="left|center"
android:layout_width="wrap_content"
android:paddingBottom="5px"
android:paddingTop="5px"
android:paddingLeft="5px">

android:layout_width="wrap_content"
android:layout_height="fill_parent">
android:id="@+id/avatar"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_marginRight="6dip"
android:visibility="gone" />
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxWidth="30dip"
android:minWidth="30dip"
android:maxHeight="30dip"
android:minHeight="30dip"
android:layout_marginRight="6dip"
android:indeterminate="true" />
android:orientation="vertical"
android:layout_width="0dip"
android:layout_weight="1"
android:layout_height="fill_parent">
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"/>
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10px"
android:textColor="#0099CC"/>


You’ll see that i’ve added something more than a ProgressBar element: a RelativeLayout. If we don’t set the ProgressBar’s height to fill_parent, it will squish our list items vertically to match it’s height. However, the ProgressBar element, unlike an ImageView, will stretch and distort if told to “fill_parent” in its layout_height or layout_width value. to avoid distortion of the indeterminate ProgressBar or the hiding of the text from our tweets, I’ve added the RelativeLayout to encapsulate ProgressBar and ImageView alike, providing the ability to stretch vertically and buffer our round ProgressBar with dead space.

Note also that I’ve set the sizes of the ProgressBar to match the size of the avatars we download from Twitter. You will want to change these size settings as appropriate for your app. This, combined with the layout hack above, will make our tweet items have a consistent layout, whether we are displaying avatars or a ProgressBar.

Step 2: TweetImageAdapter and our ViewHolder class need to be altered to store the ProgressBar for each list item, and pass it to the ImageManager.displayImage() method so it can be made invisible when the correct image is displayed.

First we add a ProgressBar member to the ViewHolder class:


public static class ViewHolder {
public TextView username;
public TextView message;
public ImageView image;
public ProgressBar progress; //ADDED
}

Then we alter the tweetItemAdapter.getView() method to retrieve and pass along this ProgressBar object to the ImageManager through the displayImage() method:


@Override
public View getView(int position, View convertView, ViewGroup parent) {
View v = convertView;
ViewHolder holder;
if (v == null) {
LayoutInflater vi =
(LayoutInflater)activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
v = vi.inflate(R.layout.listitem, null);
holder = new ViewHolder();
holder.username = (TextView) v.findViewById(R.id.username);
holder.message = (TextView) v.findViewById(R.id.message);
holder.image = (ImageView) v.findViewById(R.id.avatar);
holder.progress = (ProgressBar) v.findViewById(R.id.progress_bar); //ADDED
v.setTag(holder);
}
else
holder=(ViewHolder)v.getTag();

final Tweet tweet = tweets.get(position);
if (tweet != null) {
holder.username.setText(tweet.username);
holder.message.setText(tweet.message);
holder.image.setTag(tweet.image_url);
imageManager.displayImage(tweet.image_url, activity,
holder.image, holder.progress); //CHANGED
}
return v;
}

Ok, now our ImageManager, the class responsible for updating the image displayed when an avatar has finished downloading, has access to the ProgressBar component as well as the ImageView. We’re ready for the last step.

Learn Node.js by Example

Take my online course featuring screencasts and sample projects!

Step 3: Change the image display methods to manage visibility of the ImageView and ProgressBar list item components, as well as to set the appropriate source image for the ImageView. Recall that we had two methods capable of displaying an image, one that could do so immediately (if the image was found in our local cache) and one that operated asynchronously, displaying the image as soon as the background download process is complete.

The first method, displayImage(), can be altered like so:


public void displayImage(String url, Activity activity,
ImageView imageView, ProgressBar progressBar) {

if(imageMap.containsKey(url)) {
imageView.setImageBitmap(imageMap.get(url));
progressBar.setVisibility(View.GONE); //ADDED
imageView.setVisibility(View.VISIBLE); //ADDED
}
else {
queueImage(url, activity, imageView, progressBar);
imageView.setVisibility(View.GONE); //ADDED
progressBar.setVisibility(View.VISIBLE); //ADDED
}
}

Notice that the ProgressBar object is now being passed in, as designed in the last step. Using this, we are able to set visibility of the ImageView/ProgressBar UI elements (making sure only one is visible at any given time), depending on whether the image bitmap has been set or not.

Additionally, we pass the progressBar object along into the queueImage method – more on that soon.

The second place we need to add this visibility shuffle code is the run() method of the Runnable BitmapDisplayer class, which will be invoked on the UI thread, from the background thread, to set the image. Same basic changes – the BitmapDisplayer class now looks like:


//Used to display bitmap in the UI thread
private class BitmapDisplayer implements Runnable {
Bitmap bitmap;
ImageView imageView;
ProgressBar progressBar;

public BitmapDisplayer(Bitmap b, ImageView i, ProgressBar p) {
bitmap = b;
imageView = i;
progressBar = p;
}

public void run() {
if(bitmap != null) {
imageView.setImageBitmap(bitmap);
progressBar.setVisibility(View.GONE); //ADDED
imageView.setVisibility(View.VISIBLE); //ADDED
}
else {
imageView.setVisibility(View.GONE); //ADDED
progressBar.setVisibility(View.VISIBLE); //ADDED
}
}
}

Step 5: Last, but not least, you’ll notice above that the BitmapDisplayer object needs to have a ProgressBar object passed in as an input parameter. The BitmapDisplayer object in TweetView is populated by data from ImageRef objects, so ImageRef objects now need to contain this additional object. Let’s take care of that, and while we are at it, change the ImageQueueManager run() method to get the BitmapDisplayer the updated input parameters that it needs:


private class ImageRef {
public String url;
public ImageView imageView;
public ProgressBar progressBar;

public ImageRef(String u, ImageView i, ProgressBar p) {
url = u;
imageView = i;
progressBar = p;
}
}

private class ImageQueueManager implements Runnable {
@Override
public void run() {
try {
while(true) {
// Thread waits until there are images in the
// queue to be retrieved
if(imageQueue.imageRefs.size() == 0) {
synchronized(imageQueue.imageRefs) {
imageQueue.imageRefs.wait();
}
}

// When we have images to be loaded
if(imageQueue.imageRefs.size() != 0) {
ImageRef imageToLoad;

synchronized(imageQueue.imageRefs) {
imageToLoad = imageQueue.imageRefs.pop();
}

Bitmap bmp = getBitmap(imageToLoad.url);
imageMap.put(imageToLoad.url, bmp);
Object tag = imageToLoad.imageView.getTag();

// Make sure we have the right view - thread safety defender
if(tag != null && ((String)tag).equals(imageToLoad.url)) {
BitmapDisplayer bmpDisplayer =
new BitmapDisplayer(bmp, imageToLoad.imageView,
imageToLoad.progressBar);

Activity a =
(Activity)imageToLoad.imageView.getContext();

a.runOnUiThread(bmpDisplayer);
}
}

if(Thread.interrupted())
break;
}
} catch (InterruptedException e) {}
}
}

All done! Now we will see a lovely perpetually spinning progress wheel, sure to delight users while they wait for Twitter avatars to be downloaded. Best of all, through some simple layout modifications, we should see no distortion or inconsistency in our item layouts, leading to a pleasant user experience.

I’ve created a new branch of the TweetView Github project containing these modifications. You can find it here: progress_bar branch. Just switch to this branch to see this version of the app.

As always, comments or questions are very welcome. Happy coding!

Technorati code: 7ESRQSJMX8BM

  • Weber Antoine

    Hello cacois,

    Thanks a lot for this tutorial ( and all the previous ones) . :)
    Those really helped me to understand the Asynchronous loading.

    However, I’m steel getting problem with this, in fact compilation worked well, but I have progressBars still displayed until I scroll the listView (even if I know that images are allready loaded [they are small and on a local server] ). Could you tell me if I am missing something, and how i can get this code working for the first elements of the listView even whithout scrolling?

    Regards,
    Antoine

    PS: The tag seems to miss in your first xml file.

    • Weber Antoine

      PS: the tag “/RelativeLayout” seems to miss in your first xml file.

    • Weber Antoine

      Maybe we need to .invalidate() the listView (or the item in it) each time an image has been received. I will search on that and keep you posted.

      • http://www.codehenge.net Constantine Aaron Cois

        That shouldn’t be necessary, it should be updating on its own. Depending on how you add items to a ListView, it may or may not automatically invoke notifyDataSetChanged(), which is what needs to happen to force the ListView to refresh.

        I’ll run some tests in the github code and see if I can reproduce the issue.

        • Weber Antoine

          in fact, in order to improve performance I am using another type of “holder” which do the “findViewById” only if needed :

          public class ListItemWrapper {
          View base;
          HashMap map = new HashMap();
          //HashMap icon = new HashMap();

          ListItemWrapper(View base){
          this.base = base;
          }

          public View getLabel(int key){
          if(!map.containsKey(key)){
          map.put(key, (View) base.findViewById(key));
          }
          return map.get(key);
          }
          }

          I’m really interested in keeping that sort of helper because I have lots of textView in my list items, who need to be set dynamicaly.
          Have you an idea on how I could mix your ImageManager with that sort of holder? And still have the first element up to date ?

          • Weber Antoine

            problem solved, I was forgetting to do the imageView.setTag(url) so the Thread safety verification never called BItmapDisplayer. So i added that one in the ‘else’ part of the displayImage fonction, and it work like a charm. Thanks again for this great piece of code :)

          • http://www.codehenge.net Constantine Aaron Cois

            Fantastic! That would definitely do that – the tag is a little bit of a hack, but necessary to make sure the images don’t get mixed up.

          • Ravikumar

            Awesome code

  • sathish android

    Hi Constantine Aaron Cois,
    I am very thankful to you for considering my suggestion and posted this new one. Thanks a lot.

    I hope my ideas are very helpful to every one including me if you solve those?
    By reading and understanding your solutions,beginners like me are getting very good knowledge on android development that is a great thing.

    Can you please suggest a work around for the below requirement also?
    Requirement:
    After showing list view with images from the urls , i want to click on one list item then needs to show camera, capture a new image using camera and update the list view with this newly captured image in place of previously selected list item’s image.

    Can you give me some suggestions to integrate the above requirement into our “TweetView” like project?

    Thanks in advance,
    Sathish.
    sathya.sri69@gmail.com

  • Fizo

    hi

  • Hafiz

    hi, great tutorial here. Ive got a slight problem with it though. I implemented it in my project, however, the images dont update in the listview, they only update when i scroll the listview items down or up, been trying to figure out what the cause might be

    • fizo

      sorry i discovered the problem. forgot to add the setTag() method.. Great job mate

  • Andrew

    I am getting duplicate images all over the scroll when scrolling…Also, am trying to implement a default image if no image is available and getting errors…any ideas/suggestions?

  • Nsh_chatlani

    Thank you so much. :)

  • KooL

    Thanks a lot … it helped me a lot :)

  • Adi

    Thank you so much . . . your tutorials have helped me a lot .. looking forward for newer ones.
    Please add some tuts on location based services on Android…Thanks again

  • Nick Nelson

    Hey man your tutorials have been great for me. I have used the base concepts of your code and expanded upon it a ton. I am now pulling a whole bunch of data from a remote MySQL database and lazy loading it in a listview. Great job on these. I think I read a couple tutorials back that you said you were going to do a follow up tutorial on how to load a certain number of objects at a time and then load the next set of objects when needed (kind of like the official twitter app). Any word on if you’re still doing that?

    • http://www.codehenge.net Constantine Aaron Cois

      Hey Nick! Thanks for the comment. I’m definitely still planning on doing a number of follow-up tutorials to this one, including the topic you mentioned. I’ve been super busy working on a screencast course for node.js, and as soon as its published I’ll owe my android tutorials some love.

      I’ll try to keep you guys posted!

  • Roshni

    sir,
    i want a tutorial on “how the data is dynamically added in database via list-view in android app”

  • Dinesh

    Dude change the color. Dark color is affecting eyes.

  • http://www.facebook.com/kailash.pawar Kailash Pawar

    I am new to Android.
    Hello Sir
    I have to download around 36 images. I have the URL of all images.!!
    Where I have to insert those URLs in code..??
    please help
    and want those images to be in Grid View

44 queries in 1.301 seconds