19、联系人UI设计:Material Design联系人列表、RecyclerView优化、联系人首字母悬浮索引、搜索栏动画

做联系人系统,UI 是用户第一眼看到的东西。说实话,很多开发者把精力全放在数据库和逻辑上,结果 UI 一打开卡顿、索引点不动、搜索动画生硬——用户直接给差评。

我个人习惯是,先把 UI 骨架搭好,再往里填逻辑。这一章我们就来聊聊,怎么用 Material Design 把联系人列表做得既好看又流畅。

19.1 Material Design 联系人列表

Material Design 不是一套死板的规范,它更像一套设计语言。对于联系人列表,Google 官方其实给了不少参考,比如 Contact 应用里的列表项设计。

一个标准的联系人列表项,通常包含:

  • 头像:圆形,可以是用户照片,也可以是首字母占位图
  • 姓名:主文字,加粗显示
  • 电话号码/公司:副文字,灰色小字
  • 右侧图标:比如拨号、收藏等快捷操作

我在项目中遇到过,有些团队直接把所有字段堆上去,结果列表项高度不一致,看着特别乱。我的建议是:保持列表项高度统一,通常 72dp 或 80dp 比较合适

核心原则:列表项的信息密度要适中。联系人列表不是表格,别把数据库字段全展示出来。

下面是一个典型的联系人列表项布局代码:

<!-- item_contact.xml -->
<com.google.android.material.card.MaterialCardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="80dp"
    android:layout_marginHorizontal="8dp"
    android:layout_marginVertical="2dp"
    app:cardCornerRadius="12dp"
    app:cardElevation="2dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal"
        android:gravity="center_vertical"
        android:padding="12dp">

        <TextView
            android:id="@+id/avatar"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:background="@drawable/circle_avatar_bg"
            android:gravity="center"
            android:textSize="18sp"
            android:textColor="@android:color/white"
            android:text="张" />

        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_marginStart="12dp"
            android:orientation="vertical">

            <TextView
                android:id="@+id/name"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="16sp"
                android:textStyle="bold"
                android:textColor="@color/text_primary" />

            <TextView
                android:id="@+id/phone"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="14sp"
                android:textColor="@color/text_secondary"
                android:layout_marginTop="4dp" />

        </LinearLayout>

        <ImageView
            android:id="@+id/action_call"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:src="@drawable/ic_call"
            android:padding="8dp" />

    </LinearLayout>

</com.google.android.material.card.MaterialCardView>

19.2 RecyclerView 优化:别让列表卡成 PPT

联系人列表动辄几百上千条,如果不做优化,滑动起来就是灾难。我见过一个项目,列表里每个 item 都加载了高清头像,结果一滑就掉帧。

RecyclerView 优化的核心就三点:

  1. ViewHolder 复用——这是 RecyclerView 的根基,别自己 new View
  2. 图片加载异步化——用 Glide 或 Coil,别在主线程 decode
  3. 减少布局层级——能用一个 LinearLayout 就别套两层 RelativeLayout

我的经验:联系人头像建议用 48dp 的圆形裁剪,Glide 加载时指定 .override(96, 96),既清晰又省内存。

还有一个容易被忽略的点:DiffUtil。当联系人数据更新时,如果用 notifyDataSetChanged(),整个列表会闪烁。用 DiffUtil 可以精确计算出哪些 item 变了,只刷新变化的部分。

class ContactDiffCallback(
    private val oldList: List<Contact>,
    private val newList: List<Contact>
) : DiffUtil.Callback() {

    override fun getOldListSize() = oldList.size
    override fun getNewListSize() = newList.size

    override fun areItemsTheSame(oldPos: Int, newPos: Int): Boolean {
        return oldList[oldPos].id == newList[newPos].id
    }

    override fun areContentsTheSame(oldPos: Int, newPos: Int): Boolean {
        return oldList[oldPos] == newList[newPos]
    }
}

嗯,这里要注意:areContentsTheSame 里最好比较关键字段,比如姓名和电话都变了才算内容变化。如果只比较 id,那头像更新了也不会刷新。

19.3 联系人首字母悬浮索引

这个功能在系统通讯录里很常见——右侧有一列字母,点击或滑动时,列表会跳到对应字母的位置。说白了,就是给列表加一个「快速导航」。

