Подтвердить что ты не робот

Как обновить некоторые данные в 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;
    }
} 
4b9b3361

Ответ 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);