2010年12月22日 星期三

【Android】使用BaseAdapter自訂ListView

使用BaseAdapter來客製化有圖案或其他元件的ListView。

主程式 MyListView.java
public class MyListView extends ListActivity {
    //預先定義順序常數
    protected static final int MyListView_camera = 0,
        MyListView_album = 1, MyListView_map = 2;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        //設定此Activity使用的res layout
        setContentView(R.layout.list);
        
        //設定ListView未取得內容時顯示的view, empty建構在list.xml中。
        getListView().setEmptyView(findViewById(R.id.empty));
        
        //自訂方法載入ListView值
        fillData();
    }
    
    //當ListView的項目被按下
    @Override
    protected void onListItemClick(ListView l, View v, int position, long id) {
        
        //由觸發的View物件v(即按下的那一列)取得adapter的checkbox
        CheckBox cbx = (CheckBox)v.findViewById(R.id.MyAdapter_CheckBox_checkBox);
        //取得adapter的textview
        TextView title = (TextView)v.findViewById(R.id.MyAdapter_TextView_title);
        
        if(cbx.isChecked()){
            //設定可見checkbox的狀態
            cbx.setChecked(false);
            Toast.makeText(this, title.getText().toString() + "已取消核取!",
                Toast.LENGTH_SHORT).show();
        }else{
            cbx.setChecked(true);
            Toast.makeText(this, title.getText().toString() + "已核取!",
                Toast.LENGTH_SHORT).show();
        }
        super.onListItemClick(l, v, position, id);
    }
    
    void fillData(){
        //從res string.xml讀出預先寫好的字串陣列
        CharSequence[] list = getResources().getStringArray(R.array.list);
        setListAdapter(new MyAdapter(this, list));
    }
}

MyAdapter.java
package iamshiao.sample;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.TextView;

public class MyAdapter extends BaseAdapter{    
    
    private LayoutInflater myInflater;
    CharSequence[] list = null;
   
    public MyAdapter(Context ctxt, CharSequence[] list){
        myInflater = LayoutInflater.from(ctxt);
        this.list = list;
    }
    
    @Override
    public int getCount()
    {
        return list.length;
    }

    @Override
    public Object getItem(int position)
    {
        return list[position];
    }
  
    @Override
    public long getItemId(int position)
    {
        return position;
    }
  
    @Override
    public View getView(int position,View convertView,ViewGroup parent)
    {
        //自訂類別,表達個別listItem中的view物件集合。
        ViewTag viewTag;
        
        if(convertView == null){
            //取得listItem容器 view
            convertView = myInflater.inflate(R.layout.adapter, null);
            
            //建構listItem內容view
            viewTag = new ViewTag(
                    (ImageView)convertView.findViewById(
                        R.id.MyAdapter_ImageView_icon),
                    (TextView) convertView.findViewById(
                        R.id.MyAdapter_TextView_title),
                    (CheckBox) convertView.findViewById(
                        R.id.MyAdapter_CheckBox_checkBox)
                    );
            
            //設置容器內容
            convertView.setTag(viewTag);
        }
        else{
            viewTag = (ViewTag) convertView.getTag();
        }
        
        //設定內容圖案
        switch(position){
            case MyListView.MyListView_camera:
                viewTag.icon.setBackgroundResource(R.drawable.ic_launcher_camera);
                break;
            case MyListView.MyListView_album:
                viewTag.icon.setBackgroundResource(R.drawable.ic_launcher_gallery);
                break;
            case MyListView.MyListView_map:
                viewTag.icon.setBackgroundResource(R.drawable.ic_launcher_maps);
                break;
        }
        //設定內容文字
        viewTag.title.setText(list[position]);
        
        return convertView;
    }
    
    //自訂類別,表達個別listItem中的view物件集合。
    class ViewTag{
        ImageView icon;
        TextView title;
        CheckBox cbx;
    
        public ViewTag(ImageView icon, TextView title, CheckBox cbx){
            this.icon = icon;
            this.title = title;
            this.cbx = cbx;
        }
    }
}

list.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:orientation="vertical"
>
      <ListView 
        android:id="@android:id/list"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
      />
      
      <TextView
        android:id="@+id/empty"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/empty"
        android:textAppearance="?android:attr/textAppearanceLarge" 
      />
</LinearLayout>

adapter.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="wrap_content"
  android:layout_height="fill_parent"
>
    <ImageView
        android:id="@+id/MyAdapter_ImageView_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_marginLeft="10px"
    />
       
    <TextView 
        android:id="@+id/MyAdapter_TextView_title" 
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content"
        android:layout_marginLeft="15px"
        android:layout_toRightOf="@id/MyAdapter_ImageView_icon"
        android:gravity="center_vertical"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:minHeight="?android:attr/listPreferredItemHeight"
    />
      
    <CheckBox android:id="@+id/MyAdapter_CheckBox_checkBox"
        android:layout_width="wrap_content"
         android:layout_height="wrap_content" 
         android:layout_marginRight="5px"
         android:layout_alignParentRight="true"
         android:minHeight="?android:attr/listPreferredItemHeight"
         android:focusable="false"
         android:clickable="false"
    />
    
</RelativeLayout>

MyListView是清單的主要程式,Myadapter則是用來定義清單格式;所以MyListView讀取list這一個給清單用的xml檔,而Myadapter則是載入adapter這個定義每項清單內容的外觀的xml檔。

MyListView中除了設定使用的layout與listview未載入資料時使用的empty view外,最主要的工作就是以自訂方法的方式設定載入listview的內容。 在fillData()這個自訂方法中利用
CharSequence[] list = getResources().getStringArray(R.array.list);
取得預先存在values/string.xml的字串陣列。

