Android Adapter淺談

本文部分內容啓發於:http://www.cnblogs.com/allin/archive/2010/05/11/1732200.html


首先上類圖:



一、在程序開發中,我們常用到ListView,使用ListView需要三個元素:

    1、ListView控件:展示每條數據的框架,是一個ViewGroup;

    2、Adapter適配器:連接數據與展示View的中介,通知ListView繪製多少View,每條View怎麼繪製,View內部事件如何響應;

    3、數據:具體被映射的字符串、圖片、狀態等信息,有特定的格式要求,具體視Adapter實現而定;


二、適配器Adapter的基礎功能定義在接口Adapter中,包括10個方法:

    1、registerDataSetObserver/unregisterDataSetObserver:註冊/撤銷Adapter關聯數據發生變化的監察者,註冊後可以完成重繪界面等操作;

    2、getCount:返回當前Adapter需要繪製多少條數據,ListView按返回值繪製N行View;

    3、getItem(int position):返回當前Adapter持有的對應position的數據,返回值對應ListView繪製的某一行展現的數據;

    4、getItemId(int position):返回當前Adapter對應位置的數據的唯一標識id。通常如果沒有就返回position,如果是數據庫內讀出的數據就返回主鍵,如果item對應有int的唯一標識符就返回該值,該值如果設置不正確,可能會導致ListView的onItemClick內的操作對象不是你要處理的對象,比如你刪某個item,卻刪了另一個,或者刪除不起作用;

    5、hasStableIds:返回當前Adapter持有的數據實體們是否有穩定的唯一標識id,比如4中講到的返回position是不穩定的,可能隨着數據變化導致其值變化,此時需要          返回false,如果返回主鍵或者唯一標識符則該方法可以返回true。其作用是:返回true,ListView會依據4中的返回的id來確定當前顯示哪條內容,也就是firstVisibleChild的位置。

    6、getView(int position, View convertView, ViewGroup parent):返回當前Adapter對應position應該繪製的View,這個View可以千奇百怪,需要配套getItemViewType使用,一個列表內所有數據展示都一模一樣的就不需要了;

    7、getItemViewType(int position):返回當前Adapter對應position位置數據的View的類型,比如你第一行繪製了一個TextView,第二行繪製了一個ImageView,在ListView滾動的時候,第二行肯定不能複用來顯示第一行應該顯示的數據,這時候就讓它們返回不同的ViewType,ListView控件就不會不識趣了,這裏需要注意AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER = -2 已經被佔用了,ITEM_VIEW_TYPE_IGNORE = -1 也沒了,所以我們用正數一般都沒事;

    8、getViewTypeCount:返回當前Adapter持有多少種ViewType,6中返回了多少種,這裏寫總數就行;

    9、isEmpty:返回當前Adapter是否持有數據,官方文檔說法是:它用於判斷空的View是否需要展示,如果沒有headers和footers,返回getCount==0就好了,否則可以自定義;


    這些裏面我們一般情況下主要和getCount、getItem(int position)、getItemId(int position)、getView打交道,因爲BaseAdapter已經實現了上面的其他方法;


三、Adapter的繼承者包括ListAdapter和SpinnerAdapter,也是接口。

    1、 其中ListAdapter連接ListView和數據,增加了判斷ListView中繪製的View是否是分隔符的接口,定義了兩個方法:

        1)areAllItemsEnabled:返回true標識所有的item對應的View都是可點擊可選擇的,也就是沒有分隔符;

        2)isEnabled(int position):返回true標識當前Adapter對應position位置數據不是一個分隔符,因爲分隔符不可點擊且不可選擇;

    2、 SpinnerAdapter連接Spinner和數據,獲取Spinner的下拉框內的顯示控件,只定義了一個方法:

        1)getDropDownView(int position, View convertView, ViewGroup parent):返回當前Adapter對應position應該繪製的下拉框內的View;


