6

The idea is to have a list of items where after clicking an item, a ProgressBar will slowly fill as the task is completed. For example, picture a list of files, with a Download button by each one. When the download button is clicked, the file is downloaded in the background and a progress bar is filled showing how close the file is to completion.

To accomplish this, I create an AsyncTask which occasionally calls notifyDataSetChanged on the adaptor to redraw it. While this works after one button is clicked, until the AsyncTask is completed, I am unable to click other buttons in the ListView. Can someone please tell me what I am doing wrong?

I am running this on the emulator (x86) on Ice Cream Sandwich.

I have a DownloadItem to represent the progress of a download (the code below is simplified for brevity):

class DownloadItem {
    public String name;       // Name of the file being downloaded
    public Integer progress;  // How much is downloaded so far
    public Integer length;    // Size of the file
}

I then have an ArrayAdapter that adapts a list of DownloadItem for the ListView:

class DownloadArrayAdapter extends ArrayAdapter<DownloadItem> {
    List<DownloadItem> mItems;
    public DownloadArrayAdapter(List<DownloadItem> items) {
      mItems = items;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View row = convertView;
        if(row == null) {
            // Inflate
            Log.d(TAG, "Starting XML inflation");
            LayoutInflater inflater = (LayoutInflater) this.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            row = inflater.inflate(R.layout.download_list_item, parent, false);
            Log.d(TAG, "Finished XML inflation");
        }

        DownloadItem item = mItems.get(position);

        ProgressBar downloadProgressBar = (ProgressBar) row.findViewById(R.id.downloadProgressBar);
        Button downloadButton = (Button) row.findViewById(R.id.downloadButton);

        downloadButton.setTag(item);
        downloadProgressBar.setMax(item.length);
        downloadProgressBar.setProgress(item.progress);

        return row;
    }
}

So far, so good - this properly renders the list. In my Activity, I have the onClickListener:

class DownloadActivity extends Activity {
    //...
    public void onDownloadButtonClick(View view) {
        DownloadItem item = (DownloadInfo)view.getTag();
        DownloadArrayAdapter adapter = (DownloadArrayAdapter) view.getAdapter();
        new DownloadTask(adapter, item).execute();
        //new DownloadTask(adapter, item).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
    }
}

I've tried this with executeOnExecutor as well as just execute, but with no luck. DownloadTask is:

class DownloadTask extends AsyncTask<Void, Integer, Void> {
    ArrayAdapter<?> mAdapter;
    DownloadItem mItem;

    public DownloadTask(ArrayAdapter<?> adapter, DownloadItem item) {
        mItem = item;
    }

    //Dummy implementation
    @Override
    public Void doInBackground(Void ... params) {
        for(int i=0; i<mItem.length; ++i) {
            Thread.sleep(10); publishProgress(i);
        }
        return null;
    }

    @Override
    public void onProgressUpdate(Integer ... values) {
        mItem.progress = values[0];
        mAdapter.notifyDataSetChanged();
    }
}

This works - almost. When I do this, after clicking one button, the ProgressBar updates as normal - but I can't click other buttons in the ListView until the AsyncTask returns. That is, onDownloadButtonClick is never called. If I remove the mAdapter.notifyDataSetChanged() call from the onProgressUpdate function, multiple tasks are updated simultaneously, but of course, the list isn't invalidated and so I have to scroll around to see changes.

What am I doing wrong, and how can I fix this?

Edit: I played with this some more, and it seems that the frequency of calling notifyDataSetChanged affects whether onClick is being called or just being lost. With the above code, by clicking frantically I can occasionally get a second download bar to start. If I increase the Thread.Sleep to something much larger, like 2000, the list works as I originally expected.

So new question - how do I get the ProgressBars to update smoothly while not blocking onClick from being called?

EDIT #2: I have pushed a sample project with this problem to my GitHub account: https://github.com/mdkess/ProgressBarListView

mindvirus
  • 5,016
  • 4
  • 29
  • 46
  • 1
    It seems you use list view? If so, and if you have time, I think it's worth to watch this: [Google I/O 2010 - List view](http://www.youtube.com/watch?v=wDBM6wVEO70). A suggestion: you can use `Thread.sleep(800)` (for example), `10` is not necessary. –  Jun 26 '12 at 03:21
  • 1
    Where do you set the click listener? You miss this in the dummy code. – reTs Jun 26 '12 at 03:40
  • @reTS: I set it in the XML, with the android:onClick parameter for the button. The handler is definitely being called - once, anyway. See my edit to the question though as well. – mindvirus Jun 26 '12 at 03:45
  • @haibison: That talk looks great - thanks, I will watch it as soon as possible. Your suggestion really helped, see my edit above - it seems that the relative frequency of calling notifyDataSetChanged is causing onClick events to be lost. – mindvirus Jun 26 '12 at 03:45
  • notifyDataSetChanged is calling the listView to do something blocking the ui thread and this is not easy to be changed. Also ,the DownloadTask is actually refreshing the whole list, which is not necessary IMO, as one downloadTask only needs to update one progress bar. Try to pass a reference of progress bar into the async task(maybe put it in the downloadItem), and update the specific progress bar in onProgressUpdate, to reduce the work of ui thread. – reTs Jun 26 '12 at 05:22
  • reTs: The problem that I found with that is that the ProgressBar objects are reused by the ListView. In getView, the convertView is an out of view row of the ListView, which is designed to be reused. So I could do it that way by ignoring the convertView, but that seems to go against the spirit of the ListView. – mindvirus Jun 26 '12 at 14:28

