24、联系人标签系统:自定义标签表、给联系人打标签、按标签筛选、标签云展示
标签系统,说白了就是给联系人贴“小纸条”。
我刚开始做联系人模块时,觉得用分组就够了。后来用户反馈说:“我想给同一个人打多个标签,比如‘同事’、‘羽毛球友’、‘项目A’。” 分组显然搞不定。嗯,这时候标签系统就派上用场了。
今天我们就来手撸一套完整的标签系统。包括数据库设计、打标签逻辑、按标签筛选,最后再做一个漂亮的标签云。
24.1 自定义标签表设计
标签和联系人是什么关系?多对多。一个联系人可以有多个标签,一个标签也可以属于多个联系人。所以我们需要三张表:联系人表、标签表、关联表。
先看标签表的结构。我个人习惯把标签表设计得尽量简单,只存核心字段:
CREATE TABLE tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
color INTEGER DEFAULT 0xFF2196F3,
created_at INTEGER DEFAULT (strftime('%s','now'))
);
这里有个小细节:color字段存的是ARGB颜色值。为什么不用字符串?因为整型比较快,而且Android的Color类直接支持整型解析。
再看关联表:
CREATE TABLE contact_tags (
contact_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
added_at INTEGER DEFAULT (strftime('%s','now')),
PRIMARY KEY (contact_id, tag_id),
FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
);
注意这里用了联合主键,避免重复打标签。外键加了ON DELETE CASCADE,删联系人时自动清理关联数据。我曾经踩过这个坑——删了联系人,关联表里一堆脏数据,筛选时莫名其妙多出空标签。
Room.databaseBuilder()里调用.enableForeignKeyConstraints(true)。
24.2 给联系人打标签
打标签的逻辑其实很简单:先查标签是否存在,不存在就创建,然后插入关联记录。
我封装了一个方法,你们可以直接用:
@Transaction
public void addTagToContact(long contactId, String tagName) {
// 1. 查找或创建标签
Tag tag = tagDao.findByName(tagName);
if (tag == null) {
tag = new Tag();
tag.name = tagName;
tag.color = generateRandomColor();
long id = tagDao.insert(tag);
tag.id = id;
}
// 2. 检查是否已经打过这个标签
ContactTag existing = contactTagDao.find(contactId, tag.id);
if (existing != null) {
return; // 已经打过了,跳过
}
// 3. 插入关联
ContactTag ct = new ContactTag();
ct.contactId = contactId;
ct.tagId = tag.id;
contactTagDao.insert(ct);
}
这里用了@Transaction注解,保证两步操作要么都成功,要么都失败。你想想看,如果标签创建成功了,关联插入失败,那这个标签就成了“孤儿标签”,多尴尬。
颜色生成我用了简单的随机算法:
private int generateRandomColor() {
Random rnd = new Random();
int color = Color.argb(255,
rnd.nextInt(200) + 55,
rnd.nextInt(200) + 55,
rnd.nextInt(200) + 55);
return color;
}
为什么要加55?避免颜色太浅看不清。嗯,这个是我调UI时试出来的经验值。
24.3 按标签筛选联系人
筛选的核心SQL其实就是一个带关联查询的IN操作:
SELECT DISTINCT c.* FROM contacts c
INNER JOIN contact_tags ct ON c.id = ct.contact_id
WHERE ct.tag_id IN (:tagIds)
在Room里写成这样:
@Query("""
SELECT DISTINCT c.* FROM contacts c
INNER JOIN contact_tags ct ON c.id = ct.contact_id
WHERE ct.tag_id IN (:tagIds)
""")
LiveData<List<Contact>> getContactsByTags(List<Long> tagIds);
这里有个性能问题要注意。如果标签数量很多,IN语句可能会变慢。我建议限制一次最多选5个标签。为什么是5个?我在项目里测试过,5个以内性能基本没差别,超过10个就开始有明显延迟了。
24.4 标签云展示
标签云说白了就是把所有标签按使用频率排个序,用得多的显示大一点、颜色深一点。
先查每个标签的使用次数:
@Query("""
SELECT t.*, COUNT(ct.contact_id) AS usage_count
FROM tags t
LEFT JOIN contact_tags ct ON t.id = ct.tag_id
GROUP BY t.id
ORDER BY usage_count DESC
""")
LiveData<List<TagWithCount>> getAllTagsWithCount();
然后根据usage_count计算字体大小和透明度。我常用的算法:
private void setupTagCloud(List<TagWithCount> tags, ViewGroup container) {
int maxCount = tags.get(0).usageCount;
int minCount = tags.get(tags.size() - 1).usageCount;
for (TagWithCount tag : tags) {
TextView tv = new TextView(context);
tv.setText(tag.name);
// 根据使用次数计算字体大小:12sp ~ 28sp
float ratio = (maxCount == minCount) ? 0.5f :
(float)(tag.usageCount - minCount) / (maxCount - minCount);
int textSize = (int)(12 + ratio * 16);
tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize);
// 颜色透明度:0.4 ~ 1.0
int alpha = (int)(102 + ratio * 153);
int color = Color.argb(alpha,
Color.red(tag.color),
Color.green(tag.color),
Color.blue(tag.color));
tv.setTextColor(color);
// 点击跳转到筛选结果
tv.setOnClickListener(v -> {
navigateToFilteredList(tag.id);
});
container.addView(tv);
}
}
布局我用的是FlexboxLayout,让标签自动换行排列。Google的flexbox库用起来很顺手,比FlowLayout稳定多了。
下面这张图展示了标签系统的整体数据流:
24.5 避坑指南与最佳实践
做标签系统时,有几个坑我替你们踩过了:
- 标签名大小写问题:用户可能输入“同事”和“同事 ”(带空格),或者“Work”和“work”。我建议统一转小写并trim后再存,展示时保留原始输入。
- 删除标签时的联动:删标签时记得删关联表里的数据。用
ON DELETE CASCADE可以自动处理,但Room里要手动写@ForeignKey的onDelete属性。 - 标签数量限制:一个联系人最多打多少个标签?我建议限制20个。不是技术做不到,而是UI上展示不下。你想想看,一个联系人下面挂50个标签,用户看着也晕。
- 搜索性能:如果标签数据量很大(比如上万),
LIKE '%标签名%'会非常慢。可以考虑用FTS4全文搜索,或者加个INDEX。
核心要点回顾:
- 三表设计:contacts + tags + contact_tags
- 联合主键防重复,外键级联删除
- 打标签用事务保证一致性
- 筛选用INNER JOIN + WHERE IN
- 标签云按使用频率动态计算大小和颜色
标签系统看起来简单,但做好细节并不容易。我记得有一次上线后用户反馈:“我给100个联系人打了标签,结果标签云页面卡死了。” 排查后发现是FlexboxLayout里塞了太多TextView,后来改用RecyclerView配合StaggeredGridLayoutManager才解决。嗯,性能优化永远在路上。
好了,标签系统的核心内容就这些。动手实现时遇到问题,欢迎随时交流。
公众号:蓝海资料掘金营,微信deep3321