如何更新ListView中的某些数据而不使用notifyDataSetChanged()?

我试图创build一个列表下载任务列表。

下载任务在Service (DownloadService)中pipe理。 每次接收到大量数据时,任务都会通过包含ListView (SavedShowListFragment)的Fragment接收的Broadcast发送进度。 在接收到Broadcast消息时,SavedShowListFragment将更新适配器中下载任务的进度,并触发notifyDataSetChanged()

列表中的每一行都包含一个ProgressBar ,一个用于下载文件标题的文本TextView ,一个用于进度数值的TextView ,以及一个用于暂停/恢复下载或在下载完成时播放保存的显示的Button

问题是,暂停/恢复/播放Button往往没有响应( onClick()没有被调用),我认为这是因为整个列表更新非常频繁与notifyDataSetChanged() (每次大块数据,即1024字节被接收,可以每秒多次,特别是当有多个下载任务运行时)。

我想我可以在下载任务中增加数据块的大小,但我真的认为我的方法不是最优的!

可能非常频繁地调用notifyDataSetChanged()使ListView UI无响应?

有没有办法更新ListView行中的一些Views ,即在我的情况下ProgressBarTextViewProgressBar的数值,而不调用notifyDataSetChanged() ,它更新整个列表?

要更新ListView下载任务的进度,是否有更好的select比“getChunk / sendBroadcast / updateData / notifyDataSetChanged”?

以下是我的代码的相关部分。