2 Answers2

6

I have figured this out.

Instead of calling notifyDataSetChanged(), I stored a reference to each ProgressBar in the DownloadItem object. Then, when scrolling through the ListView, when old objects were passed as convertView, I removed the ProgressBar from the old DownloadInfo and put it on the new one.

Thus, getView for my array adapter then became:

 @Override
 public View getView(int position, View convertView, ViewGroup parent) {
    View row = convertView;
    final DownloadInfo info = getItem(position);
    // We need to set the convertView's progressBar to null.

    ViewHolder holder = null;

    if(null == row) {
      LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
      row = inflater.inflate(R.layout.file_download_row, parent, false);

      holder = new ViewHolder();
      holder.textView = (TextView) row.findViewById(R.id.downloadFileName);
      holder.progressBar = (ProgressBar) row.findViewById(R.id.downloadProgressBar);
      holder.button = (Button)row.findViewById(R.id.downloadButton);
      holder.info = info;

      row.setTag(holder);
    } else {
      holder = (ViewHolder) row.getTag();

      holder.info.setProgressBar(null);
      holder.info = info;
      holder.info.setProgressBar(holder.progressBar);
    }

    holder.textView.setText(info.getFilename());
    holder.progressBar.setProgress(info.getProgress());
    holder.progressBar.setMax(info.getFileSize());
    info.setProgressBar(holder.progressBar);

    holder.button.setEnabled(info.getDownloadState() == DownloadState.NOT_STARTED);
    final Button button = holder.button;
    holder.button.setOnClickListener(new OnClickListener() {
      @Override
      public void onClick(View v) {
        info.setDownloadState(DownloadState.QUEUED);
        button.setEnabled(false);
        button.invalidate();
        FileDownloadTask task = new FileDownloadTask(info);
        task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
      }
    });
    return row;
  }

The download AsyncTask would then set the progress on the progressBar, if it was not null, and this worked as expected.

I uploaded the corrected code to GitHub, you can see it here: https://github.com/mdkess/ProgressBarListView

mindvirus
  • 5,016
  • 4
  • 29
  • 46
  • Hi....Your code was of great help to me...But I have a problem. When I run your code in an activity, it works just too well. When I run your code in a fragment, I get the progress bar object null. Why is it so? – android developer Oct 20 '13 at 13:32
  • What does it happen if you remove an item when one or two others are in progress (dowload for ex) ? because I try, but when I delete an item, progressBars bug. – Cocorico Jul 09 '14 at 09:21
  • Hi.. your code is great.. but I have one issue with it.. please help me. – Sagar Maiyad Oct 17 '15 at 05:43
1

Did you try setting click listener to download item in adapter itself like following:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    View row = convertView;
    if(row == null) {
        // Inflate
        Log.d(TAG, "Starting XML inflation");
        LayoutInflater inflater = (LayoutInflater) this.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        row = inflater.inflate(R.layout.download_list_item, parent, false);
        Log.d(TAG, "Finished XML inflation");
    }

    final DownloadItem item = mItems.get(position);

    ProgressBar downloadProgressBar = (ProgressBar) row.findViewById(R.id.downloadProgressBar);
    Button downloadButton = (Button) row.findViewById(R.id.downloadButton);

    downloadButton.setTag(item);
    downloadProgressBar.setMax(item.length);
    downloadProgressBar.setProgress(item.progress);

    downloadButton.setOnClickListener(new View.OnClickListener() {

        @Override
        public void onClick(View v) {
            new DownloadTask(DownloadArrayAdapter.this, item).execute();
        }
    });

    return row;
}
Veer
  • 2,071
  • 19
  • 24
  • Alas, that didn't do any good - it's not even getting to the onClick. I think that it has to do with calling notifyDataSetChanged too frequently, so I'm experimenting with holding a reference to the actual progress bar now. Good thought, though. (By the way, I didn't downvote you). – mindvirus Jun 29 '12 at 02:42
  • Thanks. Never mind who down voted me. Actually, i didn't read your question correctly. I thought, you setting tag to rowView.. But you set tag to download Button.. So i gave you this solution.. Yes, passing reference of progress bar and updating progress bar directly should work.. BTW you asked the question in very nice manner and i like your implementation as well :-).. – Veer Jun 29 '12 at 07:29