2011-04-29

在 Android 裡使用 ListActivity, ListView, ListAdpater

Android 的 layout 主要來自 XML 設定檔,由於 XML 設定檔是靜態的,且沒有提供 for-loop 之類的語法,所以要在畫面上呈現動態資料時會有不足。

解決方法一,在 TextView 裡逐筆輸出資料,像是 在 Android 裡使用 SQLite 裡的作法,缺點是每一筆資料都只是文字,不是一個 Android 元件,很難運用在互動行為上,譬如說點擊或勾選之類的。

解決方法二,用 coding 的方式逐筆建立 Android 元件,這可以解決互動行為的問題,但缺點是用 coding 做 layout 是盡量避免的方式。

解決方法三,就是 Android List 家族上場的時候了。
簡單的概念就是,在 XML layout 裡使用一種叫做 ListView 的元件,然後在 coding 裡將一堆資料以集合物件的方式塞給 ListView,由 ListView 去做 loop 的動作,就這樣,收工。

簡單的步驟就是這樣:
data > adapter > view > activity
要特別注意的是,使用 ListView 需要兩個 XML layout 設定檔,一個設定主頁面,就是定義 ListView 所在的檔案,另一個設定每一筆 ListView 子元件的 layout,前者用在 Activity 裡,後者用在 Adapter  裡。

ListAdapter

Adapter 族譜如下:


ListAdapter 有個兄弟叫做 SpinnerAdapter,這邊先不管他,一般會用到的三個 ListAdapter 實做為 ArrayAdapter、SimpleAdapter 與 SimpleCursorAdapter。

ArrayAdapter

用 array 或 list 作為集合物件的容器,將呼叫 array 或 list 裡物件的 toString() 得到的字串寫到 ListView 裡。

ArrayAdapter 需要的資訊:
  • ListView 子元件的 layout resource id,預設限制為 TextView。
    ArraayAdapter 提供一種特別的讀取 layout resource 方式,就是指定兩個 id,第一個 id 為 layout id,第二個 id 為前面 layout id 所表示 layout 檔裡的 TextView id。
    可以實做 getView(...) 來改用其他 View 元件。
  • 一堆用 array 或 list 裝起的資料。
    也可以在建立 ArrayAdapter 之後,使用 ArrayAdapter的 API 來維護資料集合。
    或者可以用 ArraayAdapter.createFromResource(...) 這個 static method 從 resource 裡讀取 array 來建立 ArraayAdapter。
子元件設定檔 res/layout/list_item_a.xml 如下: 
<TextView 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content" 
    android:layout_height="wrap_content" 
    />
Android 提供一些子元件 layout 範本可供使用,位置為 android-sdk\platforms\android-8\data\res\layout。

Activity 程式如下:
public class A_Adapter extends ListActivity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        this.setListAdapter(this.createArrayAdapter());
    }

    private ArrayAdapter<String> createArrayAdapter() {
        String[] array = new String[] {
                "禮拜一", "禮拜二", "禮拜三", "禮拜四", "禮拜五"
        };
        return new ArrayAdapter<String>(this, R.layout.list_item_a, array);
    }
}
結果如下:


SimpleAdapter

前面的 ArrayAdapter 輸出靜態的資料到 ListView,只能透過 toString(...) 來顯示資料,且每筆資料只能顯示一個資訊是最大的缺點,SimpleAdapter 改進這個問題,接受的資料集合容器為 List<Map>,可在 Map 裡任意放入多筆需要的資料。

List 裡的每一個 Map 就是一筆資料,例如一筆通訊錄,每一個 Map 裡可以放進姓名、電話、地址等資訊。

SimpleAdapter 需要的資訊,除了 ArrayAdapter 描述的子元件的 layout resource id 與一堆用 list 裝起的資料外,另外還需要兩樣資訊:
  • Map 裡的 key 值。
  • Map 裡的 key 值對應到子元件 layout 裡的元件 id。
簡單說就是,Map 裡的姓名要放到哪個 TextView,電話要放到哪個 TextView、、、等對應資料。

子元件設定檔 res/layout/list_item_b.xml 如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:padding="10sp"
    >
    <TextView  
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:id="@+id/txt1"
        />
    <TextView  
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:id="@+id/txt2"
        />
