【问题标题】:How to update some data in a Listview without using notifyDataSetChanged()?如何在不使用 notifyDataSetChanged() 的情况下更新 Listview 中的某些数据?
【发布时间】:2023-03-10 22:24:01
【问题描述】:

我正在尝试创建一个带有下载任务列表的ListView。

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

列表中的每一行都包含一个ProgressBar,一个TextView 表示正在下载的文件的标题,一个表示进度的数值,还有一个Button 用于暂停/恢复下载或播放下载完成后保存的节目。

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

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

频繁调用notifyDataSetChanged() 是否会导致ListView UI 无响应?

有没有办法只更新ListView 行中的一些Views,即在我的情况下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;
    }
} 

【问题讨论】:

  • (我不是专家,只是一个学生)我认为progressBar.setProgress 足以更新列表,我错了吗? ...这个链接有帮助吗? stackoverflow.com/questions/11200299/…
  • 我的问题不是关于如何设置进度,而是如何只更新 ListView 行中的一些视图。还是谢谢。
  • 如果我正确理解您的问题,您的问题是 notifyDataSetChanged 将在 1 秒内被多次调用。我不知道如何在不使用 notifyDataSetChanged 的​​情况下更新 ListView UI,但在这种情况下,我会每 4 ~ 6 秒调用一次 notifyDataSetChanged,而不是每更改一次数据。
  • @edisonthk 是的,我可以这样做,但是每 4-6 秒更新一次进度条不是很好!如果我找不到任何其他解决方案,这可能就是我会做的......
  • @jul 没必要一定是4-6s。假设每 2 毫秒下载一次进程更新,UI 线程在每次服务更新时保持更新将非常繁重。为了克服这个问题,我会让 UI 线程每 100 毫秒或 500 毫秒更新一次,而不是 2 毫秒,而 500 毫秒对人类来说已经足够快了。

标签: android listview notifydatasetchanged


【解决方案1】:

当然,正如 pjco 所说,不要以那种速度更新。我建议每隔一段时间发送广播。更好的是,有一个容器来存储进度等数据,并通过轮询来更新每个间隔。

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


解决方案

基本上,您需要在没有notifyDataSetChanged 的情况下更新特定位置。在以下示例中,我假设如下:

  1. 您的列表视图称为 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) 应该使用相同的代码,但使用您的数据集进行更新,并且不使用参数。

此外,我刚刚注意到您发布了一些代码。由于您使用的是 Holder,因此您可以简单地在函数中获取 holder。我不会更新代码(我认为它是不言自明的)。

最后,强调一下,更改触发进度更新的整个代码。一种快速的方法是更改​​您的服务:使用 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);
}

【讨论】:

  • 我们是否以这种方式实现自己的notifyDataSetChanged()?检查答案 - stackoverflow.com/a/12234498/609782,因为即使 notifyDataSetChanged() 仅在可见的项目上调用 getView()。
  • @Darpan notifyDataSetChanged() 仅对可见项目调用 getView() ;真的。但请注意,它会破坏所有可用的视图并将 null convertViews 传递给 getView()。换句话说,可见视图被重新创建。
  • 我也是这么说的,但我明白你的意思。区别在于重新创建“所有”视图和“更新”仅“一个”视图,就像您对代码所做的那样。不错。
【解决方案2】:

简短的回答:不要根据数据速度更新 UI

除非您正在编写速度测试风格的应用程序,否则以这种方式更新对用户没有任何好处。

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

您是否尝试过每 1 秒呼叫一次notifyDataSetChanged()?

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

您应该以不会导致 UI 阻塞的时间间隔轮询下载量,而不是根据下载量更新进度。

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

使用sleep 的值来确保您不会过于频繁地更新。您可以尝试低至 200ms,但可以肯定的是,我不会低于 500ms。确切的值取决于您的目标设备以及需要布局传递的项目数量。

注意:这只是一种方法,有很多方法可以完成这样的循环。

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

    哦,别忘了在getView() 中的这个按钮上设置标签,就像你已经在做的那样:

    holder.downloadStateBtn.setTag(position);
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-05-04
      • 1970-01-01
      • 2017-06-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多