Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 使用數據源碼解析Android中的Adapter、BaseAdapter、ArrayAdapter、SimpleAdapter和SimpleCursorAdapter

使用數據源碼解析Android中的Adapter、BaseAdapter、ArrayAdapter、SimpleAdapter和SimpleCursorAdapter

編輯:關於Android編程

Adapter相當於一個數據源,可以給AdapterView提供數據,並根據數據創建對應的UI,可以通過調用AdapterView的setAdapter方法使得AdapterView將Adapter作為數據源。

常見的AdapterView的子類有ListView、GridView、Spinner和ExpandableListView等。

本文就以ListView為例講解各種常見的Adapter的使用。

以下是Adapter相關類的關系圖:
這裡寫圖片描述


Adapter接口

Adapter源碼鏈接如下:
https://github.com/android/platform_frameworks_base/blob/master/core/java/android/widget/Adapter.java

Adapter接口定義了如下方法:

public abstract void registerDataSetObserver (DataSetObserver observer)
Adapter表示一個數據源,這個數據源是有可能發生變化的,比如增加了數據、刪除了數據、修改了數據,當數據發生變化的時候,它要通知相應的AdapterView做出相應的改變。為了實現這個功能,Adapter使用了觀察者模式,Adapter本身相當於被觀察的對象,AdapterView相當於觀察者,通過調用registerDataSetObserver方法,給Adapter注冊觀察者。

public abstract void unregisterDataSetObserver (DataSetObserver observer)
通過調用unregisterDataSetObserver方法,反注冊觀察者。

public abstract int getCount ()
返回Adapter中數據的數量。

public abstract Object getItem (int position)
Adapter中的數據類似於數組,裡面每一項就是對應一條數據,每條數據都有一個索引位置,即position,根據position可以獲取Adapter中對應的數據項。

public abstract long getItemId (int position)
獲取指定position數據項的id,通常情況下會將position作為id。在Adapter中,相對來說,position使用比id使用頻率更高。

public abstract boolean hasStableIds ()
hasStableIds表示當數據源發生了變化的時候,原有數據項的id會不會發生變化,如果返回true表示Id不變,返回false表示可能會變化。Android所提供的Adapter的子類(包括直接子類和間接子類)的hasStableIds方法都返回false。

public abstract View getView (int position, View convertView, ViewGroup parent)
getView是Adapter中一個很重要的方法,該方法會根據數據項的索引為AdapterView創建對應的UI項。


ListAdapter接口

ListAdapter接口繼承自Adapter接口,ListAdapter源碼鏈接如下:
https://github.com/android/platform_frameworks_base/blob/master/core/java/android/widget/ListAdapter.java

ListAdapter可以作為AbsListView的數據源,AbsListView的子類有ListView、GridView和ExpandableListView。

ListAdapter相比Adapter新增了areAllItemsEnabled和isEnabled兩個方法。


SpinnerAdapter接口

SpinnerAdapter接口繼承自Adapter接口,SpinnerAdapter源碼鏈接如下:
https://github.com/android/platform_frameworks_base/blob/master/core/java/android/widget/SpinnerAdapter.java

SpinnerAdapter可以作為AbsSpinner的數據源,AbsSpinner的子類有Gallery, Spinner和AppCompatSpinner。

相比Adapter,SpinnerAdapter中新增了getDropDownView方法,該方法與Adapter接口中定義的getView方法類似,該方法主要是供AbsSpinner調用,用於生成Spinner下拉彈出區域的UI。在SpinnerAdapter的子類BaseAdapter中,getDropDownView方法默認直接調用了getView方法。

ArrayAdapter和SimpleAdapter都重寫了getDropDownView方法,這兩個類中的getDropDownView方法與其getView的方法都調用了createViewFromResource方法,所以這兩個類中方法getView與方法getDropDownView代碼基本一致。

CursorAdapter也重寫了getView與getDropDownView方法,雖然這兩個方法沒有使用公共代碼,但是這兩個方法代碼邏輯一致。
綜上,我們可知當我們在覆寫getDropDownView方法時,應該盡量使其與getView的代碼邏輯一致。


BaseAdapter抽象類

BaseAdapter是抽象類,其實現了ListAdapter接口和SpinnerAdapter接口,其源碼鏈接如下:
https://github.com/android/platform_frameworks_base/blob/master/core/java/android/widget/BaseAdapter.java

BaseAdapter主要實現了以下功能:
- BaseAdapter實現了觀察者模式,Adapter接口定義了方法registerDataSetObserver和unregisterDataSetObserver,BaseAdapter中維護了一個DataSetObservable類型的變量mDataSetObservable,並實現了方法registerDataSetObserver和unregisterDataSetObserver。

BaseAdapter重寫了getDropDownView方法,其調用了getView方法,如下所示:

public View getDropDownView(int position, View convertView, ViewGroup parent) {
    return getView(position, convertView, parent);
}

