ButtonGroup:不只是把按钮放进 Row · AndroidX 源码指南
AAndroidX 源码指南
M3E 设计系统
ANDROIDX / COMPOSE / MATERIAL 3

ButtonGroup:不只是把按钮放进 Row

理解按压宽度联动、溢出菜单、connected shapes,以及单选与多选的状态模型。

它解决哪三个问题

ButtonGroup 的源码职责比 Row 多:

  1. 子项使用 animateWidth 后,按下项可以扩宽,同时压缩相邻项。
  2. 内容过多或过宽时,尾部项目进入 dropdown menu。
  3. 通过 connected shapes 构建首、中、尾不对称的连接按钮。

最小 ButtonGroup

ButtonGroup(
    overflowIndicator = { menuState ->
        ButtonGroupDefaults.OverflowIndicator(menuState)
    },
) {
    clickableItem(onClick = {}, label = "日")
    clickableItem(onClick = {}, label = "周")
    clickableItem(onClick = {}, label = "月")
}

overflowIndicator 是必填参数。空间不够时,ButtonGroup 使用它作为末尾入口,并把不可见项目放入内部 DropdownMenu

为什么按压时会挤压旁边

默认 expandedRatio0.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 语义即可。

什么时候不用它

如果按钮之间互不相关、不需要宽度联动、也不会发生溢出,普通 RowFlowRow 更直接。不要只因为按钮并排,就引入 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 的状态模型。