在下载服务中下载任务

 public class DownloadService extends Service { //... private class DownloadTask extends AsyncTask<SavedShow, Void, Map<String, Object>> { //... @Override protected Map<String, Object> doInBackground(SavedShow... params) { //... BufferedInputStream in = new BufferedInputStream(connection.getInputStream()); byte[] data = new byte[1024]; int x = 0; while ((x = in.read(data, 0, 1024)) >= 0) { if(!this.isCancelled()){ outputStream.write(data, 0, x); downloaded += x; MyApplication.dbHelper.updateSavedShowProgress(savedShow.getId(), downloaded); Intent intent_progress = new Intent(ACTION_UPDATE_PROGRESS); intent_progress.putExtra(KEY_SAVEDSHOW_ID, savedShow.getId()); intent_progress.putExtra(KEY_PROGRESS, downloaded ); LocalBroadcastManager.getInstance(DownloadService.this).sendBroadcast(intent_progress); } else{ break; } } //... } //... } } 

SavedShowListFragment

 public class SavedShowListFragment extends Fragment { //... @Override public void onResume() { super.onResume(); mAdapter = new SavedShowAdapter(getActivity(), MyApplication.dbHelper.getSavedShowList()); mListView.setAdapter(mAdapter); //... } private ServiceConnection mDownloadServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName className, IBinder service) { // Get service instance DownloadServiceBinder binder = (DownloadServiceBinder) service; mDownloadService = binder.getService(); // Set service to adapter, to 'bind' adapter to the service mAdapter.setDownloadService(mDownloadService); //... } @Override public void onServiceDisconnected(ComponentName arg0) { // Remove service from adapter, to 'unbind' adapter to the service mAdapter.setDownloadService(null); } }; private BroadcastReceiver mMessageReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if(action.equals(DownloadService.ACTION_UPDATE_PROGRESS)){ mAdapter.updateItemProgress(intent.getLongExtra(DownloadService.KEY_SAVEDSHOW_ID, -1), intent.getLongExtra(DownloadService.KEY_PROGRESS, -1)); } //... } }; //... } 

SavedShowAdapter

 public class SavedShowAdapter extends ArrayAdapter<SavedShow> { private LayoutInflater mLayoutInflater; private List<Long> mSavedShowIdList; // list to find faster the position of the item in updateProgress private DownloadService mDownloadService; private Context mContext; static class ViewHolder { TextView title; TextView status; ProgressBar progressBar; DownloadStateButton downloadStateBtn; } public static enum CancelReason{ PAUSE, DELETE }; public SavedShowAdapter(Context context, List<SavedShow> savedShowList) { super(context, 0, savedShowList); mLayoutInflater = (LayoutInflater) context.getSystemService( Context.LAYOUT_INFLATER_SERVICE ); mContext = context; mSavedShowIdList = new ArrayList<Long>(); for(SavedShow savedShow : savedShowList){ mSavedShowIdList.add(savedShow.getId()); } } public void updateItemProgress(long savedShowId, long progress){ getItem(mSavedShowIdList.indexOf(savedShowId)).setProgress(progress); notifyDataSetChanged(); } public void updateItemFileSize(long savedShowId, int fileSize){ getItem(mSavedShowIdList.indexOf(savedShowId)).setFileSize(fileSize); notifyDataSetChanged(); } public void updateItemState(long savedShowId, int state_ind, String msg){ SavedShow.State state = SavedShow.State.values()[state_ind]; getItem(mSavedShowIdList.indexOf(savedShowId)).setState(state); if(state==State.ERROR){ getItem(mSavedShowIdList.indexOf(savedShowId)).setError(msg); } notifyDataSetChanged(); } public void deleteItem(long savedShowId){ remove(getItem((mSavedShowIdList.indexOf(savedShowId)))); notifyDataSetChanged(); } public void setDownloadService(DownloadService downloadService){ mDownloadService = downloadService; notifyDataSetChanged(); } @Override public View getView(final int position, View convertView, ViewGroup parent) { ViewHolder holder; View v = convertView; if (v == null) { v = mLayoutInflater.inflate(R.layout.saved_show_list_item, parent, false); holder = new ViewHolder(); holder.title = (TextView)v.findViewById(R.id.title); holder.status = (TextView)v.findViewById(R.id.status); holder.progressBar = (ProgressBar)v.findViewById(R.id.progress_bar); holder.downloadStateBtn = (DownloadStateButton)v.findViewById(R.id.btn_download_state); v.setTag(holder); } else { holder = (ViewHolder) v.getTag(); } holder.title.setText(getItem(position).getTitle()); Integer fileSize = getItem(position).getFileSize(); Long progress = getItem(position).getProgress(); if(progress != null && fileSize != null){ holder.progressBar.setMax(fileSize); holder.progressBar.setProgress(progress.intValue()); holder.status.setText(Utils.humanReadableByteCount(progress) + " / " + Utils.humanReadableByteCount(fileSize)); } holder.downloadStateBtn.setTag(position); SavedShow.State state = getItem(position).getState(); /* set the button state */ //... /* set buton onclicklistener */ holder.downloadStateBtn.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { int position = (Integer) v.getTag(); SavedShow.State state = getItem(position).getState(); if(state==SavedShow.State.DOWNLOADING){ getItem(position).setState(SavedShow.State.WAIT_PAUSE); notifyDataSetChanged(); mDownloadService.cancelDownLoad(getItem(position).getId(), CancelReason.PAUSE); } else if(state==SavedShow.State.PAUSED || state==SavedShow.State.ERROR){ getItem(position).setState(SavedShow.State.WAIT_DOWNLOAD); notifyDataSetChanged(); mDownloadService.downLoadFile(getItem(position).getId()); } if(state==SavedShow.State.DOWNLOADED){ /* play file */ } } }); return v; } } 

Solutions Collecting From Web of "如何更新ListView中的某些数据而不使用notifyDataSetChanged()?"

当然,正如pjco所说,不要以这样的速度更新。 我会build议间隔发送广播。 更好的是,通过轮询有一个容器进行数据更新和每个间隔更新。

不过,我认为在不使用notifyDataSetChanged情况下更新列表视图也是一件好事。 实际上,当应用程序具有更高的更新频率时,这是最有用的。 记住:我并不是说你的更新触发机制是正确的。


基本上,你会想更新一个特定的位置没有notifyDataSetChanged 。 在下面的例子中,我假设了以下几点:

  1. 你的listview被称为mListView。
  2. 你只想更新进度
  3. 你的convertView中的进度条的ID为R.id.progress

 public boolean updateListView(int position, int newProgress) { int first = mListView.getFirstVisiblePosition(); int last = mListView.getLastVisiblePosition(); if(position < first || position > last) { //just update your DataSet //the next time getView is called //the ui is updated automatically return false; } else { View convertView = mListView.getChildAt(position - first); //this is the convertView that you previously returned in getView //just fix it (for example:) ProgressBar bar = (ProgressBar) convertView.findViewById(R.id.progress); bar.setProgress(newProgress); return true; } } 

笔记

这个例子当然不完整。 您可以使用以下顺序:

  1. 更新你的数据(当你收到新的进展)
  2. 调用updateListView(int position)应该使用相同的代码,但更新使用您的数据集,没有参数。

另外,我只注意到你有一些代码发布。 既然你正在使用一个持有人,你可以简单地拿到持有人的function。 我不会更新代码(我认为这是不言而喻的)。

最后,为了强调,请更改您的整个代码以触发进度更新。 一个快速的方法是改变你的服务:用if语句包装发送广播的代码,该语句检查上一次更新是否已经超过一秒或半秒,以及下载是否完成(不需要检查完成但是确保完成后发送更新):

在你的下载服务中

 private static final long INTERVAL_BROADCAST = 800; private long lastUpdate = 0; 

现在在doInBackground中,用if语句包装发送意图

 if(System.currentTimeMillis() - lastUpdate > INTERVAL_BROADCAST) { lastUpdate = System.currentTimeMillis(); Intent intent_progress = new Intent(ACTION_UPDATE_PROGRESS); intent_progress.putExtra(KEY_SAVEDSHOW_ID, savedShow.getId()); intent_progress.putExtra(KEY_PROGRESS, downloaded ); LocalBroadcastManager.getInstance(DownloadService.this).sendBroadcast(intent_progress); } 

简短的回答:不要根据数据速度更新用户界面

除非您正在编写速度testing样式的应用程序,否则用户无法以此方式进行更新。

ListView非常好的优化 ,(因为你似乎已经知道,因为你正在使用ViewHolder模式)。

你有没有尝试每1秒调用notifyDataSetChanged()

每1024个字节是快速的 。 如果有人以8Mbps的速度下载, 每秒钟可以更新超过1000次 ,这肯定会导致ANR。

根据下载的金额更新进度,而不是按照不会导致UI阻止的时间间隔进行轮询。

无论如何,为了避免阻塞UI线程,您可以将更新发布到Handler

玩弄sleep的价值,以确保你不会经常更新。 你可以尝试低至200 毫秒 ,但我不会低于500毫秒 ,以确保。 确切的价值取决于你的目标设备和需要布局的项目数量。

注意:这只是一个方法来做到这一点,有很多方法来完成这样的循环。

 private static final int UPDATE_DOWNLOAD_PROGRESS = 666; Handler myHandler = new Handler() { @Override handleMessage(Message msg) { switch (msg.what) { case UPDATE_DOWNLOAD_PROGRESS: myAdapter.notifyDataSetChanged(); break; default: break; } } } private void runUpdateThread() { new Thread( new Runnable() { @Override public void run() { while ( MyFragment.this.getIsDownloading() ) { try { Thread.sleep(1000); // Sleep for 1 second MyFragment.this.myHandler .obtainMessage(UPDATE_DOWNLOAD_PROGRESS) .sendToTarget(); } catch (InterruptedException e) { Log.d(TAG, "sleep failure"); } } } } ).start(); } 

虽然它不是你的问题的答案,但是可以在你的getView()方法中完成的一个优化是这样的,而不是每次像这样创build和设置点击监听器:

 holder.downloadStateBtn.setTag(position); holder.downloadStateBtn.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { int position = (Integer) v.getTag(); // your current normal click handling } }); 

您可以只创build一次类variables,并在创build行的View

 final OnClickListener btnListener = new OnClickListener() { @Override public void onClick(View v) { int position = (Integer) v.getTag(); // your normal click handling code goes here } } 

然后在getView()

  if (v == null) { v = mLayoutInflater.inflate(R.layout.saved_show_list_item, parent, false); // your ViewHolder stuff here holder.downloadStateBtn.setOnClickListener(btnClickListener);//<<<<< v.setTag(holder); } else { holder = (ViewHolder) v.getTag(); } 

哦,不要忘了在getView()这个button上设置标签,就像你已经在做的那样:

 holder.downloadStateBtn.setTag(position);