覆寫其他一些方法,設置了默認值,比如覆寫hasStableIds方法,使其默認返回false


ArrayAdapter類

類ArrayAdapter繼承並實現了BaseAdapter抽象類,其源碼鏈接如下:
https://github.com/android/platform_frameworks_base/blob/master/core/java/android/widget/ArrayAdapter.java

ArrayAdapter是最簡單的Adapter,AdapterView會將ArrayAdapter中的數據項調用toString()方法,作為文本顯示出來。

ArrayAdapter的使用代碼如下所示:

package com.ispring.adapter;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Toast;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;


public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ListView listView = (ListView)findViewById(R.id.listView);
        String[] values = {"iPhone","小米","三星","華為","中興","聯想","黑莓","魅族"};
        //List list = Arrays.asList(values);
        //Arrays.asList(values)返回的是一個只讀的List,不能進行add和remove
        //new ArrayList<>(Arrays.asList(values))則是一個可寫的List,可以進行add和remove
        List list = new ArrayList<>(Arrays.asList(values));
        final ArrayAdapter adapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1, list);
        listView.setAdapter(adapter);
        //單擊item之後,刪除對應的item
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView parent, View view, int position, long id) {
                String item = adapter.getItem(position);
                adapter.remove(item);
                Toast.makeText(MainActivity.this, item, Toast.LENGTH_SHORT).show();
            }
        });
    }
}

界面如下所示:
這裡寫圖片描述

我們綁定了ListView的OnItemClickListener事件,當單擊其中一項的時候就會通過ArrayAdapter的remove()方法刪除對應項。

ArrayAdapter有以下幾個構造函數:<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwcmUgY2xhc3M9"brush:java;"> public ArrayAdapter(Context context, @LayoutRes int resource) { this(context, resource, 0, new ArrayList()); } public ArrayAdapter(Context context, @LayoutRes int resource, @IdRes int textViewResourceId) { this(context, resource, textViewResourceId, new ArrayList()); } public ArrayAdapter(Context context, @LayoutRes int resource, @NonNull T[] objects) { this(context, resource, 0, Arrays.asList(objects)); } public ArrayAdapter(Context context, @LayoutRes int resource, @IdRes int textViewResourceId, @NonNull T[] objects) { this(context, resource, textViewResourceId, Arrays.asList(objects)); } public ArrayAdapter(Context context, @LayoutRes int resource, @NonNull List objects) { this(context, resource, 0, objects); } public ArrayAdapter(Context context, @LayoutRes int resource, @IdRes int textViewResourceId, @NonNull List objects) { mContext = context; mInflater = LayoutInflater.from(context); mResource = mDropDownResource = resource; mObjects = objects; mFieldId = textViewResourceId; }

上面幾個構造函數其實最後都調用了最後構造函數,有幾點需要說明一下:

上面的構造函數中涉及到了resource和textViewResourceId這兩個參數,resource肯定是數據項對應的UI的layout文件,那textViewResourceId是什麼呢?我們之前提到,ArrayAdapter是用來讓AdapterView顯示文本項的,所以其數據項需要顯示在一個TextView上。如果resource本身就是以作為根結點,那麼textViewResourceId設置為0即可。如果resource所對應的layout不是以作為根結點,那麼該layout中也必須要有一個節點,textViewResourceId即時該的id。字段mResource存儲了resource的值,字段mFieldId存儲了textViewResourceId。

ArrayAdapter的構造函數既可以接收List作為數據源,又可以接收一個數組作為數據源,如果傳入的是一個數組,那麼在構造函數中也會通過Arrays.asList()將數組轉換成list,最終用mObjects存儲該list。

ArrayAdapter重寫了getCount、getItem、getItemId、getView與getDropDownView,其中getView與getDropDownView這兩個方法都調用了createViewFromResource方法,其源碼如下所示:

private View createViewFromResource(LayoutInflater inflater, int position, View convertView,
            ViewGroup parent, int resource) {
        View view;
        TextView text;

        if (convertView == null) {
            view = inflater.inflate(resource, parent, false);
        } else {
            view = convertView;
        }

        try {
            if (mFieldId == 0) {
                //  If no custom field is assigned, assume the whole resource is a TextView
                text = (TextView) view;
            } else {
                //  Otherwise, find the TextView field within the layout
                text = (TextView) view.findViewById(mFieldId);
            }
        } catch (ClassCastException e) {
            Log.e("ArrayAdapter", "You must supply a resource ID for a TextView");
            throw new IllegalStateException(
                    "ArrayAdapter requires the resource ID to be a TextView", e);
        }

        T item = getItem(position);
        if (item instanceof CharSequence) {
            text.setText((CharSequence)item);
        } else {
            text.setText(item.toString());
        }

        return view;
    }