实现思路其实不复杂:

  • 把联系人按拼音首字母分组,比如 A、B、C……Z、#
  • 右侧绘制一个字母列表,每个字母对应一个分组
  • 点击字母时,让 RecyclerView 滚动到对应分组的位置

我建议用 RecyclerView.SmoothScroller 来实现滚动,而不是直接 scrollToPosition。为什么呢?因为直接跳转太生硬,用户体验不好。

fun scrollToSection(section: String) {
    val position = contactAdapter.getPositionForSection(section)
    if (position != -1) {
        val smoothScroller = LinearSmoothScroller(context).apply {
            targetPosition = position
        }
        layoutManager?.startSmoothScroll(smoothScroller)
    }
}

右侧的字母索引,我习惯用自定义 View 来绘制。这样灵活性更高,而且可以加一些动画效果,比如当前字母高亮、触摸反馈等。

避坑指南:我曾经在字母索引上踩过一个坑——触摸事件和 RecyclerView 的滑动冲突。解决方案是:字母索引 View 要拦截触摸事件,不要传递给 RecyclerView。

19.4 搜索栏动画:让交互更有温度

搜索栏是联系人应用的「入口」。用户想找一个人,第一反应就是点搜索。如果搜索栏只是简单显示/隐藏,那太无趣了。

Material Design 推荐的做法是:

  • 点击搜索图标时,搜索栏从顶部或左侧展开
  • 输入文字时,列表实时过滤,同时显示「清除」按钮
  • 取消搜索时,搜索栏收起,列表恢复原样

我个人比较喜欢用 TransitionAnimator 来实现搜索栏的展开/收起。比如用 ValueAnimator 改变搜索栏的宽度,配合 Interpolator 让动画更自然。

fun expandSearchBar() {
    val animator = ValueAnimator.ofInt(0, searchBarWidth)
    animator.addUpdateListener { animation ->
        val params = searchBar.layoutParams
        params.width = animation.animatedValue as Int
        searchBar.layoutParams = params
    }
    animator.duration = 300
    animator.interpolator = FastOutSlowInInterpolator()
    animator.start()
}

还有一个细节:搜索时的列表过滤。我建议用 Filterable 接口,或者直接在 Adapter 里维护一个过滤后的列表。不要每次搜索都重新查询数据库,那样太慢了。

性能要点:搜索过滤最好在内存中进行。把联系人数据加载到 List 里,然后用 filtercontains 匹配。如果数据量超过 1 万条,可以考虑用 Trie 树或拼音索引加速。

19.5 知识体系总览

下面这张图,我把本章的核心知识点串起来了。你可以把它当作一个「思维导图」,方便回顾。

联系人UI设计 Material Design列表 • 列表项布局规范 • 头像/姓名/电话结构 • MaterialCardView使用 RecyclerView优化 • ViewHolder复用 • 图片异步加载 • DiffUtil增量更新 首字母悬浮索引 • 拼音首字母分组 • 右侧字母索引View • SmoothScroller滚动 搜索栏动画 • 展开/收起动画 • 实时过滤 • Interpolator曲线 性能与交互 • 内存过滤 vs 数据库查询 • 触摸事件冲突处理 • 列表项高度统一

你看,整个联系人 UI 设计其实就围绕这四大块:列表本身、性能优化、快速索引、搜索交互。每一块都有很多细节,但核心思路是相通的——让用户用起来舒服,让代码跑起来流畅

我记得有一次,一个同事问我:「为什么我的联系人列表滑动起来这么卡?」我一看,他每个 item 里都放了一个圆角头像,而且没有用缓存。嗯,这就是典型的「没做优化」的后果。你想想看,如果列表里有 500 个联系人,每个头像都要重新加载,那 RecyclerView 再厉害也扛不住。

所以,做 UI 的时候,别光想着「好不好看」,还要想想「快不快」。这两者不冲突,但需要你花心思去平衡。

最后一个小技巧:如果你想让联系人列表的搜索栏更有「质感」,可以试试在动画里加一个微小的弹性效果。用 OvershootInterpolator 或者自定义贝塞尔曲线,用户会感觉这个 App 很「跟手」。


公众号:蓝海资料掘金营,微信deep3321