注:本文翻译自Google官方的Android Developers Training文档,译者技术一般,由于喜爱安卓而产生了翻译的念头,纯属个人兴趣爱好。
原文链接:http://developer.android.com/training/contacts-provider/retrieve-names.html
这节课将讲解如何获取那些和搜索字符串匹配的联系人列表,我们将会使用下面的技术:
匹配联系人姓名
通过将搜索字符串和联系人姓名的全部或部分进行匹配,来获取联系人列表。因为Contact Provider能够允许同名的多个实例,所以该方法将会返回一个匹配的列表。
匹配指定类型的数据,比如电话号码
通过将搜索字符串和某一个特定的数据进行匹配(如电子邮件地址)获取一个联系人的列表。例如,这一方法将会允许你列出所有电子邮件地址和搜索字符串一致的联系人列表。
匹配任何类型的数据
不管是什么类型的数据(包括名字,电话号码,地址,电子邮件地址,等等。),只要和搜索字符串相匹配,就显示在联系人列表中。例如,这一方法允许你接收任意类型的数据作为搜索字符串,并将所有匹配的数据列举出来。
Note:
这节课中的所有例子,都使用一个CursorLoader从Contact Provider中获取数据。一个CursorLoader在一个UI线程之外的线程上运行。这可以保证该查询不会减慢UI的反应时间,从而导致一个糟糕的用户体验。更多信息,可以阅读:Loading Data in the Background。
要用Contact Provider做任何类型的搜索,你的应用必须具有READ_CONTACTS权限。要请求这一权限,在你的清单文件中添加<uses-permission>标签作为<manifest>的子标签:
<uses-permission android:name="android.permission.READ_CONTACTS" />
要将结果显示在ListView,你需要一个主布局文件来定义整个UI(包括ListView),之后用一个项目布局文件来定义ListView中每一项的布局。例如,你可以在住布局文件“res/layout/contacts_list_view.xml”中进行定义,包含下列XML:
<?xml version="1.0" encoding="utf-8"?> <ListView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/list" android:layout_width="match_parent" android:layout_height="match_parent"/>
在这个XML文件中,使用了内置的Android ListView控件android:id/list。
使用下面的XML代码来定义项目布局:
<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/text1" android:layout_width="match_parent" android:layout_height="wrap_content" android:clickable="true"/>
这里的XML使用了内置的Android TextView控件android:text1。
Note:
这节课中不讲解获取用户搜索字符串的UI,因为你可能希望直接获取字符串。例如,你可以给用户一个选项,允许系统搜索时,去匹配短消息中是否有出现这个字符串。
你所写的两个布局文件定义了一个展示一个ListView的用户接口。下一步是编写代码使用这一UI来显示通讯录列表。
要显示通讯录列表,我们首先定义一个Fragment,它被一个Activity加载。使用一个Fragment是一个更灵活的方法,因为你可以使用一个Fragment来显示列表,然后用第二个Fragment来显示用户选择的联系人的具体信息。使用这一方法,你可以将这节课中所讲的一个技术和课程Retrieving Details for a Contact中所讲的技术相结合起来。
要学习如何在一个Activity中使用一个或多个Fragment对象,可以阅读:Building a Dynamic UI with Fragments。
要编写针对于Contacts Provider的查询,Android框架提供了一个联系人类,叫做ContactsContract,它定义了很有用的常量及方法来访问提供器。当你使用该类时,你不需要定义你自己的通讯录URI常量,表名,或者列。要使用这个类,包含下列的声明:
import android.provider.ContactsContract;
因为该代码使用了CursorLoader来从提供器获取数据,你必须指定它实现加载器接口LoaderManager.LoaderCallbacks。同时,为了检测用户从搜索结果列表中选择了哪一个联系人,需实现适配器接口AdapterView.OnItemClickListener。例如:
... import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.widget.AdapterView; ... public class ContactsFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor>, AdapterView.OnItemClickListener {
定义代码其它部分会用到的全局变量:
... /* * Defines an array that contains column names to move from * the Cursor to the ListView. */ @SuppressLint("InlinedApi") private final static String[] FROM_COLUMNS = { Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB ? Contacts.DISPLAY_NAME_PRIMARY : Contacts.DISPLAY_NAME }; /* * Defines an array that contains resource ids for the layout views * that get the Cursor column contents. The id is pre-defined in * the Android framework, so it is prefaced with "android.R.id" */ private final static int[] TO_IDS = { android.R.id.text1 }; // Define global mutable variables // Define a ListView object ListView mContactsList; // Define variables for the contact the user selects // The contact's _ID value long mContactId; // The contact's LOOKUP_KEY String mContactKey; // A content URI for the selected contact Uri mContactUri; // An adapter that binds the result Cursor to the ListView private SimpleCursorAdapter mCursorAdapter; ...
Note:
因为Contacts.DISPLAY_NAME_PRIMARY需要Android 3.0(API版本11)或更高,如果将你的应用的minSdkVersion设置为10或更低会在具有ADK的Eclipse中生成一个Android Lint警告。为了关闭这一警告,可以在FROM_COLUMNS的定义之前添加注解:@SuppressLint("InlinedApi")。
实例化Fragment。添加一个Android系统所需要的空共有构造函数,然后在Fragment对象的回调函数onCreateView()中填充其UI,例如:
// Empty public constructor, required by the system public ContactsFragment() {} // A UI Fragment must inflate its View @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the fragment layout return inflater.inflate(R.layout.contacts_list_layout, container, false); }
配置SimpleCursorAdapter,它和显示搜索结果的ListView绑定起来。要获取显示通讯录的ListView对象,你需要调用Fragment的父activity的Activity.findViewById()方法。当你调用setAdapter()时,使用父activity的Context,例如:
public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); ... // Gets the ListView from the View list of the parent activity mContactsList = (ListView) getActivity().findViewById(R.layout.contact_list_view); // Gets a CursorAdapter mCursorAdapter = new SimpleCursorAdapter( getActivity(), R.layout.contact_list_item, null, FROM_COLUMNS, TO_IDS, 0); // Sets the adapter for the ListView mContactsList.setAdapter(mCursorAdapter); }
当你显示搜索结果时,你通常会希望用户选择某一个联系人以备后续的处理。例如,当用户点击一个联系人,你可以在地图上显示他的住址。要提供这一功能,首先你需要定义当前的Fragment作为点击监听器,方法是通过指定该类实现AdapterView.OnItemClickListener,可以参考Define a Fragment that displays the list of contacts。
要继续配置该监听器,通过在onActivityCreated()调用方法setOnItemClickListener(),将它和ListView绑定起来。例如:
public void onActivityCreated(Bundle savedInstanceState) { ... // Set the item click listener to be the current fragment. mContactsList.setOnItemClickListener(this); ... }
由于你指定当前的Fragment作为ListView的OnItemClickListener,你现在需要实现它所需要的onItemClick()方法,该方法用来处理点击事件。这一部分知识在下一节展开。
定义一个常量,它包含有你希望从查询中返回的列。在ListView中显示每一项的联系人展示姓名,展示姓名包含了联系人名字的主要部分。在Android 3.0(API版本11)及更高的系统中,该名字的列是Contacts.DISPLAY_NAME_PRIMARY;而在之前版本的系统中,列名是Contacts.DISPLAY_NAME。
Contacts._ID这一列被绑定进行SimpleCursorAdapter所使用。Contacts._ID以及LOOKUP_KEY被一起用来构造一个用户所选择的联系人的内容URI。
... @SuppressLint("InlinedApi") private static final String[] PROJECTION = { Contacts._ID, Contacts.LOOKUP_KEY, Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB ? Contacts.DISPLAY_NAME_PRIMARY : Contacts.DISPLAY_NAME };
为了在一个Cursor中,从个人列中获取数据,你需要一个具有列索引的Cursor。你可以为Cursor列定义索引,因为索引的顺序是和你的投影里列名顺序一致的,例如:
// The column index for the _ID column private static final int CONTACT_ID_INDEX = 0; // The column index for the LOOKUP_KEY column private static final int LOOKUP_KEY_INDEX = 1;
为了指定你想要的数据,创建一个文本表达和变量的结合,告知提供器哪些数据列是要搜索的,哪些值是要找的。
对于文本表达,定义一个常量列出所有搜索列。虽然这个表达可以包含值,但还是推荐用占位符“?”来代表变量。在取回过程中,占位符会被一个装有变量的数组所替代。使用“?”作为占位符,可以保证搜索语句不是由SQL补全所生成的。这样可以有效避免恶意的SQL注入攻击。例如:
// Defines the text expression @SuppressLint("InlinedApi") private static final String SELECTION = Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB ? Contacts.DISPLAY_NAME_PRIMARY + " LIKE ?" : Contacts.DISPLAY_NAME + " LIKE ?"; // Defines a variable for the search string private String mSearchString; // Defines the array to hold values that replace the ? private String[] mSelectionArgs = { mSearchString };
在之前的章节中,你可以为ListView设置一个项目点击监听器。现在通过定义方法AdapterView.OnItemClickListener.onItemClick(),来实现这一事件的响应:
@Override public void onItemClick( AdapterView<?> parent, View item, int position, long rowID) { // Get the Cursor Cursor cursor = parent.getAdapter().getCursor(); // Move to the selected contact cursor.moveToPosition(position); // Get the _ID value mContactId = getLong(CONTACT_ID_INDEX); // Get the selected LOOKUP KEY mContactKey = getString(CONTACT_KEY_INDEX); // Create the contact's content Uri mContactUri = Contacts.getLookupUri(mContactId, mContactKey); /* * You can use mContactUri as the content URI for retrieving * the details for a contact. */ }
由于你使用一个CursorLoader来获取数据,你必须初始化那些控制异步查询的后台线程及相关变量。在onActivityCreated()中执行初始化,这样会在Fragment的UI显示出来之前立即执行,如下所示:
public class ContactsFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor> { ... // Called just before the Fragment displays its UI @Override public void onActivityCreated(Bundle savedInstanceState) { // Always call the super method first super.onActivityCreated(savedInstanceState); ... // Initializes the loader getLoaderManager().initLoader(0, null, this);
实现onCreateLoader()方法,这一方法会在你调用了initLoader()方法之后立即被加载器框架所调用。
在onCreateLoader()中,配置搜索字符串。为了将一个字符串变为模式,插入百分号(%)来代表0个或多个字符,或者使用“_”来代表一个单一的字符。例如,模式“%Jefferson%”既可以匹配“Thomas Jefferson”也可以匹配“Jefferson Davis”。
在该方法中,返回一个新的CursorLoader。对于内容URI,使用Contacts.CONTENT_URI。这个URI代表了整个表,如下所示:
... @Override public Loader<Cursor> onCreateLoader(int loaderId, Bundle args) { /* * Makes search string into pattern and * stores it in the selection array */ mSelectionArgs[0] = "%" + mSearchString + "%"; // Starts the query return new CursorLoader( getActivity(), Contacts.CONTENT_URI, PROJECTION, SELECTION, mSelectionArgs, null ); }
实现onLoadFinished()方法。加载器框架会在Contacts Provider返回查询结果后,调用onLoadFinished()。在这一方法中,将结果Cursor放置于SimpleCursorAdapter内。这将会自动把搜索结果更新至ListView内:
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { // Put the result Cursor in the adapter for the ListView mCursorAdapter.swapCursor(cursor); }
当加载器框架检测到了结果Cursor包含了陈旧数据时,会调用onLoaderReset()。对已存在的Cursor中删除SimpleCursorAdapter对已存在的Cursor的引用。如果你不删除的话,加载器框架将不会回收Cursor,这回导致内存泄露。例如:
@Override public void onLoaderReset(Loader<Cursor> loader) { // Delete the reference to the existing Cursor mCursorAdapter.swapCursor(null); }
你现在有了应用的核心部分,它将联系人姓名和搜索字符串进行匹配,然后将结果返回到一个ListView中。用户可以点击一个联系人姓名选择它。这会激活一个监听器,在其中你可以进一步处理联系人的数据。例如,你可以获取联系人的详细信息。更多的相关知识,会在下一节课中详细展开。
如果要学习更多关于搜索用户接口的信息,可以阅读API文档:Creating a Search Interface。
下面的章节将会讲解在Contacts Provider中寻找联系人的其它方法。
三). 匹配某一类型的数据
这一方法允许你指定你想要匹配的特定类型的数据。根据名字来获取是这一类查询的特定例子,但是你也可以对任意和联系人相关的数据这样做。例如,你可以获取具有特定邮编的联系人;在这个例子中,搜索字符串需要和邮编这一列中的数据进行匹配。
要实现这一类型的数据获取,首先实现下列代码,这些都在之前的章节列举了:
下面的步骤会向你展示你需要添加的额外代码,它用来将搜索字符串和某一特定类型的数据进行匹配,然后显示结果。
要搜索一个特定类型的详细数据,你需要知道数据类型对应的自定义MIME类型的值。每一个数据类型有一个单一的MIME类型值,它由和对应数据类型的ContactsContract.CommonDataKinds子类中的常量CONTENT_ITEM_TYPE所定义。在这个子类有对应于那些数据类型的名称;例如,对应于email的子类就叫做ContactsContract.CommonDataKinds.Email,Email数据的自定义MIME类型由常量Email.CONTENT_ITEM_TYPE所定义。
使用表ContactsContract.Data来进行你的搜索。所有你需要的投影,选择语句,排序类型的常量都在这个类或其继承类中定义了。
要定义一个投影,选择一个或更多在ContactsContract.Data或者由它继承的类中定义的列。在返回行之前,Contacts Provider会在ContactsContract.Data和其它表之间做一个隐式的join。例如:
@SuppressLint("InlinedApi") private static final String[] PROJECTION = { /* * The detail data row ID. To make a ListView work, * this column is required. */ Data._ID, // The primary display name Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB ? Data.DISPLAY_NAME_PRIMARY : Data.DISPLAY_NAME, // The contact's _ID, to construct a content URI Data.CONTACT_ID // The contact's LOOKUP_KEY, to construct a content URI Data.LOOKUP_KEY (a permanent link to the contact };
要在某一特定类型的数据进行字符串搜索,需要按照下列要求构建一个选择语句:
列名包含你的搜索字符串。这一名字会随着数据类型所改变,所以你需要找到在ContactsContract.CommonDataKinds的子类中,和那个数据类型相对应的类,之后从子类中选择列名。例如,要搜索Email地址,使用列名Email.ADDRESS。
搜索字符串在选择语句中,用“?”来表示。
列名包含了自定义MIME类型,其名字为Data.MIMETYPE。
数据类型对应的自定义MIME类型值。如同之前所描述的,这是在ContactsContract.CommonDataKinds子类中的常量CONTENT_ITEM_TYPE。例如,对email数据的MIME类型时Email.CONTENT_ITEM_TYPE。连接这个值时,使用单引号('),在常量的开始和结束位置添加单引号;不然的话,提供器会将这个值认为是一个变量名而不是一个字符串值。你不需要为这个值使用“?”占位符,因为你使用的是一个常量而不是用户提供的值。
例如:
/* * Constructs search criteria from the search string * and email MIME type */ private static final String SELECTION = /* * Searches for an email address * that matches the search string */ Email.ADDRESS + " LIKE ? " + "AND " + /* * Searches for a MIME type that matches * the value of the constant * Email.CONTENT_ITEM_TYPE. Note the * single quotes surrounding Email.CONTENT_ITEM_TYPE. */ Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'";
之后,定义变量来包含选择语句:
String mSearchString;
String[] mSelectionArgs = { "" };
现在你已经指定了你想要的数据,也知道了要如何寻找它,在onCreateLoader()的实现中定义一个查询。在该方法中会返回一个新的CursorLoader,使用你的投影,选择文本表达和选择数据作为变量。对于内容URI,使用Data.CONTENT_URI。例如:
@Override public Loader<Cursor> onCreateLoader(int loaderId, Bundle args) { // OPTIONAL: Makes search string into pattern mSearchString = "%" + mSearchString + "%"; // Puts the search string into the selection criteria mSelectionArgs[0] = mSearchString; // Starts the query return new CursorLoader( getActivity(), Data.CONTENT_URI, PROJECTION, SELECTION, mSelectionArgs, null ); }
这些代码片段是对一个特殊类型的数据进行反向查找的例子。如果你的应用关注于特定类型的数据(如Email),且你希望允许用户获得和该数据相关联的姓名,这么做是最好的方法。
四). 匹配任何类型的数据
要基于任何类型的数据寻找联系人,包括名字,email,邮编,电话号码等等。这个搜索结果会包含一个很广泛的搜索结果。例如,如果搜索字符串是“Doe”,那么搜素任何数据的话,返回的联系人中会有名字叫“John Doe”的,也会返回住在“Doe Street”的。
要实现这一获取数据的方法,首先要实现下列代码,这些在前面的章节中已经展开过:
下面的步骤会向你展示你需要添加的额外代码,它用来将搜索字符串和某一特定类型的数据进行匹配,然后显示结果。
不要定义SELECTION常量,或者mSelectionArgs变量。在这类型的查询中这些是不需要的。
实现onCreateLoader()方法,返回一个新的CursorLoader。你不需要将搜索字符串转换到一个模式,因为Contacts Provider会自动地执行它。使用Contacts.CONTENT_FILTER_URI作为基本URI,通过调用Uri.withAppendedPath()将搜索字符串附加上去。使用该URI会自动为任何数据类型自动激活搜索,如下所示:
@Override public Loader<Cursor> onCreateLoader(int loaderId, Bundle args) { /* * Appends the search string to the base URI. Always * encode search strings to ensure they're in proper * format. */ Uri contentUri = Uri.withAppendedPath( Contacts.CONTENT_FILTER_URI, Uri.encode(mSearchString)); // Starts the query return new CursorLoader( getActivity(), contentUri, PROJECTION, null, null, null ); }
这个样例代码是应用对Contact Provider执行广泛搜索时的基础代码。对于想要实现类似功能的应用来说,这个方法是很有用的。