外观
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
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
labels | List<String> | — | 选项名。横向时一般第 0 项是「推荐」/「全部」这类固定项 |
selectedIndex | Int | — | 选中下标,-1 表示无选中 |
onSelect | (Int) -> Unit | — | 选中回调,参数是下标 |
modifier | Modifier | Modifier | 修饰符 |
leading | (@Composable () -> Unit)? | null | 行首 / 列首的自定义插槽,属于这一行、跟着胶囊一起滚 |
fillItemWidth | Boolean | true | 仅 PillTabsColumn。每个胶囊是否占满整行宽度 |
itemAlignment | Alignment.Horizontal | Alignment.Start | 仅 PillTabsColumn。fillItemWidth = true 时此项无效 |
PillTab
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
label | String | — | 文字,单行、不换行 |
selected | Boolean | — | 选中态,状态完全外置 |
onClick | () -> Unit | — | 点击回调 |
modifier | Modifier | Modifier | 修饰符 |
size | PillTabSize | PillTabSize.MEDIUM | 尺寸档位 |
enabled | Boolean | true | 禁用时 38% 透明度且不可点 |
icon | ImageVector? | null | 左侧图标 |
count | Int? | null | 右侧数字角标 |
trailingContent | @Composable (() -> Unit)? | null | 右侧自定义插槽,优先级高于 count |
PillTabBadge
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
count | Int | — | 角标数字 |
selected | Boolean | — | 跟随所在胶囊的选中态 |
modifier | Modifier | Modifier | 修饰符 |
enabled | Boolean | true | 跟随所在胶囊的启用态 |
PillTabSize
| 档位 | 高度 | 文字 | 图标 | 图标 / 角标间距 | 左右内边距 |
|---|---|---|---|---|---|
SMALL | 28dp | labelSmall | 14dp | 6dp | 12dp |
MEDIUM | 34dp | labelLarge | 16dp | 8dp | 16dp |
LARGE | 40dp | titleSmall | 18dp | 10dp | 20dp |
要点
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在组件库里都已不存在。