該方法的主要邏輯是查找TextView,並給TextView設置文本值,其邏輯如下所示:

如果在ArrayAdapter的構造函數中沒有設置mFieldId的值或者mFieldId的值為0,那麼就將整個View作為TextView。

如果在ArrayAdapter的構造函數中設置了mFieldId的值,那麼就會調用(TextView)view.findViewById(mFieldId)查找TextView。

ArrayAdapter還增加了add、addAll、insert、remove、clear等方法,當調用了這些方法時,數據會發生變化,ArrayAdapter就會在這些方法裡面調用notifyDataSetChanged方法,比如remove源碼如下所示:

public void remove(T object) {
        synchronized (mLock) {
            if (mOriginalValues != null) {
                mOriginalValues.remove(object);
            } else {
                mObjects.remove(object);
            }
        }
        if (mNotifyOnChange) notifyDataSetChanged();
    }

如果我們在一個for循環中多次調用add方法添加數據,那麼默認會多次觸發notifyDataSetChanged方法的執行,由於每次notifyDataSetChanged方法執行後,AdapterView都會重新渲染UI,所以多次觸發notifyDataSetChanged方法執行會導致效率比較低。最好的辦法是在所有數據變化完成後,我們自己調用notifyDataSetChanged方法。

為此,ArrayAdapter內部提供了一個boolean類型的變量mNotifyOnChange,默認值為true,每次調用add、addAll、insert、remove、clear等方法,都會先判斷mNotifyOnChange的值,只有當mNotifyOnChange為true,才會執行notifyDataSetChanged方法。我們可以通過調用setNotifyOnChange方法將mNotifyOnChange設置為false,然後在for循環中多次調用add方法,這樣不會觸發notifyDataSetChanged方法,在執行完for循環之後,我們自己再調用notifyDataSetChanged方法。

還有一點需要說明的是,如果我們將一個數組作為數據源傳遞給ArrayAdapter,那麼當調用ArrayAdapter的add、addAll、insert、remove、clear等寫操作的方法時就會拋出異常java.lang.UnsupportedOperationException。這是為什麼呢?

我們之前在上面提到,如果在構造函數中傳入數組,會調用Arrays.asList()將數組轉換成List,並賦值給字段mObjects存儲。但是Arrays.asList()返回的List是只讀的,不能夠進行add、remove等寫操作,Arrays.asList()返回的List是其實是一個java.util.AbstractList對象,其add、remove方法的默認實現就是拋出異常。

同理,如果我們傳入了一個只讀的List對象給ArrayAdapter的構造函數,那麼在調用add、addAll、insert、remove、clear等寫操作的方法時也會拋出異常。

在上面的例子中,我們執行了一下方法:

List list = new ArrayList<>(Arrays.asList(values));

我們將Arrays.asList(values)得到的只讀的list作為參數實例化了一個ArrayList,新得到的ArrayList是可寫的,所以我們將它作為參數傳遞給ArrayAdapter之後,可以正常調用其remove()方法。

所以,在使用ArrayAdapter的時候,最好傳給構造函數一個可寫的List,這樣才能正常使用ArrayAdapter的寫操作方法。


SimpleAdapter類

類SimpleAdapter繼承並實現了BaseAdapter抽象類,其源碼鏈接如下:
https://github.com/android/platform_frameworks_base/blob/master/core/java/android/widget/SimpleAdapter.java

SimpleAdaper的作用是方便地將數據與XML文件定義的各種View綁定起來,從而創建復雜的UI。

SimpleAdapter的使用代碼如下所示:

package com.ispring.adapter;

import android.app.Activity;
import android.os.Bundle;
import android.widget.ListView;
import android.widget.SimpleAdapter;

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


public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ListView listView = (ListView)findViewById(R.id.listView);

        final String[] names = {"Windows","Mac OS","Linux","Android","Chrome OS"};

        final String[] descriptions = {
                "Windows是微軟公司的操作系統",
                "Mac OS是蘋果公司的操作系統",
                "Linux是開源免費操作系統",
                "Android是Google公司的智能手機操作系統",
                "Chrome OS是Google公司的Web操作系統"
        };

        final int[] icons = {
                R.drawable.windows,
                R.drawable.mac,
                R.drawable.linux,
                R.drawable.android,
                R.drawable.chrome
        };

        List> list = new ArrayList<>();

        for(int i = 0; i < names.length; i++){
            HashMap map = new HashMap<>();
            map.put("name", names[i]);
            map.put("description", descriptions[i]);
            map.put("icon", icons[i]);
            list.add(map);
        }


        //每個數據項對應一個Map,from表示的是Map中key的數組
        String[] from = {"name", "description", "icon"};

        //數據項Map中的每個key都在layout中有對應的View,
        //to表示數據項對應的View的ID數組
        int[] to = {R.id.name, R.id.description, R.id.icon};

        //R.layout.item表示數據項UI所對應的layout文件
        SimpleAdapter adapter = new SimpleAdapter(this, list, R.layout.item, from, to);

        listView.setAdapter(adapter);
    }
}

