为何两个Material 3 Button组件高度不一致?
我正在实现一个内置CircularProgressIndicator的动态“加载按钮”,已经完成了缩放逻辑,但遇到了按钮默认高度不一致的问题。简化后的代码如下,上方按钮高度为40.dp,下方按钮高度为48.dp。我知道可以通过LocalMinimumInteractiveComponentEnforcement将下方按钮设为40.dp,但这样尺寸偏小,不符合需求。
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { AppTheme { // A surface container using the 'background' color from the theme Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background, ) { Column { var buttonSize by remember { mutableStateOf(DpSize.Zero) } val density = LocalDensity.current Button(onClick = { }) { Text(text = "I have height of 40 dp") } Button( modifier = Modifier .then(if (buttonSize != DpSize.Zero) Modifier.size(buttonSize) else Modifier) .onSizeChanged { newSize -> if (buttonSize == DpSize.Zero) { buttonSize = with(density) { newSize .toSize() .toDpSize() } } }, onClick = {}, ) { Text(text = "I have height of 48 dp") } } } } } } }
原因分析
两个按钮高度差异的核心在于布局测量顺序与Modifier的作用时机:
- 第一个Button:Compose会根据Button默认内部padding、文本尺寸计算出内容适配的最小高度(40.dp,符合Material Design中小按钮的默认规格),直接以此尺寸完成布局。
- 第二个Button:
onSizeChanged会在首次布局完成后触发回调,此时Button已经应用了Compose默认的交互式组件最小触摸尺寸限制(48.dp)——这是为了保证可触摸区域足够大的系统行为。当你通过onSizeChanged记录这个尺寸后,后续会强制将Button固定为48.dp,导致它比第一个按钮高。
简言之,第一个Button没有被强制固定尺寸,使用的是内容驱动的最小高度;第二个Button因为捕获了带触摸限制的尺寸,进而锁定了更大的高度。
解决方法
以下两种方案可让两个按钮高度一致,同时避免尺寸偏小的问题:
方案1:直接计算Button的真实内容尺寸
不依赖onSizeChanged捕获带限制的尺寸,而是提前计算文本+Button默认内部padding的真实尺寸:
Column { val textMeasurer = rememberTextMeasurer() val textSize = textMeasurer.measure( text = "I have height of 40 dp", style = MaterialTheme.typography.labelLarge // 匹配Button默认文本样式 ).size val buttonPadding = ButtonDefaults.ContentPadding // 获取Button默认内部padding val density = LocalDensity.current val buttonWidth = with(density) { textSize.width.toDp() + buttonPadding.start + buttonPadding.end } val buttonHeight = with(density) { textSize.height.toDp() + buttonPadding.top + buttonPadding.bottom } val targetSize = DpSize(buttonWidth, buttonHeight) Button(onClick = { }) { Text(text = "I have height of 40 dp") } Button( modifier = Modifier.size(targetSize), onClick = {}, ) { Text(text = "Now I have height of 40 dp") } }
方案2:临时禁用触摸限制捕获真实尺寸
如果必须使用onSizeChanged,可在首次测量时禁用触摸尺寸限制,捕获真实内容尺寸后再恢复:
Column { var buttonSize by remember { mutableStateOf(DpSize.Zero) } val density = LocalDensity.current val isSizeCaptured = remember { mutableStateOf(false) } Button(onClick = { }) { Text(text = "I have height of 40 dp") } CompositionLocalProvider( LocalMinimumInteractiveComponentEnforcement provides !isSizeCaptured.value ) { Button( modifier = Modifier .then(if (buttonSize != DpSize.Zero) Modifier.size(buttonSize) else Modifier) .onSizeChanged { newSize -> if (buttonSize == DpSize.Zero) { buttonSize = with(density) { newSize.toSize().toDpSize() } isSizeCaptured.value = true } }, onClick = {}, ) { Text(text = "Now I have height of 40 dp") } } }
内容的提问来源于stack exchange,提问作者Tob237
相关产品推荐
相关产品推荐

