5、联系人详情展示:查询RawContacts表、获取电话号码(Data表)、获取邮箱地址、多数据类型合并展示
好,咱们接着往下走。上一章我们把联系人列表搞定了,现在点进去看详情,对吧?
说实话,联系人详情这块,是Android通讯录开发里最容易踩坑的地方。为什么?因为数据太散了。一个联系人的名字、电话、邮箱、地址、头像……全部分散在不同的表里。你想想看,要是没理清楚,代码写出来就是一坨浆糊。
我个人习惯是,先把数据模型想清楚,再动手写代码。别急着敲键盘,先画图。
5.1 数据到底存在哪?
咱们先回顾一下Contacts Provider的核心表结构。上一章我提过,但这里得再深入一点。
| 表名 | 作用 | 关键字段 |
|---|---|---|
| ContactsContract.Contacts | 聚合后的联系人(一个联系人对应一行) | _ID, DISPLAY_NAME, PHOTO_URI |
| ContactsContract.RawContacts | 原始联系人数据(来自不同账户) | _ID, CONTACT_ID, ACCOUNT_NAME, ACCOUNT_TYPE |
| ContactsContract.Data | 具体数据行(电话、邮箱、地址等) | _ID, RAW_CONTACT_ID, MIMETYPE, DATA1~DATA15 |
嗯,这里要注意:一个联系人可能对应多个RawContacts。比如你手机里同步了Google账号和微信账号,同一个联系人可能有两个RawContacts行。而每个RawContacts下面,又挂着一堆Data行。
所以,查询详情的基本思路是:
- 根据联系人ID,查出所有关联的RawContacts
- 根据RawContacts的ID,查出所有关联的Data行
- 根据Data行的MIMETYPE,区分出电话、邮箱、地址等
核心原则:不要直接查Contacts表拿电话!电话只存在Data表里。我见过有新手直接去Contacts表里翻PHONE_NUMBER字段,结果发现是空的——因为那个字段早就废弃了。
5.2 查询RawContacts表
先写个查询RawContacts的代码。假设我们已经有了联系人ID(就是Contacts表里的_ID)。
// 查询RawContacts
public List<Long> queryRawContactIds(Context context, long contactId) {
List<Long> rawContactIds = new ArrayList<>();
Uri uri = RawContacts.CONTENT_URI;
String selection = RawContacts.CONTACT_ID + " = ?";
String[] selectionArgs = new String[]{String.valueOf(contactId)};
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(
uri,
new String[]{RawContacts._ID},
selection,
selectionArgs,
null
);
if (cursor != null) {
while (cursor.moveToNext()) {
rawContactIds.add(cursor.getLong(0));
}
}
} catch (Exception e) {
// 处理异常
} finally {
if (cursor != null) {
cursor.close();
}
}
return rawContactIds;
}
这段代码很简单,对吧?但我在项目中遇到过一个问题:有些RawContacts是被标记为删除的。如果你不加上 RawContacts.DELETED + " = 0" 这个条件,可能会查到一堆脏数据。
避坑指南:我曾经在一个企业通讯录项目里,因为没过滤DELETED字段,导致详情页显示了一堆空数据。排查了半天才发现是同步过程中产生的废弃RawContacts没清理干净。所以,建议查询时加上 RawContacts.DELETED + " = 0"。
5.3 获取电话号码(Data表)
拿到RawContacts的ID列表后,下一步就是查Data表。电话的MIMETYPE是 Phone.CONTENT_ITEM_TYPE,也就是 "vnd.android.cursor.item/phone_v2"。
// 查询电话号码
public List<String> queryPhoneNumbers(Context context, List<Long> rawContactIds) {
List<String> phoneNumbers = new ArrayList<>();
if (rawContactIds == null || rawContactIds.isEmpty()) {
return phoneNumbers;
}
// 构建IN查询条件
StringBuilder sb = new StringBuilder();
for (int i = 0; i < rawContactIds.size(); i++) {
if (i > 0) sb.append(",");
sb.append("?");
}
String placeholders = sb.toString();
Uri uri = Data.CONTENT_URI;
String selection = Data.RAW_CONTACT_ID + " IN (" + placeholders + ") AND "
+ Data.MIMETYPE + " = ?";
String[] selectionArgs = new String[rawContactIds.size() + 1];
for (int i = 0; i < rawContactIds.size(); i++) {
selectionArgs[i] = String.valueOf(rawContactIds.get(i));
}
selectionArgs[rawContactIds.size()] = Phone.CONTENT_ITEM_TYPE;
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(
uri,
new String[]{Phone.NUMBER, Phone.TYPE, Phone.LABEL},
selection,
selectionArgs,
null
);
if (cursor != null) {
while (cursor.moveToNext()) {
String number = cursor.getString(0);
int type = cursor.getInt(1);
String label = cursor.getString(2);
// 这里可以处理号码类型:HOME、MOBILE、WORK等
phoneNumbers.add(number);
}
}
} catch (Exception e) {
// 处理异常
} finally {
if (cursor != null) {
cursor.close();
}
}
return phoneNumbers;
}
这里有个小技巧:查询Data表时,尽量只查你需要的列。别用 null 代替投影列,那样会拉回所有15个DATA字段,性能差很多。你想想看,Data表可能有几十万行数据,全查出来内存就炸了。
5.4 获取邮箱地址
邮箱的查询逻辑和电话几乎一模一样,只是MIMETYPE不同。邮箱的MIMETYPE是 Email.CONTENT_ITEM_TYPE,也就是 "vnd.android.cursor.item/email_v2"。
// 查询邮箱地址
public List<String> queryEmails(Context context, List<Long> rawContactIds) {
List<String> emails = new ArrayList<>();
if (rawContactIds == null || rawContactIds.isEmpty()) {
return emails;
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < rawContactIds.size(); i++) {
if (i > 0) sb.append(",");
sb.append("?");
}
String placeholders = sb.toString();
Uri uri = Data.CONTENT_URI;
String selection = Data.RAW_CONTACT_ID + " IN (" + placeholders + ") AND "
+ Data.MIMETYPE + " = ?";
String[] selectionArgs = new String[rawContactIds.size() + 1];
for (int i = 0; i < rawContactIds.size(); i++) {
selectionArgs[i] = String.valueOf(rawContactIds.get(i));
}
selectionArgs[rawContactIds.size()] = Email.CONTENT_ITEM_TYPE;
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(
uri,
new String[]{Email.ADDRESS, Email.TYPE, Email.LABEL},
selection,
selectionArgs,
null
);
if (cursor != null) {
while (cursor.moveToNext()) {
String address = cursor.getString(0);
emails.add(address);
}
}
} catch (Exception e) {
// 处理异常
} finally {
if (cursor != null) {
cursor.close();
}
}
return emails;
}
嗯,代码确实很像。但别急着复制粘贴,咱们得想想怎么优化。
5.5 多数据类型合并展示
在实际项目中,你不会只查电话和邮箱。可能还有地址、IM账号、网站、生日……如果每个类型都写一个查询方法,那代码就太冗余了。
我个人习惯是:一次查询,按MIMETYPE分类。这样只需要一次数据库查询,然后自己在内存里做分类。
// 一次查询所有Data,按MIMETYPE分类
public Map<String, List<ContentValues>> queryAllData(Context context, List<Long> rawContactIds) {
Map<String, List<ContentValues>> dataMap = new HashMap<>();
if (rawContactIds == null || rawContactIds.isEmpty()) {
return dataMap;
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < rawContactIds.size(); i++) {
if (i > 0) sb.append(",");
sb.append("?");
}
String placeholders = sb.toString();
Uri uri = Data.CONTENT_URI;
String selection = Data.RAW_CONTACT_ID + " IN (" + placeholders + ")";
String[] selectionArgs = new String[rawContactIds.size()];
for (int i = 0; i < rawContactIds.size(); i++) {
selectionArgs[i] = String.valueOf(rawContactIds.get(i));
}
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(
uri,
null, // 这里可以指定需要的列,但为了演示先查全部
selection,
selectionArgs,
null
);
if (cursor != null) {
while (cursor.moveToNext()) {
ContentValues values = new ContentValues();
// 把需要的字段都取出来
String mimeType = cursor.getString(cursor.getColumnIndex(Data.MIMETYPE));
// 根据MIMETYPE存到不同的列表里
List<ContentValues> list = dataMap.get(mimeType);
if (list == null) {
list = new ArrayList<>();
dataMap.put(mimeType, list);
}
// 这里只存关键数据,实际项目中可以封装成对象
values.put("data1", cursor.getString(cursor.getColumnIndex("data1")));
values.put("data2", cursor.getString(cursor.getColumnIndex("data2")));
values.put("data3", cursor.getString(cursor.getColumnIndex("data3")));
list.add(values);
}
}
} catch (Exception e) {
// 处理异常
} finally {
if (cursor != null) {
cursor.close();
}
}
return dataMap;
}
拿到这个Map之后,展示就简单了。根据MIMETYPE去Map里取对应的列表,然后渲染到UI上。
小提示:Data表的data1~data15字段含义取决于MIMETYPE。比如电话类型下,data1是号码,data2是类型(HOME/MOBILE等),data3是自定义标签。邮箱类型下,data1是地址,data2是类型。所以,一定要先判断MIMETYPE,再解析data字段。
5.6 核心流程梳理
说了这么多,咱们用一张图把整个流程串起来。这样你脑子里就有个清晰的脉络了。
这张图把整个流程分成了5步。说白了,核心就是:先查RawContacts拿到ID列表,再查Data表拿到所有数据,最后按类型分类展示。
5.7 合并展示的UI设计
最后聊聊UI展示。我个人建议用RecyclerView来实现,每个数据类型作为一个Section。比如:
- 电话区域:显示所有电话号码,每个号码旁边带类型标签(家庭、工作、手机等)
- 邮箱区域:显示所有邮箱地址,同样带类型标签
- 地址区域:如果有地址数据,也展示出来
每个Section之间用分割线隔开,或者用不同的背景色区分。这样用户一眼就能看清楚。
经验之谈:我在做国际版通讯录时,发现有些联系人有十几个电话号码。如果全部平铺展示,界面会很长。后来我改成默认只显示前3个,点击"展开"再显示全部。这个交互细节,用户反馈很好。
好了,联系人详情展示的核心逻辑就这些。记住:一次查询Data表,按MIMETYPE分类,比多次查询高效得多。代码写起来也清爽。
公众号:蓝海资料掘金营,微信deep3321