Как обновить некоторые данные в Listview без использования notifyDataSetChanged()?
Я пытаюсь создать ListView
со списком загружаемых задач.
Задачи загрузки управляются в Service
(DownloadService). Каждый раз, когда принимается кусок данных, задача отправляет прогресс через Broadcast
, полученный Fragment
, содержащий ListView
(SavedShowListFragment). При получении сообщения Broadcast
SavedShowListFragment обновляет ход задач загрузки в адаптере и запускает notifyDataSetChanged()
.
Каждая строка в списке содержит ProgressBar
, a TextView
для названия загружаемого файла и одну для числового значения прогресса, а <<29 > - для приостановки/возобновления загрузки или воспроизведения сохраненное шоу после завершения загрузки.
Проблема заключается в том, что pause/resume/play Button
часто не реагирует (onClick()
не вызывается), и я думаю, что, поскольку весь список обновляется очень часто с помощью notifyDataSetChanged()
(каждый раз, когда фрагмент данных, то есть принимается 1024 байта, что может быть много раз в секунду, особенно при запуске нескольких загружаемых задач).
Я предполагаю, что могу увеличить размер блока данных в задачах загрузки, но я действительно считаю, что мой метод не оптимален вообще!
Может ли очень часто вызываться notifyDataSetChanged()
сделать пользовательский интерфейс ListView
неактивным?
Есть ли способ обновить только некоторые Views
в строках ListView
, то есть в моем случае ProgressBar
и TextView
с числовым значением прогресса, не вызывая notifyDataSetChanged()
, какие обновления весь список?
Чтобы обновить ход задач загрузки в ListView
, есть ли лучший вариант, чем "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;
}
}
Ответы
Ответ 1
Конечно, как указано в pjco, не обновляйтесь с такой скоростью. Я бы рекомендовал отправлять трансляции с интервалами. Еще лучше, у вас есть контейнер для данных, таких как прогресс и обновление каждого интервала путем опроса.
Однако, я думаю, что также полезно обновлять список в любое время без notifyDataSetChanged
. На самом деле это наиболее полезно, когда приложение имеет более высокую частоту обновления. Помните: я не говорю, что ваш механизм запуска обновления корректен.
Решение
В принципе, вам нужно обновить определенную позицию без notifyDataSetChanged
. В следующем примере я предположил следующее:
- Ваше listview называется mListView.
- Вы хотите обновить прогресс.
- В вашем индикаторе выполнения в вашем конвертируемом коде есть 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;
}
}
Примечания
Этот пример, конечно, не является полным. Вероятно, вы можете использовать следующую последовательность:
- Обновите свои данные (когда вы получите новый прогресс)
- Вызов
updateListView(int position)
, который должен использовать тот же код, но обновлять, используя ваш набор данных и без параметра.
Кроме того, я просто заметил, что у вас есть какой-то код. Поскольку вы используете держатель, вы можете просто получить держатель внутри функции. Я не буду обновлять код (я думаю, что это самоочевидно).
Наконец, просто чтобы подчеркнуть, измените весь код для запуска обновлений прогресса. Быстрый способ изменить вашу службу: обернуть код, который отправляет широковещательную рассылку, с помощью оператора 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);
}
Ответ 2
Короткий ответ: не обновлять пользовательский интерфейс на основе скорости передачи данных
Если вы не пишете приложение стиля теста скорости, у пользователя нет пользы для обновления.
ListView
очень хорошо оптимизирован (как вы, кажется, уже знаете, потому что вы используете шаблон ViewHolder).
Вы пробовали звонить notifyDataSetChanged()
каждые 1 секунду?
Каждый 1024 байта смехотворно быстрый. Если кто-то загружается со скоростью 8 Мбит/с, которая может обновлять более 1000 раз в секунду, и это может привести к ANR.
Вместо того, чтобы обновлять прогресс на основе загруженного количества, вы должны опросить сумму с интервалом, который не вызывает блокировки пользовательского интерфейса.
В любом случае, чтобы избежать блокировки потока пользовательского интерфейса, вы можете отправлять обновления в 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();
}
Ответ 3
Хотя это не ответ на ваш вопрос, а одна оптимизация, которая может быть выполнена в вашем методе getView()
, это, вместо того, чтобы создавать и настраивать прослушивание кликов каждый раз, например:
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
}
});
Вы можете просто создать его один раз как переменную класса и установить его при создании строки 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();
}
oh и не забудьте установить тег на этой кнопке в getView()
, как вы уже делаете:
holder.downloadStateBtn.setTag(position);