如何在ListView中使用Holder pattern來重用view

如何在ListView中使用Holder pattern來重用view

在ptt上面看到很多人問相同問題, 把我的經驗分享出來。
http://www.ptt.cc/bbs/AndroidDev/M.1413996267.A.CC8.html
BaseAdapter的特性就是可以重用View來節省資源,
ListView的重用結構會長這樣,


這張圖可以很清楚看到 item 9 一開始在recycle pool,
當使用者手滑動, item 1被滑出去item 9就進來listview可視範圍
item 1進入recycle pool, 照這個邏輯,
如果使用者繼續往上滑, 那麼item 2, 3…就會進入回收區,
item 9, 1, 2 …就會出現在畫面上。

這代表什麼意思? 代表著就算你資料有一百筆, 一千筆,
畫面呈現的也只有9個item, 這邊代表著view也只生成9個,
就不會佔用太多的資源。

那我怎麼知道他是第幾筆資料?
這個不用擔心 BaseAdapter幫你處理好這件事情了。
只要你資料順序塞好, 不管你是丟在怎樣的資料結構,
不管是ArrayList, Array, HashMap…等等。
只要你資料設定好, 就可以開始使用這個有趣的設計了。

程式怎麼做呢?
一開始宣告一個MyAdapter繼承BaseAdapter

public class MyAdapter extends BaseAdapter {
    @Override
    public int getCount() {
        return 0;
    }

    @Override
    public Object getItem(int arg0) {
        return null;
    }

    @Override
    public long getItemId(int arg0) {
        return 0;
    }

    @Override
    public View getView(int arg0, View arg1, ViewGroup arg2) {
        return null;
    }
}

上面你會看到四個方法, 基本上只要處理兩個方法
getCount跟getView即可,
剩下的兩個方法暫時不會用到, 有興趣的可以研究看看。

getCount就是跟adapter說你有幾筆資料要呈現,
假設現在你有100筆資料, 塞在arraylist內,

那麼我們來模擬一下

private ArrayList<Integer> mList;

public MyAdapter(){
   mList = new ArrayList<Integer>();
   for(int i = 0; i < 100; i++){
       mList.add(i);
   }
}

首先在建構子上建立一個arraylist並且塞值到這個陣列內,
接著把資料結構的長度塞給getCount方法,
讓listview知道你的資料有多長。

@Override
public int getCount() {
    return mList.size();
}

接著到重頭戲getView
這個方法就是listView會不斷的來存取, 是一個callback function,
而不是由你自己去存取,

在看一次這個方法 我把參數改成英文名稱比較好認

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    return null;
}

第一個是我們的item到哪一個位置?
第二個是我們這個item所使用的view
第三個是我們item的parent
一開始會先把目前的view抓出來 判斷他是不是被初始化過
如果有被初始化過 代表著他之前就被使用過了

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    View view = convertView;
    if(null == view){

    } else{

    }
    return view;
}

這樣一來我們就可以把這個實體化過的view傳回去給listview了。

接著如果他是空的 則對他進行初始化

要初始化之前 先取得inflate

回到建構子補上

private LayoutInflater mLayoutInflater;
public MyAdapter(Context mContext){
   mLayoutInflater = LayoutInflater.from(mContext);
   //...
}

再回到getView
那個layout就是我們item view所裝的xml布局
透過LayoutInflater可以實體化成為一個view的物件
這樣一來就可以使用程式做很多靈活的操作了。
那layout裡面長怎樣呢?

<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/text"
        android:layout_centerVertical="true"
        android:textSize="18sp"
        android:layout_marginLeft="20dp"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_toRightOf="@+id/image"
        android:textColor="#000000"/>

</RelativeLayout>

接著就要把這個textview存在holder裡面
什麼是holder?
就是幫我們記住item內所有元件的內容
先宣告一個類別來存放

class Holder{
    TextView itemText;
}

因為我們item layout很簡單 所以這樣就完成一個宣告

接著回到getView

Holder holder;
if(null == view){
    view = mLayoutInflater.inflate(R.layout.listview_item, null);
    holder = new Holder();
    holder.itemText = (TextView) view.findViewById(R.id.item_text);
    view.setTag(holder);
}

由上面可以看到 當view是空的時候
就把holder new出來 然後把itemText裝進去
在透過view設定tag 裝進holder
那麼下次view不是空的條件下 就可以直接把holder拿出來用
就完成了reuse了功效了

else{
    holder = (Holder) view.getTag();
}

這個adapter基本型態已經完成,
然後把資料結構內的資料塞進每一個item

holder.itemText.setText(mList.get(position) + "");

就會出現這樣的畫面


現在要來說明怎麼透過改變資料 來更新view
假設我們要改變某一列
假設是第2筆資料出現的地方背景就顯示紅色
那麼只要在retun view;之前加入

if(position == 2){
    view.setBackgroundColor(Color.RED);
}

就會出現第二列是紅色的


但是很奇怪 你往上滑動 居然第24列也是紅色的


這個就是reuse要注意的地方
因為listview就是把滑出去的item拿過來重用
所以那個重用的item背景還是紅色的
因此每一個item一開始都要先初始化

view.setBackgroundColor(Color.WHITE);
if(position == 2){
    view.setBackgroundColor(Color.RED);
}

這樣就正常了


想要讓一個Button按下去 某一個值進行變化
其實很簡單 只要開一個方法

public void setRowColor(int pos, int value){
   mList.set(pos, value);
   notifyDataSetChanged();
}

notifyDataSetChanged是用來刷新listview的畫面
也就是會再去讀取一圈getview
假設剛剛變化背景的判斷式改成

if(mList.get(position) == 200){
   view.setBackgroundColor(Color.RED);
}

外面設定一個button 按下的時候 將arraylist的值改變
那麼你就會看到listview有一列變紅色的
這就是用值去改變view
而不是用view去修正值

程式碼