其中R.layout.item表示數據項UI所對應的layout文件,如下所示:




    

    
        

        
    

界面如下所示:
這裡寫圖片描述

SimpleAdapter只有一個構造函數,簽名如下所示:

public SimpleAdapter (Context context, List> data, int resource, String[] from, int[] to)

data表示的是List數據源,其中List中的元素都是Map類型,並且Map的key是String類型,Map的value可以是任意類型,我們一般使用HashMap作為List中的數據項。

resource表示數據項UI所對應的layout文件,在本例中即R.layout.item。在本例中,每條數據項都要包含圖片、名稱、描述三條信息,所以我們在item.xml中定義了一個ImageView表示圖片,兩個TextView分別表示名稱和描述,並且都設置了ID值。

每個數據項對應一個Map,from表示的是Map中key的數組。

數據項Map中的每個key都在layout中有對應的View,to表示數據項對應的View的ID數組。

你可能會好奇SimpleAdapter是怎麼把數據和layout自動關聯起來的呢?

SimpleAdapter實現了以下方法:getCount、getItem、getItemId、getView和getDropDownView,其中getView和getDropDownView都調用了createViewFromResource方法,其源碼如下所示:

private View createViewFromResource(LayoutInflater inflater, int position, View convertView,
            ViewGroup parent, int resource) {
        View v;
        if (convertView == null) {
            v = inflater.inflate(resource, parent, false);
        } else {
            v = convertView;
        }

        bindView(position, v);

        return v;
    }

在createViewFromResource中,會調用bindView方法,bindView方法的作用就是將數據項與對應的View綁定起來,從而使得View在界面上展現出數據內容。
bindView的源碼如下所示:

private void bindView(int position, View view) {
        final Map dataSet = mData.get(position);
        if (dataSet == null) {
            return;
        }

        final ViewBinder binder = mViewBinder;
        final String[] from = mFrom;
        final int[] to = mTo;
        final int count = to.length;

        //在for循環中遍歷一條數據項中所有的view
        for (int i = 0; i < count; i++) {           
            final View v = view.findViewById(to[i]);
            if (v != null) {
                final Object data = dataSet.get(from[i]);
                String text = data == null ? "" : data.toString();
                if (text == null) {
                    text = "";
                }

                boolean bound = false;
                if (binder != null) {
                    //首先嘗試用binder對View和data進行綁定
                    bound = binder.setViewValue(v, data, text);
                }

                //如果binder不存在或binder沒有綁定成功,SimpleAdapter會嘗試進行自動綁定
                if (!bound) {
                    if (v instanceof Checkable) {
                        if (data instanceof Boolean) {
                            ((Checkable) v).setChecked((Boolean) data);
                        } else if (v instanceof TextView) {
                            // Note: keep the instanceof TextView check at the bottom of these
                            // ifs since a lot of views are TextViews (e.g. CheckBoxes).
                            setViewText((TextView) v, text);
                        } else {
                            throw new IllegalStateException(v.getClass().getName() +
                                    " should be bound to a Boolean, not a " +
                                    (data == null ? "" : data.getClass()));
                        }
                    } else if (v instanceof TextView) {
                        // Note: keep the instanceof TextView check at the bottom of these
                        // ifs since a lot of views are TextViews (e.g. CheckBoxes).
                        setViewText((TextView) v, text);
                    } else if (v instanceof ImageView) {
                        if (data instanceof Integer) {
                            setViewImage((ImageView) v, (Integer) data);                            
                        } else {
                            setViewImage((ImageView) v, text);
                        }
                    } else {
                        throw new IllegalStateException(v.getClass().getName() + " is not a " +
                                " view that can be bounds by this SimpleAdapter");
                    }
                }
            }
        }
    }

bindView方法會遍歷to數組中View的id,然後通過view.findViewById()方法找到相應的View。那怎麼將數據和View綁定起來呢?

主要分兩步:

首先通過ViewBinder實現開發者自己綁定數據
SimpleAdapter中內部有一個ViewBinder類型的成員變量mViewBinder,通過SipmleAdater的setViewBinder方法可以對其賦值,mViewBinder的默認值是null。

ViewBinder是SimpleAdapter的一個內部接口,其定義了setViewValue方法。我們可以定義一個對象,實現ViewBinder接口的setViewValue方法,然後通過setViewBinder賦值給mViewBinder。

在bindView方法中,會首先判斷mViewBinder存不存在,如果存在就調用mViewBinder的setViewValue方法,該方法會返回一個boolean值,如果返回true表示開發者自己已經成功將數據和View綁定起來了,
bound值為true,後面就不會再執行其他邏輯。

