很多时候我们都通过BaseAdapter.getView()中的convertView来提高ListView的性能,这个时候如果我的的ListView的Item里有一个正在更新ProgressBar,结果就悲惨了。。。 滑动界面时并没有达到我们想要的效果。解决这个问题其实很容易,在数据集中保存一下更新的进度,然后在getView中不断去设置进度。
还有一个问题就是,当有进度更新的时候,我们是要不断mAdapter.notifyDatasetChanged()来更新ListView吗?这样做当然可以,但是效率极其低下,要知道notifyDatasetChanged的代价是非常高的。那么,有没有更好的方式呢?当然有,那就是手动更新item的方式。
下面,我们来一步步的实现一个最佳的更新方式,至于何时选用手动更新,何时选用notify,我认为在有数据频繁更新或者只需要更新一条数据的时候要选择前者,普通的情况还是选择notify更容易一些。
首先,我们来建立一个表示任务的实体类:
public class Task { private String name; private boolean isDownload; private int progress; public Task() {} public Task(String name) { this.name = name; } ... setter and getter ... }
很容易理解,name表示任务的标题, isDownload表示是否正在下载,我们下面会通过isDownload来控制ProgressBar的显示和隐藏,progress当然就是进度了。
ListView的布局我们来看一下:
<ListView android:id="@+id/list" android:layout_width="wrap_content" android:layout_height="wrap_content" android:cacheColorHint="@android:color/transparent" android:divider="@android:color/darker_gray" android:dividerHeight="1dp" android:listSelector="@android:color/transparent" />
还有ListView的Item的布局:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingTop="10dp" android:paddingBottom="10dp" android:minHeight="?android:attr/listPreferredItemHeight" android:orientation="vertical" > <TextView android:id="@+id/item_name" android:layout_width="match_parent" android:layout_height="wrap_content" /> <ProgressBar android:id="@+id/item_progress" style="?android:attr/progressBarStyleHorizontal" android:layout_width="match_parent" android:layout_height="wrap_content" android:visibility="invisible" /> </LinearLayout>
都是最简单的布局,我们直接略过,Item的布局包含一个标题和一个进度条。
如何来模拟下载过程呢? 看下面代码:
private void download(final int positionInAdapter) { ... new Thread(new Runnable() { @Override public void run() { for (int i = 1; i < 101; i++) { final int progress = i; runOnUiThread(new Runnable() { @Override public void run() { publishProgress(positionInAdapter, progress); } }); SystemClock.sleep(500); } } }).start(); }
这段代码的意思是每隔500ms更新一下进度,而positionInAdapter这个参数表示我们点击的这个item在Adapter中的位置(也就是在数据集中的位置)。
在来看看Adapter怎么写的,
public class MyAdapter extends BaseAdapter { private Context mContext; private ArrayList<Task> mTasks; public MyAdapter(Context context, ArrayList<Task> tasks) { mContext = context; mTasks = tasks; } @Override public int getCount() { return mTasks.size(); } @Override public Object getItem(int position) { return mTasks.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { final ViewHolder holder; if(convertView == null) { convertView = View.inflate(mContext, R.layout.item, null); holder = new ViewHolder(); holder.name = (TextView) convertView.findViewById(R.id.item_name); holder.progress = (ProgressBar) convertView.findViewById(R.id.item_progress); convertView.setTag(holder); }else { holder = (ViewHolder) convertView.getTag(); } holder.name.setText(mTasks.get(position).getName()); if(mTasks.get(position).isDownload()) { holder.progress.setVisibility(View.VISIBLE); holder.progress.setProgress(mTasks.get(position).getProgress()); }else { holder.progress.setVisibility(View.INVISIBLE); } return convertView; } static class ViewHolder { TextView name; ProgressBar progress; } }
一个最普通的Adapter写法,稍微值得注意的是40~45行,我们根据isDownload来判断是否显示和更新ProgressBar。
到这里我们的基本工作就算做完了,下面就是要在Activity中来更新ListView的item了,而不是通过notifyDatasetChanged方法。
在Activity中,我们通过ListView的ItemClick来实现模拟下载,在ItemClick中我们调用download方法,来看看download中我们省略的那些代码。
private void download(final int positionInAdapter) { mTasks.get(positionInAdapter).setDownload(true); if(positionInAdapter >= mListView.getFirstVisiblePosition() && positionInAdapter <= mListView.getLastVisiblePosition()) { int positionInListView = positionInAdapter - mListView.getFirstVisiblePosition(); ProgressBar item = (ProgressBar) mListView.getChildAt(positionInListView) .findViewById(R.id.item_progress); item.setVisibility(View.VISIBLE); } ... }
上来,我们先去更新一下task列表中表示下载的boolean变量,表示该条数据正在下载。
下面一个判断是关键,我们需要判断当前点击该条item是否在ListView的可见域内(这个判断其实是多余的,既然能点击到,肯定是可见了,但是为了严谨,我们还是加了这个判断),这里为什么要这么判断呢?来看下图。
在该图中,我们ListView第一个可见项对应在数据集中的位置应该是2,但是0和1是不可见的。所以,我们只需要判断一下,当前数据集中的位置是否在ListView.getFirstVisibleItem()和ListView.getLastVisibleItem()之间就可以判断出该item是否处于可见区域内了。
接下来第5行,我们通过positionInAdapter - mListView.getFirstVisibleItem()来获取当前item在ListView中的位置,如果我们点击对应图片中的4号位置,那么我们需要更新ListView中第4-2=2的item。现在我们又可以获取ListView的Item了,当然我们就可以获取该Item中的ProgressBar了,6~8行,我们精确的获取到了ProgressBar,并且更新ProgressBar为显示状态。
到目前为止,可以通过ItemClick来实现控制ProgressBar的显示了,当然,我们没有使用notifyDatasetChanged()。
接下来,我们来看看是如果更新进度的。更新进度的过程主要在publishProgress方法中。
public void publishProgress(final int positionInAdapter, final int progress) { // mTasks.get(positionInAdapter).setDownload(true); mTasks.get(positionInAdapter).setProgress(progress); if(positionInAdapter >= mListView.getFirstVisiblePosition() && positionInAdapter <= mListView.getLastVisiblePosition()) { int positionInListView = positionInAdapter - mListView.getFirstVisiblePosition(); ProgressBar item = (ProgressBar) mListView.getChildAt(positionInListView) .findViewById(R.id.item_progress); item.setProgress(progress); } }
代码和上面的非常相似,其实原理也是一样的。
首先来看看这个方法的参数:positionInAdapter表示在数据集中的位置,progress表示进度,这个不难理解。
接下来,我们通过更新task任务列表中对应条目的progress来保存一下现在的进度,但是,我们没有notify。
接下来还是同样的逻辑,只不过,我们把控制ProgressBar显示的部分替换成了更新ProgressBar进度了。
这样,我们就实现了,在不调用notifyDatasetChanged()的情况下来更新ListView的Item的目的。这样做有一个很明显的好处就是,每次,我们只更新一条item,其他的item我们并没有去更新,而notifyDatasetChanged的实现方式是,保存当前的位置,并更新所有的item,然后恢复位置。这样一比较,我们这种方式的优势就体现出来了。
有人可能还会有疑问,我们在滑动的时候怎么处理呢? 别忘了,我们在更新的前面都在task列表中更新了数据,所以在滑动的时候,我们交给Adapter.getView()去处理了。