標籤:
很多時候我們都通過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() {@Overridepublic void run() {for (int i = 1; i < 101; i++) {final int progress = i;runOnUiThread(new Runnable() {@Overridepublic 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;}@Overridepublic int getCount() {return mTasks.size();}@Overridepublic Object getItem(int position) {return mTasks.get(position);}@Overridepublic long getItemId(int position) {return position;}@Overridepublic 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()去處理了。
ListView嵌套ProgressBar更新方式