如果開發者沒有自己綁定數據(這是常見的情形),那麼SimpleAdapter會自己嘗試去綁定數據

具體來說,如果mViewBinder不存在或者mViewBinder的setViewValue方法返回false,那麼bound值為false,這時候Android就會按照自己的邏輯盡量去將數據和View進行綁定。


SimpleCursorAdapter類

類SimpleCursorAdapter的繼承路線是:
SimpleCursorAdapter->ResourceCursorAdapter->CursorAdapter->BaseAdapter

CursorAdapter代碼較多,ResourceCursorAdapter代碼較少,SimpleCursorAdapter新增的代碼主要是實現數據與View的綁定,涉及到內部類ViewBinder和bindView方法。

CursorAdapter源碼鏈接如下:
https://github.com/android/platform_frameworks_base/blob/master/core/java/android/widget/CursorAdapter.java

ResourceCursorAdapter源碼鏈接如下:
https://github.com/android/platform_frameworks_base/blob/master/core/java/android/widget/ResourceCursorAdapter.java

SimpleCursorAdapter源碼鏈接如下:
https://github.com/android/platform_frameworks_base/blob/master/core/java/android/widget/SimpleCursorAdapter.java

類SimpleCursorAdapter的主要作用是將Cursor作為數據源創建Adapter。

Android的ApiDemos提供了SimpleCursorAdapter基本使用的示例,鏈接如下所示:
https://github.com/android/platform_development/blob/master/samples/ApiDemos/src/com/example/android/apis/view/List2.java

對應的代碼如下:

package com.example.android.apis.view;

import android.app.ListActivity;
import android.database.Cursor;
import android.provider.ContactsContract.Contacts;
import android.os.Bundle;
import android.widget.ListAdapter;
import android.widget.SimpleCursorAdapter;

/**
 * A list view example where the
 * data comes from a cursor.
 */
public class List2 extends ListActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Get a cursor with all people
        Cursor c = getContentResolver().query(Contacts.CONTENT_URI,
                CONTACT_PROJECTION, null, null, null);
        startManagingCursor(c);

        ListAdapter adapter = new SimpleCursorAdapter(this,
                // Use a template that displays a text view
                android.R.layout.simple_list_item_1,
                // Give the cursor to the list adatper
                c,
                // Map the NAME column in the people database to...
                new String[] {Contacts.DISPLAY_NAME},
                // The "text1" view defined in the XML template
                new int[] {android.R.id.text1});
        setListAdapter(adapter);
    }

    private static final String[] CONTACT_PROJECTION = new String[] {
        Contacts._ID,
        Contacts.DISPLAY_NAME
    };
}

上面的例子演示了使用SimpleCursorAdapter顯示手機通訊錄中的聯系人。通過ContentResolver的query方法得到了Cursor,然後將Cursor作為參數傳遞給了SimpleCursorAdapter,然後將其作為ListView的adapter。

但是,上面的例子有缺陷:

getContentResolver().query()在主線程上執行了ContentResolver的query()方法,但該方法是個耗時方法,很可能導致應用程序無響應,出現ANR現象。

用startManagingCursor()方法管理Cursor的生命周期,使Cursor的生命周期與Activity的生命周期相對應,具體如下:

當Activity處於stopped狀態的時候,會自動調用Cursor的deactive()方法當Activity從stopped狀態變為started狀態的時候,其又會自動調用該Cursor的requery()方法重新查詢數據當Activity銷毀的時候,該Cursor對象也會自動關閉當Activity configuration發生變化的時候(比如手機的橫屏豎屏來回切換等),Activity會重啟,在重啟的時候Cursor也會重新執行requery()方法

通過上面的描述,看起來startManagingCursor()很智能,而且貌似很完美地幫我們處理了Cursor的生命周期,但是我們需要注意的是,當Activity從onStop()轉變到onStart()的時候,其會重新執行Cursor的requery()方法,但是該方法的執行時運行在主線程上的,並且Cursor的requery()方法也是耗時方法,該方法很有可能阻塞UI線程,導致ANR現象,並且在Activity重啟的時候,也會導致Cursor執行requery()方法,進一步增加應用出現無響應的情況,即ANR。

綜上,上面獲取Cursor並管理Cursor生命周期的代碼很可能阻塞UI線程,導致ANR。為了解決這個問題,Android推薦使用CursorLoader和LoaderManager異步加載Cursor,並自動管理Cursor,具體可參見我的另一篇博文《Android中Loader及LoaderManager的使用(附源碼下載)》。

上面的博文中的示例演示的是如何用CursorLoader和SimpleAdapter顯示並過濾手機中的聯系人,下面就把示例代碼貼一下:

package com.ispring.loaderdemo;

import android.app.Activity;
import android.app.LoaderManager;
import android.content.CursorLoader;
import android.content.Loader;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.SimpleCursorAdapter;
import android.widget.TextView;


