Skip to content

PillTab / PillTabsRow / PillTabsColumn — 胶囊导航 ​

一排胶囊形状的单选导航,用来切分类 / 切频道。选中项是实心主色胶囊,横向切换时自动滚到视区正中。

名字里的 "Tab" 是重点:它是导航控件(指示「当前在哪个分类」),不是 M3 那种可多选的 FilterChip。

基本用法 ​

横向(最常用):

kotlin
var selected by remember { mutableIntStateOf(0) }

PillTabsRow(
    labels = listOf("推荐", "科幻", "悬疑", "日常"),
    selectedIndex = selected,
    onSelect = { selected = it },
)

带前置槽位:

kotlin
PillTabsRow(
    labels = listOf("全部", "已下载", "收藏"),
    selectedIndex = selected,
    onSelect = { selected = it },
    leading = { ActionImageButton(Phosphor.Funnel, onAction = { openFilter() }) },
)

纵向(选项多、标题长不适合横滚):

kotlin
PillTabsColumn(
    labels = listOf("最近阅读", "我的收藏", "下载管理", "全部作品"),
    selectedIndex = selected,
    onSelect = { selected = it },
    fillItemWidth = true,
)

单个胶囊 / 单个角标:

kotlin
PillTab(label = "科幻", selected = true, onClick = { })
PillTab(label = "全部", selected = true, onClick = { }, count = 128)
PillTabBadge(count = 12, selected = false)

参数 ​

PillTabsRow / PillTabsColumn ​

参数类型默认值说明
labelsList<String>—选项名。横向时一般第 0 项是「推荐」/「全部」这类固定项
selectedIndexInt—选中下标,-1 表示无选中
onSelect(Int) -> Unit—选中回调,参数是下标
modifierModifierModifier修饰符
leading(@Composable () -> Unit)?null行首 / 列首的自定义插槽,属于这一行、跟着胶囊一起滚
fillItemWidthBooleantrue仅 PillTabsColumn。每个胶囊是否占满整行宽度
itemAlignmentAlignment.HorizontalAlignment.Start仅 PillTabsColumn。fillItemWidth = true 时此项无效

PillTab ​

参数类型默认值说明
labelString—文字,单行、不换行
selectedBoolean—选中态,状态完全外置
onClick() -> Unit—点击回调
modifierModifierModifier修饰符
sizePillTabSizePillTabSize.MEDIUM尺寸档位
enabledBooleantrue禁用时 38% 透明度且不可点
iconImageVector?null左侧图标
countInt?null右侧数字角标
trailingContent@Composable (() -> Unit)?null右侧自定义插槽,优先级高于 count

PillTabBadge ​

参数类型默认值说明
countInt—角标数字
selectedBoolean—跟随所在胶囊的选中态
modifierModifierModifier修饰符
enabledBooleantrue跟随所在胶囊的启用态

PillTabSize ​

档位高度文字图标图标 / 角标间距左右内边距
SMALL28dplabelSmall14dp6dp12dp
MEDIUM34dplabelLarge16dp8dp16dp
LARGE40dptitleSmall18dp10dp20dp

要点 ​

  • selectedIndex = -1 是合法的无选中态,不会崩、也不会误选第 0 项。
  • leading 会跟着胶囊一起滚走。 它属于这一行,不是盖在行上方的常驻控件 —— 想要常驻就放进标题栏。
  • 横向的「选中项自动居中」只在切换时触发,并且会夹取在 0..maxValue 内,所以选第 0 项或最后一项时不会滚出界。
  • 纵向不做自动居中,这是刻意的:竖排视区本来就窄,居中会让整个列表跟着上下跳。换来的是每个胶囊可以撑满整行(fillItemWidth,默认 true)。
  • fillItemWidth = true 时 itemAlignment 无效 —— 胶囊已经占满整行了,没有对齐可言。
  • trailingContent 优先于 count;两个都传时只渲染 trailingContent。
  • enabled = false 时连点击一起禁掉,角标也跟着变灰。
  • 两个方向的胶囊间距都是 8dp,且都自带内边距(横向 start 8 / top 12 / end 16 / bottom 12,纵向 start 16 / top 12 / end 16 / bottom 12)。别在外层重复加。

说明 ​

  • 选中态取 primary / onPrimary(实心主色),这是对 M3 FilterChip 的刻意偏离。 M3 原值是 secondaryContainer / onSecondaryContainer(segmented button 的单选映射也是这一对),但它压在实际页面上实测只有 1.23:1(深色 1.99:1)—— 靠这点底色差根本认不出选中。改成 primary 填充后是 6.47:1(浅)/ 7.76:1(深),一眼可辨。M3 允许这种换法:其它角色也可以用,只要容器与文字达到 3:1 对比度。别按 M3 文档「纠正」回来。
  • 代价要说清楚:选中态从「低彩度提示」变成「实心主色胶囊」,成了这一行里最重的元素。所以同屏不要再放 FilledButton,否则两个主色实心块会互相抢注意力。
  • 角标底色从当前容器的 on 角色派生(选中时 onPrimary@18%),而不是另取一个容器角色。这不是角色错配,是 state layer 式的派生:on 角色是「必然与所在容器形成对比」的那一支,从它派生的底一定跟着容器走,换肤时不可能失配。若改用固定容器角色,一旦胶囊底色变了,角标就会和底撞在一起。
  • 未选中描边用 outline,比 PillLabel 的 outlineVariant 更实 —— 导航项需要看清边界。
  • 三个颜色(底 / 文字 / 描边)都走 animateColorAsState,所以切换分类时是平滑过渡,不是硬跳。
  • 横向自动居中的实现:每个胶囊用 onGloballyPositioned 实测自己在内容坐标系里的横向范围,切换时算出「胶囊中心 − 视区一半」作为目标滚动位置。所以它能正确居中任何宽度的项,不需要按字数估算。
  • 命名变更:PillTabs 现在是 PillTabsRow(横向行),纵向列是 PillTabsColumn(早先叫过 PillTabColumn)。旧名字 PillTabs 和 PillTabColumn 在组件库里都已不存在。