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
In Android, SeekBar is an extension of ProgressBar that adds a draggable thumb, a user can touch the thumb and drag left or right to set the value for current progress. SeekBar is one of the very useful user interface element in Android that allows the selection of integer values using a natural user interface.
Android Progress Dialog is a UI which shows the progress of a task like you want user to wait until the previous lined up task is completed and for that purpose you can use progress dialog. The best example is you see when downloading or uploading any file.
progressDrawable: progress drawable is an attribute used in Android to set the custom drawable for the progress mode. Below we set a custom gradient drawable for the progress mode of a progress bar.
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
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;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With