public class MainActivity extends Activity implements LoaderManager.LoaderCallbacks, TextWatcher {

    private EditText editText = null;

    private ListView listView = null;

    private SimpleCursorAdapter adapter = null;


    private final int CURSOR_LOADER_ID = 1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //綁定編輯框的文本變化事件
        editText = (EditText)findViewById(R.id.editText);
        editText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
                return false;
            }
        });
        editText.addTextChangedListener(this);

        //獲取ListView
        listView = (ListView)findViewById(R.id.listView);

        //創建Adapter
        adapter = new SimpleCursorAdapter(
                this,
                android.R.layout.simple_list_item_2,
                null,
                new String[]{ContactsContract.Contacts.DISPLAY_NAME, ContactsContract.Contacts.CONTACT_STATUS},
                new int[]{android.R.id.text1, android.R.id.text2},
                0);
        listView.setAdapter(adapter);

        //查詢全部聯系人
        Bundle args = new Bundle();
        args.putString("filter", null);
        LoaderManager lm = getLoaderManager();
        lm.initLoader(CURSOR_LOADER_ID, args, this);
    }

    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
    }

    @Override
    public void afterTextChanged(Editable s) {
        String filter = editText.getText().toString();
        Bundle args = new Bundle();
        args.putString("filter", filter);
        LoaderManager lm = getLoaderManager();
        lm.restartLoader(CURSOR_LOADER_ID, args, this);
    }

    @Override
    public Loader onCreateLoader(int id, Bundle args) {

        Uri uri;

        String filter = args != null ? args.getString("filter") : null;

        if(filter != null){
            //根據用戶指定的filter過濾顯示
            uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_FILTER_URI, Uri.encode(filter));
        }else{
            //顯示全部
            uri = ContactsContract.Contacts.CONTENT_URI;
        }

        String[] projection = new String[]{
                ContactsContract.Contacts._ID,
                ContactsContract.Contacts.DISPLAY_NAME,
                ContactsContract.Contacts.CONTACT_STATUS
        };

        String selection = "((" + ContactsContract.Contacts.DISPLAY_NAME + " NOTNULL) AND "+
                "(" + ContactsContract.Contacts.HAS_PHONE_NUMBER + " =1) AND "+
                "(" + ContactsContract.Contacts.DISPLAY_NAME + " != ''))";

        String sortOrder = ContactsContract.Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC";

        return new CursorLoader(this, uri, projection, selection, null, sortOrder);
    }

    @Override
    public void onLoadFinished(Loader loader, Cursor data) {
        adapter.swapCursor(data);
    }

    @Override
    public void onLoaderReset(Loader loader) {
        adapter.swapCursor(null);
    }
}

界面如下所示:
這裡寫圖片描述

所以,SimpleCursorAdapter和CursorLoader、LoaderManager一起使用是最佳實踐,總的來說分為以下幾步:

創建一個SimpleCursorAdapter的實例,將Cursor作為null值傳入,即初始化的時候Cursor還不存在。並相應設置from數組和to數組,這與SimpleAdapter中from、to類似。

調用LoaderManager的initLoader方法,在LoaderCallbacks的onCreateLoader方法中new一個CursorLoader,去異步加載Cursor。

在LoaderCallbacks的onLoadFinished回調方法中獲得Cursor對象,然後調用SimpleCursorAdapter的swapCursor方法將cursor賦值給Adapter。

在LoaderCallbacks的onLoaderReset回調方法中,將null作為參數傳遞給SimpleCursorAdapter的swapCursor方法。

需要說明的是,LoaderManager會管理Cursor的生命周期,我們無需也不能調用Cursor對象的close方法,LoaderManager會自己調用。

SimpleCursorAdapter有兩個構造函數,第一個構造函數簽名如下所示:

SimpleCursorAdapter(Context context, int layout, Cursor c, String[] from, int[] to)

該構造函數從API Level 1開始就有了,該構造函數會導致在某些情況下,多次在UI線程上查詢Cursor,這可能會導致程序無響應,從而出現ANR現象,所以該構造函數從API Level 11開始就被廢棄了,因為從API Level 11開始,SimpleCursorAdapter新增了一個構造函數,簽名如下所示:

public SimpleCursorAdapter (Context context, int layout, Cursor c, String[] from, int[] to, int flags)

相比於第一個構造函數,新增的構造函數多了一個int類型的flags參數。由於SimpleCursorAdapter是繼承自CursorAdapter的,所以該flags參數最終傳遞給了CursorAdapter的構造函數。

flags是個標記位,可以取值0或FLAG_AUTO_REQUERY 或FLAG_REGISTER_CONTENT_OBSERVER或二者的邏輯或值,Android官方推薦的值是0,FLAG_AUTO_REQUERY 和FLAG_REGISTER_CONTENT_OBSERVER的作用在下面會說明。