四、擴充的接口往下就是我們比較熟悉的類BaseAdapter了,還有個擴展接口WrapperListAdapter。

    1、 我們先講WrapperListAdapter,它定義了一個包裹另一個ListAdapter的類,增加了一個實現方法:

        1)getWrappedAdapter:返回被包裹的ListAdapter;

        在android系統內,系統只提供了它的一個實現類——HeaderViewListAdapter,也就是包含Header View和Footer View的ListAdapter,其內部維持了列表頭尾的增加刪除等操作,且封裝了一層ListAdapter,從而爲ListView提供列表頭尾的管理與數據關聯工作。該類還實現了Filterable接口,對外提供篩選功能(自動匹配功能,就是電話簿中輸入號碼查找出合適聯繫人的功能);

    2、 下面我們詳細講一下BaseAdapter,因爲它是我們常用到的ArrayAdapter、SimpleAdapter、CursorAdapter和自定義Adapter的父親。

        BaseAdapter非常簡單,絕大部分都是接口的默認實現,它持有一個android.database.DataSetObservable的對象,通過該對象註冊/註銷數據觀察者,並在接口之外提供了兩個方法:

            1)notifyDataSetChanged:外部或內部調用通知數據觀察者適配器內持有的數據發生了改變,view監聽到該事件後可以刷新界面;

            2)notifyDataSetInvalidated:外部或內部調用通知數據觀察者適配器內持有的數據已經失效,注意:按照官方解釋,該方法一旦調用,這個Adapter就失效了,也就不能接着報告數據變化事件;

        這兩個方法都調用DataSetObservable對象的方法實現;

        BaseAdapter的其他默認實現包括:

            1)hasStableIds 返回false,也就是默認itemId不穩定,如果我們有穩定id,可以覆寫爲true;

            2)areAllItemsEnabled 返回true,也就是默認沒有分隔符,所有item都可點擊可選擇;

            3)isEnabled(int position) 返回true,同2;

            4)getDropDownView 調用了getView;

            5)getItemViewType(int position) 返回0,意味着默認情況下所有我們通過getView生成的View都是同類型的;

            6)getViewTypeCount 返回1,依據5;

            7)isEmpty 返回getCount==0,因爲沒有持有列表頭尾;


五、BaseAdapter的繼承者們就是我們做界面開發直接接觸的類了,它們包括:ArrayAdapter<T>、SimpleAdapter、CursorAdapter和自定義Adapter。這裏略過CursorAdapter,沒有用過,以後看機會再補上。

    1、ArrayAdapter

        ArrayAdapter只能用於顯示一行文字,用法一般如下:

	mAdapter = new ArrayAdapter<String>(getApplicationContext(), android.R.layout.simple_spinner_item, getData());
        mListView.setAdapter(mAdapter);
        該類內置add,insert, remove, clear等方法,實現了BaseAdapter沒有搞定的4個方法,實現了自動匹配Filterable接口,還提供了兩個額外的方法:

            1)setDropDownViewResource(int resource):供Spinner使用,設置下拉框顯示字符串的View資源樣式,也就是一個xml文件,文件裏只能定義一個TextView的子類,不能加LinearLayout之類的,要注意。

            2)setNotifyOnChange(boolean notifyOnChange):設置爲false可以阻止界面知道數據發生了變化,直到外部調用notifyDataSetChanged將mNotifyOnChange開關打開爲止;

    2、SimpleAdapter

        SimpleAdapter相對而言比ArrayAdapter自由很多,我們可以用自己的定義的界面,可以展現很多種數據,包括選中狀態、文字、圖片等;它的缺點就是數據格式太死板,我們必須使用List<? extends Map<String, ?>>作爲數據源,這個轉化過程很沒有美感,且耗時;

        android實現的SimpleAdapter默認只允許待顯示對象爲Checkable、TextView、ImageView的數據,用法一般如下:

            1)佈局:

<?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="wrap_content"
    android:orientation="horizontal" >

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_launcher" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical" >

        <CheckBox
            android:id="@+id/checkBox"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="#FF000000" />
    </LinearLayout>

