Adding a ProgressBar to a ListView/OnClick called a single time

*爱你&永不变心* 提交于 2019-12-05 00:10:04

问题


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


回答1:


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




回答2:


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;
}


来源:https://stackoverflow.com/questions/11200299/adding-a-progressbar-to-a-listview-onclick-called-a-single-time

工具导航Map