string.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="hello">Hello World, Sample!</string>
    <string name="app_name">Sample</string>
    <string name="empty">無資料。</string>
    
    <string-array name="list">
     <item>相機</item>
     <item>相簿</item>
     <item>地圖</item>
    </string-array>
</resources>

然後利用MyListView的setListAdapter()方法載入自訂的MyAdapter實體,藉此自訂MyListView的內容格式。

MyListView的另一個重點是
//當ListView的項目被按下
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
    //由觸發的View物件v(即按下的那一列)取得adapter的checkbox
    CheckBox cbx = (CheckBox)v.findViewById(R.id.MyAdapter_CheckBox_checkBox);
    //取得adapter的textview
    TextView title = (TextView)v.findViewById(R.id.MyAdapter_TextView_title);
        
    if(cbx.isChecked()){
        //設定可見checkbox的狀態
        cbx.setChecked(false);
        Toast.makeText(this, title.getText().toString() + "已取消核取!",
            Toast.LENGTH_SHORT).show();
    }else{
        cbx.setChecked(true);
        Toast.makeText(this, title.getText().toString() + "已核取!",
            Toast.LENGTH_SHORT).show();
    }
    super.onListItemClick(l, v, position, id);
}
在這個部分由於我在adapter.xml中將CheckBox設定成無法核取也無法聚焦,
<CheckBox android:id="@+id/MyAdapter_CheckBox_checkBox"
    ...
    android:focusable="false"
    android:clickable="false"
/>
再另用onListItemClick這個事件讓項目被按下時再去核取內容的CheckBox,如果不這麼設定的話,項目的按下與CheckBox的按下將被視為不同兩件事,當然如果你的需求本來就是要視為兩個不同元件分開按,就不必做上述設定。
在繼承自BaseAdapter的MyAdapter中,我先利用建構子取得必須的Context跟要填入的字串陣列,然後把規定要填入的getCount()之類的方法填一填。 之後,在getView這個方法中我們要設定每一個清單項目的內容物。

首先我先用一個自訂類別來表達每一個list項目的內容...
//自訂類別,表達個別listItem中的view物件集合。
class ViewTag{
    ImageView icon;
    TextView title;
    CheckBox cbx;
    
    public ViewTag(ImageView icon, TextView title, CheckBox cbx){
        this.icon = icon;
        this.title = title;
        this.cbx = cbx;
    }
}
之後再getView中設定convertView的layout為前面提到的adapter.xml,完成後宣告一個此類別的變數並且利用convertView的取得的內容來榜定ViewTag變數的屬性與對應的adapter內容元件,最後再利用setTag()指定convertView內容為ViewTag變數。
@Override
public View getView(int position,View convertView,ViewGroup parent)
{
    //自訂類別,表達個別listItem中的view物件集合。
    ViewTag viewTag;
        
    if(convertView == null){
        //取得listItem容器 view
        convertView = myInflater.inflate(R.layout.adapter, null);
            
        //建構listItem內容view
        viewTag = new ViewTag(
            (ImageView)convertView.findViewById(
                R.id.MyAdapter_ImageView_icon),
            (TextView) convertView.findViewById(
                R.id.MyAdapter_TextView_title),
            (CheckBox) convertView.findViewById(
                R.id.MyAdapter_CheckBox_checkBox)
            );
            
        //設置容器內容
        convertView.setTag(viewTag);
    }
    else{
        viewTag = (ViewTag) convertView.getTag();
    }
    ...
}

完成內容榜定後我們要接著做內容的填入,利用在MyListView中宣告的常數我們可以區別出哪張圖片應該要載入進第幾個清單項目的icon中...
//設定內容圖案
switch(position){
    case MyListView.MyListView_camera:
        viewTag.icon.setBackgroundResource(R.drawable.ic_launcher_camera);
        break;
    case MyListView.MyListView_album:
        viewTag.icon.setBackgroundResource(R.drawable.ic_launcher_gallery);
        break;
    case MyListView.MyListView_map:
        viewTag.icon.setBackgroundResource(R.drawable.ic_launcher_maps);
        break;
}
還有填入標題
viewTag.title.setText(list[position]);

以上就完成了一個客製化的ListView,另外要特別注意當ListView項目大於畫面所能承載(一般是6個項目),那隨著捲軸捲動,CheckBox等控制項元件的狀態會流失(已勾選的會被消掉)。 你必須主動在adapter中撰寫狀態記憶用的程式,這個部分之後如果有寫SQLite搭配ListView的教學會再詳述。

完整範例下載

6 則留言:

  1. 請問我要分開處理check listview 和 checkbox 我可以怎樣做呢?

    在這個部分由於我在adapter.xml中將CheckBox設定成無法核取也無法聚焦,
    再另用onListItemClick這個事件讓項目被按下時再去核取內容的CheckBox,如果不這麼設定的話,項目的按下與CheckBox的按下將被視為不同兩件事,當然如果你的需求本來就是要視為兩個不同元件分開按,就不必做上述設定。

    回覆刪除
    回覆
    1. 一直沒注意到有留言...
      基本上我沒這麼做過,不過應該只要checkBox有Listener就可以處理了吧?

      刪除
  2. 照key了
    getListView 跟
    setListAdapter
    永遠都有紅線 compiler都不過!...

    回覆刪除
  3. 能否直接以e-mail跟你連絡
    想請教一些問題

    回覆刪除
  4. 我把checkbox的部分改成了radiogroup
    但是執行是一片空白

    回覆刪除