</LinearLayout>

            2)代碼

package com.example.testlistview;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;
import android.widget.ArrayAdapter;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.SimpleAdapter;

public class MainActivity extends Activity {
    private ListView mListView = null;
    private BaseAdapter mAdapter = null;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
        
        mAdapter = new SimpleAdapter(getApplicationContext(), getData(), R.layout.simple_adapter_demo, 
                new String[] {"text", "image", "checkable"},
                new int[] {R.id.textView, R.id.imageView, R.id.checkBox});
        mListView.setAdapter(mAdapter);
        
    }

    private List<? extends Map<String, ?>> getData() {
        ArrayList<HashMap<String, Object>> list = new ArrayList<HashMap<String,Object>>();
        HashMap<String, Object> map1 = new HashMap<String, Object>();
        map1.put("text", "nihao");
        map1.put("image", android.R.drawable.ic_secure);
        map1.put("checkable", false);
        list.add(map1);
        
        HashMap<String, Object> map2 = new HashMap<String, Object>();
        map2.put("text", "android");
        map2.put("image", android.R.drawable.ic_delete);
        map2.put("checkable", true);
        list.add(map2);
        return list;
    }

    private void initView() {
        mListView = (ListView) findViewById(R.id.listview);
    }


}

       3) 效果:


        當然,SimpleAdapter也不是隻能顯示這三種數據類型,它有個內部接口ViewBinder,只要自己實現一個ViewBinder的子類,並通過setViewBinder(ViewBinder viewBinder)賦值給SimpleAdapter,我們也可以定義對其他的控件進行賦值。這是SimpleAdapter的擴展性。

不過,說實話瑜不掩瑕,如果數據格式不是Map的,不建議用SimpleAdapter,費腦筋。


六、萬能的自定義Adapter

    好了,我們爲了講解ListView的適配器Adapter,從Adapter接口往下講了四層了,到了這裏,就是故事的高潮部分。我們講自定義Adapter,到目前爲止,能裝在ListView裏面的東西只有Checkable、String和Bitmap,這明顯不夠用,但是android就給了這麼點功能,所以我們就得自己動手DIY了。

    自定義Adapter,一般都繼承BaseAdapter,因爲BaseAdapter替我們實現了Adapter的基本功能,包括數據變化通知、部分接口實現等等,雖然ListView的setAdapter方法只要實現了ListAdapter接口的就可以用,但是有現成的,我們幹嘛要再實現一遍那十幾個接口呢,對吧!~

    我們先說一下自定義Adapter的好處:

        1)數據存儲方式靈活:隨便什麼格式的,只要自己知道格式,就能往裏面放;

        2)界面展現方式靈活:你可以任意佈局擺弄數據,可以加自定義的分隔符,可以加按鈕等等等等;


    好了,好處說完了就開始動手了。真寫起來,其實自定義Adapter沒有啥特別的,繼承BaseAdapter,如前文所敘,裏面有四個方法是需要我們搞定的,分別是:

        1)getCount(),返回傳入我們定義的Adapter內的數據列表長度,用於通知ListView繪製多少行。

        2)getItem(int position),返回傳入的數據列表的第position個數據。

        3)getItemId(int position),返回傳入的數據列表的第position個數據對應的唯一標識符,強烈建議不要返回position。

        4)getView(int position, View convertView, ViewGroup parent),返回傳入的數據列表的第position個數據對應在ListView的行內應該怎麼繪製,我們甚至可以判斷一下position,然後在第三行繪製一個ImageView,展現一個深情款款的大豬頭。當然,如果其他各行都不是ImageView的話,我們一定要覆寫getItemViewType(int position)和getViewTypeCount(),因爲BaseAdapter默認每行類型都是0,總的TypeCount爲1。

        這裏有個兩個需要注意的地方

            1)convertView會爲當前position帶來一個可用的view,是給我們複用的,作用是當滾動導致ListView內某行View不可見時,直接複用該View來展示第position個數據,這樣能大大減少內存使用量,不管要顯示的數據是不是幾千條,一般顯示一行字的ListView最多創建10幾個View就可以滿足顯示要求了,所以一定得用上。

            2)View有個setTag方法,而我們使用findViewById是要查找View樹的,相對而言,創建一個內部類ViewHolder並設爲View的Tag,會在複用convertView時直接提供對象引用(如TextView、ImageView等等),所以ViewHolder也應該用上。


        下面上代碼:

            1)佈局my_adapter_demo.xml