CursorAdapter的構造函數最終會調用內部的init方法,init方法源碼如下所示:

void init(Context context, Cursor c, int flags) {
        if ((flags & FLAG_AUTO_REQUERY) == FLAG_AUTO_REQUERY) {
            //如果flag含有FLAG_AUTO_REQUERY標記位,那麼強制給其也設置FLAG_REGISTER_CONTENT_OBSERVER標記位
            flags |= FLAG_REGISTER_CONTENT_OBSERVER;
            mAutoRequery = true;
        } else {
            mAutoRequery = false;
        }
        boolean cursorPresent = c != null;
        mCursor = c;
        mDataValid = cursorPresent;
        mContext = context;
        mRowIDColumn = cursorPresent ? c.getColumnIndexOrThrow("_id") : -1;
        if ((flags & FLAG_REGISTER_CONTENT_OBSERVER) == FLAG_REGISTER_CONTENT_OBSERVER) {
            //如果flags具有FLAG_REGISTER_CONTENT_OBSERVER標記位,那麼就注冊ChangeObserver和DataSetObserver
            mChangeObserver = new ChangeObserver();
            mDataSetObserver = new MyDataSetObserver();
        } else {
            mChangeObserver = null;
            mDataSetObserver = null;
        }

        if (cursorPresent) {
            if (mChangeObserver != null) c.registerContentObserver(mChangeObserver);
            if (mDataSetObserver != null) c.registerDataSetObserver(mDataSetObserver);
        }
    }

如果設置了FLAG_AUTO_REQUERY標記位,那麼當數據發出內容變化的通知的時候,就會執行cursor的requery()方法,並且在UI線程上執行,這會導致程序響應體驗變差,甚至ANR,所以Android不建議使用該標記位。如果flag含有FLAG_AUTO_REQUERY標記位,mAutoRequery的值就為true,並且此時CursorAdapter強制給其也設置FLAG_REGISTER_CONTENT_OBSERVER標記位。

如果設置了FLAG_REGISTER_CONTENT_OBSERVER標記位,那麼會注冊監聽器監聽數據內容的變化。具體來說,會實例化ChangeObserver和MyDataSetObserver ,並分別調用mCursor的registerContentObserver和registerDataSetObserver注冊監聽器。

ChangeObserver和MyDataSetObserver都是CursorAdapter的內部類,下面講一下二者的作用與區別。

ChangeObserver
ChangeObserver繼承自ContentObserver,其源碼如下所示:

private class ChangeObserver extends ContentObserver {
    public ChangeObserver() {
        super(new Handler());
    }

    @Override
    public boolean deliverSelfNotifications() {
        return true;
    }

    @Override
    public void onChange(boolean selfChange) {
        onContentChanged();
    }
}

當數據庫中的內容發生變化時,會觸發ContentObserver的onChange方法的執行,這是一個提前通知,相當於數據庫告訴Cursor說,我的內容變化了。ContentObserver的onChange方法中又執行了CursorAdapter的onContentChanged方法,源碼如下所示:

protected void onContentChanged() {
    if (mAutoRequery && mCursor != null && !mCursor.isClosed()) {
        if (false) Log.v("Cursor", "Auto requerying " + mCursor + " due to update");
        mDataValid = mCursor.requery();
    }
}

我們發現,在CursorAdapter的onContentChanged方法中,會判斷是否自動requery,如果之前設置過FLAG_AUTO_REQUERY標記位,那麼mAutoRequery 就為true,此時會執行mCursor的requery()方法。

MyDataSetObserver
MyDataSetObserver繼承自DataSetObserver,其源碼如下所示:

private class MyDataSetObserver extends DataSetObserver {
    @Override
    public void onChanged() {
        mDataValid = true;
        notifyDataSetChanged();
    }

    @Override
    public void onInvalidated() {
        mDataValid = false;
        notifyDataSetInvalidated();
    }
}

當mCursor執行了完了requery()方法時,就會觸發DataSetObserver的onChanged方法的執行,此時表示mCursor所代表的數據內容已經發生了變化,此時調用CursorAdapter的notifyDataSetChanged()方法,告知AdapterView數據內容更新了。

當MCursor執行完了deactivate()或close()時,就會觸發DataSetObserver的onInvalidated方法的執行,此時表示mCursor所代表的數據內容已經無效,不可用了。此時會調用CursorAdapter的notifyDataSetInvalidated()方法,告知AdapterView數據無效了。

也就是說,DataSetObserver是一個後置通知,表示mCursor所代表的數據內容已經完成了變化才會觸發DataSetObserver 響應方法的執行。

更多關於ContentObserver 和DataSetObserver 的區別,大家可以參考博客園上的一篇文章《Android CursorAdapter的監聽事件》。

CursorAdapter重寫了getCount、getItem、getItemId、hasStableIds、getView和getDropDownView方法,我們依次看一下這幾個方法。