</LinearLayout>
Activity 程式如下:
public class A_Adapter extends ListActivity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        this.setListAdapter(this.createSimpleAdapter());
    }

    private SimpleAdapter createSimpleAdapter() {
        List<Map<String, String>> data = this.createData();
        return new SimpleAdapter(this, data, R.layout.list_item_b,
                new String[] {
                        "txt1", "txt2"
                }, new int[] {
                        R.id.txt1, R.id.txt2
                });
    }

    private List<Map<String, String>> createData() {
        List<Map<String, String>> data = new ArrayList<Map<String, String>>();
        data.add(this.createMap("禮拜一", "Monday"));
        data.add(this.createMap("禮拜二", "Tuesday"));
        data.add(this.createMap("禮拜三", "Wednesday"));
        data.add(this.createMap("禮拜四", "Thursday"));
        data.add(this.createMap("禮拜五", "Friday"));
        return data;
    }

    private Map<String, String> createMap(String a, String b) {
        Map<String, String> map = new HashMap<String, String>();
        map.put("txt1", a);
        map.put("txt2", b);
        return map;
    }
}
結果如下:


SimpleCursorAdapter

前兩個 adapter 都是使用靜態的資料,相對於靜態資料的需求是從資料庫中讀取動態資料後寫到畫面,這就是 SimpleCursorAdapter 做的事情。

使用上的差別就是將靜態的 array 或者 list 換成與資料庫連結的 Cursor 物件(參考 在 Android 裡使用 SQLite),就這樣,其他的都一樣。

ListView

ListView 與 layout managers 一樣,都是繼承自 ViewGroup,用來組合多筆 View 元件成為另一個 View 元件,ListView 的特色為可 scroll 的縱向排列元件容器。


ListActivity

結合 ListAdapater 與 ListView 的地方。

ListActivity 的實做前面已經看過了,這裡要講的是 layout 設定檔。
<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
    <ListView
        android:id="@android:id/list"
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        />
    <TextView
        android:id="@android:id/empty"  
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:text="@string/empty"
        />
</LinearLayout>
兩個重點:
  • ListView 是一定要有的,另外還要提供一個 TextView,用來處理 ListAdapter 沒有任何資料時,顯示無資料的提示訊息用。
  • ListView 與 TextView 的 id 命名方式,分別為 list 和 empty,這是在程式裡寫死的,一定得叫這個,另外是 id 的前飾詞,因為這是 Android 定義的,所以得用 @android:id
Event Listener

回到本文的出發點,為 ListView 加上 Event Listeners。

ListView 的始祖 AdapterView 提供四個 Event Listener:
  • setOnClickListener(...):不能用,用了就知道原因,會丟出錯誤訊息「Don't call setOnClickListener for an AdapterView. You probably want setOnItemClickListener instead」。
  • setOnItemClickListener(...):點擊觸發。
  • setOnItemLongClickListener(...):長點擊觸發。
  • setOnItemSelectedListener(...):得用方向鍵(上或下)來觸發。
長點擊觸發 OnItemLongClickListener 之後,會再觸發 OnItemClickListener。
public class A_Adapter extends ListActivity implements OnItemClickListener,
        OnItemLongClickListener, OnItemSelectedListener {

    private static final String TAG = "A_Adapter";

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        this.setListAdapter(this.createSimpleAdapter());
        ListView lv = (ListView) this.findViewById(android.R.id.list);
        lv.setOnItemClickListener(this);
        lv.setOnItemLongClickListener(this);
        lv.setOnItemSelectedListener(this);
    }

    @Override
    public void onItemSelected(AdapterView<?> parent, View view, int position,
            long id) {
        Log.d(A_Adapter.TAG, "onItemSelected: " + view);
    }

    @Override
    public void onNothingSelected(AdapterView<?> parent) {
        Log.d(A_Adapter.TAG, "onNothingSelected: " + parent);
    }

    @Override
    public boolean onItemLongClick(AdapterView<?> parent, View view,
            int position, long id) {
        Log.d(A_Adapter.TAG, "onItemLongClick: " + view);
        return false;
    }

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position,
            long id) {
        Log.d(A_Adapter.TAG, "onItemClick: " + view);
    }

    ...
}
記得不要用 inner class 來實做 listener,每個 inner class 都會額外消耗記憶體。

沒有留言:

張貼留言