M3E 设计系统
ANDROIDX / COMPOSE / MATERIAL 3
ButtonGroup:不只是把按钮放进 Row
理解按压宽度联动、溢出菜单、connected shapes,以及单选与多选的状态模型。
它解决哪三个问题
ButtonGroup 的源码职责比 Row 多:
- 子项使用
animateWidth后,按下项可以扩宽,同时压缩相邻项。 - 内容过多或过宽时,尾部项目进入 dropdown menu。
- 通过 connected shapes 构建首、中、尾不对称的连接按钮。
最小 ButtonGroup
ButtonGroup(
overflowIndicator = { menuState ->
ButtonGroupDefaults.OverflowIndicator(menuState)
},
) {
clickableItem(onClick = {}, label = "日")
clickableItem(onClick = {}, label = "周")
clickableItem(onClick = {}, label = "月")
}overflowIndicator 是必填参数。空间不够时,ButtonGroup 使用它作为末尾入口,并把不可见项目放入内部 DropdownMenu。
为什么按压时会挤压旁边
默认 expandedRatio 是 0.15f:按压项最多扩展自身宽度的 15%,这个变化会传播给邻居。源码还允许:
0f:完全关闭扩宽和邻居压缩。1f:按压项可以扩展到默认宽度的 200%。
只有子项通过 Modifier.animateWidth(interactionSource, compressionLimit) 把交互源交给 group,这套联动才会发生。只把普通 Button 塞进去不会自动知道谁正在按压。
Connected group 是形状规则
AndroidX sample 的单选 connected group 实际使用 FlowRow + ToggleButton,再按照位置选择:
val shapes = when (index) {
0 -> ButtonGroupDefaults.connectedLeadingButtonShapes()
options.lastIndex -> ButtonGroupDefaults.connectedTrailingButtonShapes()
else -> ButtonGroupDefaults.connectedMiddleButtonShapes()
}所以“connected”主要是首、中、尾形状和间距规则,并不要求所有场景都由 ButtonGroup 容器完成。
单选与多选不要混用状态模型
// 单选:一个索引
var selectedIndex by remember { mutableIntStateOf(0) }
// 多选:每项一个 Boolean
val checked = remember { mutableStateListOf(false, false, false) }单选项还应设置 Role.RadioButton。多选项保留 ToggleButton 默认的 checkbox 语义即可。
什么时候不用它
如果按钮之间互不相关、不需要宽度联动、也不会发生溢出,普通 Row 或 FlowRow 更直接。不要只因为按钮并排,就引入 ButtonGroup 的状态与测量逻辑。
复制即用
日/周/月的单选分组(带溢出菜单):
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.ButtonGroup
import androidx.compose.material3.ButtonGroupDefaults
import androidx.compose.material3.clickableItem
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@Composable
fun PeriodSelector() {
var selectedIndex by remember { mutableIntStateOf(0) }
val labels = listOf("日", "周", "月")
ButtonGroup(
overflowIndicator = { menuState ->
ButtonGroupDefaults.OverflowIndicator(menuState)
},
) {
labels.forEachIndexed { index, label ->
clickableItem(
onClick = { selectedIndex = index },
label = label,
)
}
}
}单选分组建议再给选中项设置 Role.RadioButton;多选场景用每项一个 Boolean 的状态模型。