getCount
源碼如下所示:

public int getCount() {
    if (mDataValid && mCursor != null) {
        return mCursor.getCount();
    } else {
        return 0;
    }
}

其將mCursor的getCount作為返回值。

getItem

源碼如下所示:

public Object getItem(int position) {
    if (mDataValid && mCursor != null) {
        mCursor.moveToPosition(position);
        return mCursor;
    } else {
        return null;
    }
}

其調用了mCursor的moveToPosition方法,將mCursor移動到指定的位置,然後將mCursor返回。

getItemId
其源碼如下所示:

public long getItemId(int position) {
    if (mDataValid && mCursor != null) {
        if (mCursor.moveToPosition(position)) {
            return mCursor.getLong(mRowIDColumn);
        } else {
            return 0;
        }
    } else {
        return 0;
    }
}

首先也是調用了mCursor的moveToPosition方法,將mCursor移動到指定的位置,然後通過調用mCursor.getLong(mRowIDColumn),得到該條記錄的行號,將其作為itemId返回。

hasStableIds
其源碼如下所示:

public boolean hasStableIds() {
    return true;
}

由於mCursor中每條記錄的rowId都是固定的,所以CursorAdapter是具有穩定ID的,所以hasStableIds方法返回true。

getView
getDropDownView的代碼邏輯與getView差不多,我們只看getView即可。getView源碼如下所示:

public View getView(int position, View convertView, ViewGroup parent) {
    if (!mDataValid) {
        throw new IllegalStateException("this should only be called when the cursor is valid");
    }
    if (!mCursor.moveToPosition(position)) {
        throw new IllegalStateException("couldn't move cursor to position " + position);
    }
    View v;
    if (convertView == null) {
        v = newView(mContext, mCursor, parent);
    } else {
        v = convertView;
    }
    bindView(v, mContext, mCursor);
    return v;
}

首先mCursor會通過moveToPosition方法將游標移動到指定的position。然後,如果convertView為null就調用newView方法創建View,不過CursorAdapter的newView是個抽象方法,其子類ResourceCursorAdapter實現了newView方法,其實就是簡單調用mInflater.inflate(mLayout, parent, false)。
最後會調用bindView方法,不過在CursorView中,bindView是個抽象方法,SimpleCursorAdapter實現了bindView方法,其代碼邏輯與SimpleAdapter的代碼邏輯基本一致,在此不再贅述。

CursorAdapter中有一個比較重要的方法swapCursor,通過該方法可以切換Cursor,其源碼如下所示:

public Cursor swapCursor(Cursor newCursor) {
        if (newCursor == mCursor) {
            return null;
        }
        Cursor oldCursor = mCursor;
        if (oldCursor != null) {
            //刪除oldCursor的監聽器
            if (mChangeObserver != null) oldCursor.unregisterContentObserver(mChangeObserver);
            if (mDataSetObserver != null) oldCursor.unregisterDataSetObserver(mDataSetObserver);
        }
        mCursor = newCursor;
        if (newCursor != null) {
            //為newCursor注冊監聽器
            if (mChangeObserver != null) newCursor.registerContentObserver(mChangeObserver);
            if (mDataSetObserver != null) newCursor.registerDataSetObserver(mDataSetObserver);
            mRowIDColumn = newCursor.getColumnIndexOrThrow("_id");
            mDataValid = true;
            // notify the observers about the new cursor
            notifyDataSetChanged();
        } else {
            mRowIDColumn = -1;
            mDataValid = false;
            // notify the observers about the lack of a data set
            notifyDataSetInvalidated();
        }
        return oldCursor;
    }

該方法主要具有以下邏輯:

如果oldCursor不為null,那麼嘗試刪除oldCursor 的數據監聽器。

如果newCursor不為null,那麼嘗試注冊newCursor 的數據監聽器,然後執行notifyDataSetChanged()方法,表示數據更新了。

如果newCursor 為null,那麼執行notifyDataSetInvalidated()方法,表示數據無效了。

swapCursor方法會將oldCursor作為返回值返回。

大家可以看到,swapCursor方法並沒有執行oldCursor的close方法。其實CursorAdapter還提供了一個changeCursor方法,其源碼如下所示:

public void changeCursor(Cursor cursor) {
        Cursor old = swapCursor(cursor);
        if (old != null) {
            old.close();
        }
    }

changeCursor其實內部也還是調用了swapCursor方法,只不過會對返回的oldCursor執行close()方法。當我們在使用CursorLoader、LoaderManager的時候,不要調用CursorAdapter的changeCursor方法,因為CursorLoader和LoaderManager會自動管理Cursor的生命周期,會在合適的實際調用oldCursor的close方法,如果在使用CursorLoader時自己調用了changeCursor方法,會導致程序崩潰。

本文比較長,感謝大家耐心讀完,希望對大家有所幫助!

 
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved