12、联系人排序:按姓名排序(DisplayName)、按最近联系时间排序、自定义排序规则、Locale-aware排序
排序这件事,看起来简单,做起来全是坑。
我记得刚做联系人模块那会儿,产品经理过来说:「帮我把联系人按姓名排个序,很简单的。」我当时心想,不就是个 Collections.sort() 嘛,三分钟搞定。结果呢?搞了整整两天。为什么?因为「按姓名排序」这四个字,在中文语境下,远比你想的复杂。
今天我们就来彻底聊透联系人排序。我会把我在项目中踩过的坑、总结的经验,全都摊开来讲。
12.1 按 DisplayName 排序:最基础也最坑
先看最直观的做法。假设我们从 ContentProvider 拿到了联系人列表,每个联系人有一个 displayName 字段。直接排序的代码长这样:
List<Contact> contacts = getContactsFromProvider();
Collections.sort(contacts, new Comparator<Contact>() {
@Override
public int compare(Contact c1, Contact c2) {
return c1.getDisplayName().compareTo(c2.getDisplayName());
}
});
这段代码在英文环境下跑得挺好。但放到中文环境,问题就来了。
我曾经在一个海外版的项目里用了这个方案,测试同学拿中文联系人一测,直接提了个 P0 级别的 bug。嗯,从那以后我再也不敢直接用 compareTo 排中文了。
12.2 按最近联系时间排序:用数据说话
按时间排序相对简单,但有个细节容易被忽略。
Android 的联系人数据库里,ContactsContract.Data 表中有一个 LAST_TIME_CONTACTED 字段。如果用户曾经联系过某个联系人,这个字段会记录最后一次联系的时间戳(毫秒)。
排序代码:
Collections.sort(contacts, new Comparator<Contact>() {
@Override
public int compare(Contact c1, Contact c2) {
// 注意:时间戳越大表示越近,所以用 c2 - c1 实现降序
return Long.compare(c2.getLastContactedTime(), c1.getLastContactedTime());
}
});
这里有个坑:如果某个联系人从未被联系过,LAST_TIME_CONTACTED 的值是 0。直接排序的话,这些「零值」联系人会全部排在前面(因为 0 比任何正数都小,但我们是降序排列,0 反而成了最大值)。
我个人习惯的做法是:把从未联系过的联系人放到列表末尾,按字母顺序排列。这样用户看到的是「最近联系的人」在最上面,而不是一堆没打过电话的陌生人。
Collections.sort(contacts, new Comparator<Contact>() {
@Override
public int compare(Contact c1, Contact c2) {
long t1 = c1.getLastContactedTime();
long t2 = c2.getLastContactedTime();
// 两个都有联系时间,按时间降序
if (t1 > 0 && t2 > 0) {
return Long.compare(t2, t1);
}
// 只有一个有联系时间,有时间的排前面
if (t1 > 0) return -1;
if (t2 > 0) return 1;
// 两个都没有联系时间,按姓名升序
return c1.getDisplayName().compareTo(c2.getDisplayName());
}
});
12.3 自定义排序规则:灵活才是王道
实际项目中,排序规则往往不是单一的。用户可能希望:
- 置顶联系人永远在最前面
- 有头像的联系人排在无头像的前面
- 公司联系人排在个人联系人的前面
这时候就需要一个「多级排序器」。我一般会这样设计:
public class ContactComparator implements Comparator<Contact> {
private List<Comparator<Contact>> comparators = new ArrayList<>();
public ContactComparator addComparator(Comparator<Contact> c) {
comparators.add(c);
return this; // 支持链式调用
}
@Override
public int compare(Contact c1, Contact c2) {
for (Comparator<Contact> comparator : comparators) {
int result = comparator.compare(c1, c2);
if (result != 0) {
return result;
}
}
return 0;
}
}
// 使用示例
ContactComparator comparator = new ContactComparator()
.addComparator(new StarredComparator()) // 置顶优先
.addComparator(new HasPhotoComparator()) // 有头像优先
.addComparator(new NameComparator(locale)); // 最后按姓名排序
Collections.sort(contacts, comparator);
你想想看,这种设计的好处是什么?每个比较器只负责一件事,职责单一。想调整排序规则的时候,改一个比较器就行,不会影响其他逻辑。
12.4 Locale-aware 排序:中文排序的终极方案
终于到了最核心的部分。Locale-aware 排序,说白了就是「让排序规则跟着语言走」。中文按拼音排,日文按五十音图排,阿拉伯文按字母排——这才是用户期望的。
Android 提供了 java.text.Collator 类来实现这个功能。它是专门为不同语言环境设计的字符串比较器。
public class LocaleAwareNameComparator implements Comparator<Contact> {
private final Collator collator;
public LocaleAwareNameComparator(Locale locale) {
// 创建一个针对特定语言环境的 Collator
this.collator = Collator.getInstance(locale);
// 设置强度:忽略大小写和变音符号
this.collator.setStrength(Collator.SECONDARY);
}
@Override
public int compare(Contact c1, Contact c2) {
String name1 = c1.getDisplayName();
String name2 = c2.getDisplayName();
// 处理空值
if (name1 == null && name2 == null) return 0;
if (name1 == null) return -1;
if (name2 == null) return 1;
return collator.compare(name1, name2);
}
}
// 使用示例
Locale locale = Locale.CHINA; // 或者从系统设置获取
Comparator<Contact> comparator = new LocaleAwareNameComparator(locale);
Collections.sort(contacts, comparator);
💡 关键点:
Collator.getInstance(Locale.CHINA)会按拼音排序中文Collator.getInstance(Locale.JAPAN)会按五十音图排序日文Collator.getInstance(Locale.US)会按英文字母排序
这里有个细节:Collator 的强度设置。我一般用 SECONDARY,因为它会忽略大小写差异,但会区分变音符号(比如 é 和 e)。如果你希望连变音符号也忽略,就用 PRIMARY。
💡 避坑指南:
我曾经遇到过一个性能问题:当联系人数量超过 5000 时,每次排序都卡顿明显。后来发现是 Collator 对象创建得太频繁了。解决方案很简单——把 Collator 实例缓存起来,不要每次排序都 new 一个。
12.5 知识体系总览
下面这张图总结了联系人排序的核心逻辑和决策路径:
12.6 实战建议
最后,给你几个我在项目中沉淀下来的建议:
- 优先使用 Collator:不要自己写拼音转换库,Android 系统自带的 Collator 已经处理好了绝大多数语言的排序规则。自己写的话,多音字、生僻字都是坑。
- 排序放在子线程:联系人列表动辄上千条,如果在主线程排序,用户会明显感觉到卡顿。用
AsyncTask或者Coroutine异步处理。 - 缓存排序结果:如果联系人数据没有变化,不要重复排序。用一个
isDirty标志位来控制是否需要重新排序。 - 考虑性能边界:当联系人超过 10000 时,
Collections.sort()的耗时可能超过 100ms。这时候可以考虑用Arrays.sort()或者并行排序。
📌 一句话总结:
联系人排序的核心不是「怎么排」,而是「用什么规则排」。Locale-aware 排序 + 多级比较器组合,基本能覆盖 99% 的业务场景。
公众号:蓝海资料掘金营,微信 deep3321