我正在尝试创建一个包含下载任务列表的ListView
。
下载任务在Service
(DownloadService)中管理。每次收到一大块数据时,任务都会通过Broadcast
发送进度,该Fragment
由包含ListView
(SavedShowListFragment)的Broadcast
接收。收到notifyDataSetChanged()
消息后,SavedShowListFragment会更新适配器中下载任务的进度并触发ProgressBar
。
列表中的每一行都包含一个TextView
,一个Button
表示要下载的文件的标题,一个表示进度的数值,还有一个Button
暂停/下载完成后,继续下载或播放已保存的节目。
问题是暂停/恢复/播放onClick()
通常没有响应(notifyDataSetChanged()
未被调用),我认为这是因为整个列表经常使用{{1}更新(每次接收一个数据块,即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;
}
}
答案 0 :(得分:21)
当然,正如 pjco 所述,请勿以此速度更新。我建议每隔一段时间发送一次广播。更好的是,拥有一个容器来处理数据,例如进度,并通过轮询更新每个间隔。
但是,我认为在没有notifyDataSetChanged
的情况下更新列表视图也是一件好事。实际上,当应用程序具有更高的更新频率时,这是最有用的。请记住:我并不是说您的更新触发机制是正确的。
<强>解决方案强>
基本上,您需要在没有notifyDataSetChanged
的情况下更新特定位置。在以下示例中,我假设了以下内容:
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)
,但使用您的数据集进行更新且不使用参数。另外,我刚注意到你发布了一些代码。由于您使用的是Holder,您可以简单地将持有者放入功能中。我不会更新代码(我认为这是不言自明的)。
最后,只是为了强调,更改整个代码以触发进度更新。一种快速的方法是改变你的服务:用if语句包裹发送广播的代码,该语句检查上次更新是否超过一秒或半秒以及下载是否已完成(无需检查完成但是确保完成后发送更新):
在您的下载服务中
private static final long INTERVAL_BROADCAST = 800;
private long lastUpdate = 0;
现在在doInBackground中,使用if语句
包装发送的intentif(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);
}
答案 1 :(得分:9)
除非您正在编写速度测试样式应用程序,否则以这种方式更新没有用户利益。
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();
}
答案 2 :(得分: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);