使用详解及源码解析Android中的AdapterBaseAdapterArrayAdapterSimpleAdapter和SimpleCursorAdapter

Posted 孙群

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用详解及源码解析Android中的AdapterBaseAdapterArrayAdapterSimpleAdapter和SimpleCursorAdapter相关的知识,希望对你有一定的参考价值。

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<String> list = Arrays.asList(values);
        //Arrays.asList(values)返回的是一个只读的List,不能进行add和remove
        //new ArrayList<>(Arrays.asList(values))则是一个可写的List,可以进行add和remove
        List<String> list = new ArrayList<>(Arrays.asList(values));
        final ArrayAdapter<String> adapter = new ArrayAdapter<String>(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有以下几个构造函数:

public ArrayAdapter(Context context, @LayoutRes int resource) 
        this(context, resource, 0, new ArrayList<T>());
    

public ArrayAdapter(Context context, @LayoutRes int resource, @IdRes int textViewResourceId) 
        this(context, resource, textViewResourceId, new ArrayList<T>());
    

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<T> objects) 
        this(context, resource, 0, objects);
    

public ArrayAdapter(Context context, @LayoutRes int resource, @IdRes int textViewResourceId,
            @NonNull List<T> objects) 
        mContext = context;
        mInflater = LayoutInflater.from(context);
        mResource = mDropDownResource = resource;
        mObjects = objects;
        mFieldId = textViewResourceId;
    

上面几个构造函数其实最后都调用了最后构造函数,有几点需要说明一下:

  • 上面的构造函数中涉及到了resource和textViewResourceId这两个参数,resource肯定是数据项对应的UI的layout文件,那textViewResourceId是什么呢?我们之前提到,ArrayAdapter是用来让AdapterView显示文本项的,所以其数据项需要显示在一个TextView上。如果resource本身就是以<TextView>作为根结点,那么textViewResourceId设置为0即可。如果resource所对应的layout不是以<TextView>作为根结点,那么该layout中也必须要有一个<TextView>节点,textViewResourceId即时该<TextView>的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设置文本值,其逻辑如下所示:

  1. 如果在ArrayAdapter的构造函数中没有设置mFieldId的值或者mFieldId的值为0,那么就将整个View作为TextView。

  2. 如果在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方法的默认实现就是抛出异常,详见博文《为什么Java里的Arrays.asList不能用add和remove方法?》

同理,如果我们传入了一个只读的List对象给ArrayAdapter的构造函数,那么在调用add、addAll、insert、remove、clear等写操作的方法时也会抛出异常。

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

List<String> 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<Map<String, Object>> list = new ArrayList<>();

        for(int i = 0; i < names.length; i++)
            HashMap<String, Object> 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文件,如下所示:

<?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"
    android:paddingTop="5dp"
    android:paddingBottom="5dp">

    <ImageView android:id="@+id/icon"
        android:layout_width="100dp"
        android:layout_height="wrap_content" />

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:paddingLeft="10dp">
        <TextView android:id="@+id/name"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="@dimen/defaultFontSize" />

        <TextView android:id="@+id/description"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="@dimen/defaultFontSize"
            android:layout_marginTop="10dp"/>
    </LinearLayout>

</LinearLayout>

界面如下所示:

SimpleAdapter只有一个构造函数,签名如下所示:

public SimpleAdapter (Context context, List<? extends Map<String, ?>> data, int resource, String[] from, int[] to)
  • data表示的是List数据源,其中List中的元素都是Map类型,并且Map的key是String类型,Map的value可以是任意类型,我们一般使用HashMap<String, Object>作为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 ? "<unknown type>" : 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绑定起来呢?

主要分两步:

  1. 首先通过ViewBinder实现开发者自己绑定数据
    SimpleAdapter中内部有一个ViewBinder类型的成员变量mViewBinder,通过SipmleAdater的setViewBinder方法可以对其赋值,mViewBinder的默认值是null。

    ViewBinder是SimpleAdapter的一个内部接口,其定义了setViewValue方法。我们可以定义一个对象,实现ViewBinder接口的setViewValue方法,然后通过setViewBinder赋值给mViewBinder。

    在bindView方法中,会首先判断mViewBinder存不存在,如果存在就调用mViewBinder的setViewValue方法,该方法会返回一个boolean值,如果返回true表示开发者自己已经成功将数据和View绑定起来了,
    bound值为true,后面就不会再执行其他逻辑。

  2. 如果开发者没有自己绑定数据(这是常见的情形),那么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<Cursor>, 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<Cursor> 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<Cursor> loader, Cursor data) 
        adapter.swapCursor(data);
    

    @Override
    public void onLoaderReset(Loader<Cursor> loader) 
        adapter.swapCursor(null);
    

界面如下所示:

所以,SimpleCursorAdapter和CursorLoader、LoaderManager一起使用是最佳实践,总的来说分为以下几步:

  1. 创建一个SimpleCursorAdapter的实例,将Cursor作为null值传入,即初始化的时候Cursor还不存在。并相应设置from数组和to数组,这与SimpleAdapter中from、to类似。

  2. 调用LoaderManager的initLoader方法,在LoaderCallbacks的onCreateLoader方法中new一个CursorLoader,去异步加载Cursor。

  3. 在LoaderCallbacks的onLoadFinished回调方法中获得Cursor对象,然后调用SimpleCursorAdapter的swapCursor方法将cursor赋值给Adapter。

  4. 在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方法,我们依次看一下这几个方法。