<?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="wrap_content"
    android:orientation="horizontal" >

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_launcher" />

    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:orientation="vertical" >

        <CheckBox
            android:id="@+id/checkBox"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="#FF000000" />
    </LinearLayout>
    
    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="刪除" />

</LinearLayout>

            2)數據體Data.java

package com.example.testlistview;

import android.graphics.Bitmap;

public class Data {
    private int mId = -1;
    private String mText = null;
    private Bitmap mImage = null;
    private boolean mIsChecked = false;
    
    public Data(int mId, String mText, Bitmap mImage, boolean mIsChecked) {
        super();
        this.mId = mId;
        this.mText = mText;
        this.mImage = mImage;
        this.mIsChecked = mIsChecked;
    }

    public int getId() {
        return mId;
    }

    public void setId(int mId) {
        this.mId = mId;
    }

    public String getText() {
        return mText;
    }

    public void setText(String mText) {
        this.mText = mText;
    }

    public Bitmap getImage() {
        return mImage;
    }

    public void setImage(Bitmap mImage) {
        this.mImage = mImage;
    }

    public boolean isChecked() {
        return mIsChecked;
    }

    public void setIsChecked(boolean mIsChecked) {
        this.mIsChecked = mIsChecked;
    }
    
    

}

            3)自定義Adapter   MyAdapter.java

package com.example.testlistview;

import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

public class MyAdapter extends BaseAdapter {
    private static final String TAG = "MyAdapter";
    private List<Data> mList = null;
    private LayoutInflater mInflater = null; 
    private Context mContext = null;
    
    public MyAdapter(Context context, List<Data> list) {
        if(list == null) {
            list =new ArrayList<Data>();
        }
        mList = list;
        mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        mContext = context;
    }

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

    @Override
    public Object getItem(int position) {
        return mList.get(position);
    }

    @Override
    public long getItemId(int position) {
        //最好返回Data對應的唯一標識id
        return mList.get(position).getId();
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        Data data = mList.get(position);
        
        View view = null;
        ViewHolder holder = null;
        
        if(null == convertView) {
            view = mInflater.inflate(R.layout.my_adapter_demo, null);
            holder = new ViewHolder();
            holder.id = data.getId();
            holder.textView = (TextView) view.findViewById(R.id.textView);
            holder.imageView = (ImageView) view.findViewById(R.id.imageView);
            holder.checkBox = (CheckBox) view.findViewById(R.id.checkBox);
            holder.button = (Button) view.findViewById(R.id.button);
            //將持有的控件引用存入View的Tag內
            view.setTag(holder);
        } else {
            //複用convertView,提高ListView的效率,減少內存佔用
            view = convertView;
            //讀出控件引用
            holder = (ViewHolder) view.getTag();
            Log.d(TAG, holder.id + " is now display " + data.getId());
            holder.id = data.getId();
        }
        
        holder.textView.setText(data.getText());
        holder.imageView.setImageBitmap(data.getImage());
        holder.checkBox.setChecked(data.isChecked());
        holder.button.setOnClickListener(new OnClickListener() {
            
            @Override
            public void onClick(View v) {
                Toast.makeText(mContext, "按鈕被點擊了", Toast.LENGTH_SHORT).show();
            }
        });
        return view;
    }
    
    @Override
    public int getItemViewType(int position) {
        if(position == 2) {
            return 1;
        }
        return super.getItemViewType(position);
    }
    
    @Override
    public int getViewTypeCount() {
        return 2;
    }
    
    private final class ViewHolder {
        int id = -1;
        TextView textView = null;
        ImageView imageView = null;
        CheckBox checkBox = null;
        Button button = null;
    }

}

            4)主界面 MainActivity.java

package com.example.testlistview;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import android.os.Bundle;
import android.app.Activity;
import android.graphics.BitmapFactory;
import android.view.Menu;
import android.widget.ArrayAdapter;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.SimpleAdapter;

public class MainActivity extends Activity {
    private ListView mListView = null;
    private BaseAdapter mAdapter = null;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
        
        mAdapter = new MyAdapter(this, getData());
        mListView.setAdapter(mAdapter);
        
    }

    private List<Data> getData() {
        ArrayList<Data> list = new ArrayList<Data>();
        Data data1 = new Data(1, "nihao",
                BitmapFactory.decodeResource(this.getResources(), android.R.drawable.ic_secure), false);
        list.add(data1);
        
        Data data2 = new Data(2, "android",
                BitmapFactory.decodeResource(this.getResources(), android.R.drawable.ic_delete), true);
        list.add(data2);
        
        Data data3 = new Data(3, "nihao",
                BitmapFactory.decodeResource(this.getResources(), android.R.drawable.ic_secure), false);
        list.add(data3);
        
        Data data4 = new Data(4, "android",
                BitmapFactory.decodeResource(this.getResources(), android.R.drawable.ic_delete), true);
        list.add(data4);
        
        Data data5 = new Data(5, "nihao",
                BitmapFactory.decodeResource(this.getResources(), android.R.drawable.ic_secure), false);
        list.add(data5);
        
        Data data6 = new Data(6, "android",
                BitmapFactory.decodeResource(this.getResources(), android.R.drawable.ic_delete), true);
        list.add(data6);
        
        Data data7 = new Data(7, "nihao",
                BitmapFactory.decodeResource(this.getResources(), android.R.drawable.ic_secure), false);
        list.add(data7);
        
        Data data8 = new Data(8, "android",
                BitmapFactory.decodeResource(this.getResources(), android.R.drawable.ic_delete), true);
        list.add(data8);
        
        Data data9 = new Data(9, "nihao",
                BitmapFactory.decodeResource(this.getResources(), android.R.drawable.ic_secure), false);
        list.add(data9);
        
        Data data10 = new Data(10, "android",
                BitmapFactory.decodeResource(this.getResources(), android.R.drawable.ic_delete), true);
        list.add(data10);
        
        Data data11 = new Data(11, "nihao",
                BitmapFactory.decodeResource(this.getResources(), android.R.drawable.ic_secure), false);
        list.add(data11);
        
        Data data12 = new Data(12, "android",
                BitmapFactory.decodeResource(this.getResources(), android.R.drawable.ic_delete), true);
        list.add(data12);
        
        Data data13 = new Data(13, "nihao",
                BitmapFactory.decodeResource(this.getResources(), android.R.drawable.ic_secure), false);
        list.add(data13);
        
        Data data14 = new Data(14, "android",
                BitmapFactory.decodeResource(this.getResources(), android.R.drawable.ic_delete), true);
        list.add(data14);
        
        Data data15 = new Data(15, "nihao",
                BitmapFactory.decodeResource(this.getResources(), android.R.drawable.ic_secure), false);
        list.add(data15);
        
        Data data16 = new Data(16, "android",
                BitmapFactory.decodeResource(this.getResources(), android.R.drawable.ic_delete), true);
        list.add(data16);

        return list;
    }

    private void initView() {
        mListView = (ListView) findViewById(R.id.listview);
    }


}

        通過這個例子我們可以驗證,不同ViewType的View是不會作爲convertView使用的,另外往下滾動時,上方不可見的View會被複用來顯示下方將要顯示出來的數據。


    好了,整個Android的ListView Adapter就介紹到這裏,有後續需要注意的地方,我會接着這篇文章往下寫的。










發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章