返回

Android Compose中Weight与最小宽度并存的几种方案

Android

Android Compose 中如何结合 weight 和 min width 属性

有时我们需要实现这样的布局:一个 Row 布局中包含多个 Column,这些 Column 按照比例(weight)分配宽度。 但又需要给其中的某个 Column 设置最小宽度(min width)。 碰到这种情况,直接组合使用 Modifier.weight 和设置最小宽度属性可能不生效。这篇博客就是要解决这个问题。

问题复现与原因分析

看下面这段代码:

Row(
    modifier = Modifier
        .padding(paddingValues)
        .fillMaxWidth()
) {
    //期望按照 1:3 分配
    ColumnA(Modifier.weight(1f).defaultMinSize(minWidth = 150.dp)) //min width of 150 dp
    ColumnB(Modifier.weight(3f))
}

正常状态:
ColumnA和ColumnB按照1:3的比例排列

非常规情况:
期望状态,如果ColumnA和ColumnB按1:3比例展示,那么ColumnA宽度将低于150dp,所以需要维持ColumnA宽度在150dp,剩下的才给ColumnB.

但是结果通常不尽人意,defaultMinSizeminWidthminWidthIn 并不能保证 ColumnA 的宽度保持在设定的最小值(例如 150dp)。

问题出在哪呢?直接原因在于 Modifier.weight 和 最小宽度约束的冲突。weight 会尝试根据权重填充可用空间,而最小宽度则规定了一个下限。Compose 的布局系统在处理这种冲突时,weight 的优先级可能会更高,导致最小宽度被忽略。

解决方案

针对上面的问题,提供几种解决思路。

1. 使用 ConstraintLayout

ConstraintLayout 提供了更灵活的布局方式,可以更好地处理这种复杂的约束。

  • 原理: ConstraintLayout 通过约束来定位和确定组件的大小,可以轻松处理复杂的布局场景。
  • 代码示例:
ConstraintLayout(
    modifier = Modifier
        .fillMaxWidth()
        .padding(paddingValues)
) {
    val (columnA, columnB, columnC) = createRefs()

    ColumnA(
        modifier = Modifier
            .constrainAs(columnA) {
                start.linkTo(parent.start)
                width = Dimension.preferredWrapContent.atLeast(150.dp)
            }
    )

    ColumnB(
        modifier = Modifier
            .constrainAs(columnB) {
                start.linkTo(columnA.end)
                end.linkTo(columnC.start)
                width = Dimension.fillToConstraints
           }
    )
     ColumnC( //This is new view.
            modifier = Modifier.constrainAs(columnC){
                end.linkTo(parent.end)
            }
     )
}

在这个示例中设置了三个组件columnA、columnB、columnC. 对于 ColumnA,我们约束其开始于父布局的开始,并使用 Dimension.preferredWrapContent.atLeast(150.dp) 来指定其宽度:尽量自适应内容,但最小为 150dp。 ColumnB使用Dimension.fillToConstraints撑满剩余宽度。
而ColumnC仅仅简单约束到parent的右边。

  • 进阶使用:Chains(链)

ConstraintLayout中可以用Chains对一组水平或垂直方向的组件设置排列方式,让分配方式更加可控。 例如, 可以使用 createHorizontalChain 并指定 ChainStyle

2. 自定义 Layout

如果需要更精细的控制,可以自定义 Layout

  • 原理: 通过重写 MeasurePolicymeasure 方法,可以自定义子组件的测量和布局逻辑。
  • 代码示例:
@Composable
fun CustomRow(
    modifier: Modifier = Modifier,
    minWidthForA: Dp,
    content: @Composable () -> Unit
) {
    Layout(
        content = content,
        modifier = modifier
    ) { measurables, constraints ->
        // 1. 先测量 ColumnA,确保它至少有 minWidthForA 的宽度.
        val measurableA = measurables[0]
        val placeableA = measurableA.measure(Constraints(minWidth = minWidthForA.roundToPx(), maxWidth = constraints.maxWidth))

        // 2. 测量 ColumnB, 可用宽度减去 ColumnA 的宽度.
        val remainingWidth = constraints.maxWidth - placeableA.width
        val measurableB = measurables[1]
        val placeableB = measurableB.measure(Constraints(maxWidth = remainingWidth))

    val measurableC = if(measurables.size>2) measurables[2] else null
    val placeableC = measurableC?.measure(Constraints(maxWidth =  constraints.maxWidth))


        // 3. 设置整个 CustomRow 的宽度和高度。
         val totalWidth = placeableA.width+ (placeableB.width.takeIf { placeableC==null } ?: (placeableB.width+ (placeableC?.width?:0) ))
        //val totalWidth = constraints.maxWidth  //可以使用全部宽度。
        val totalHeight = maxOf(placeableA.height, placeableB.height,placeableC?.height?:0)

        layout(totalWidth, totalHeight) {
            // 4. 放置 ColumnA 和 ColumnB.
            placeableA.placeRelative(0, 0)
          var  columnBOffset = placeableA.width
            placeableB.placeRelative(columnBOffset, 0)
         var columnCOffset =   columnBOffset+ placeableB.width
            placeableC?.placeRelative(columnCOffset,0)
        }
    }
}

使用:

CustomRow(
   modifier = Modifier.fillMaxWidth(),
   minWidthForA = 150.dp
) {
       ColumnA()
       ColumnB()
}

这个 CustomRow 首先测量 ColumnA,确保其宽度至少为 minWidthForA。 然后,它计算剩余宽度,并用这个宽度测量 ColumnB。 最后放置两个 Column。如有ColumnC同理计算。

  • 进阶使用
    可以给自定义的layout传递更多的参数,例如自定义的weight, 对齐方式等等。

3. 根据内容动态调整

这个方法利用组合的特性实现。

  • 原理: 先不设置权重,测量第一个 Column 的宽度,如果其宽度小于最小值,则后续动态的调整比例。
  • 代码实现:

@Composable
fun AdaptiveRow(
        modifier: Modifier,
        minWidthA: Dp,
        contentA:@Composable ()->Unit,
        contentB:@Composable ()->Unit
){
        var columnAWidth by remember { mutableStateOf(0.dp) }
    Row(modifier = modifier,){
      Box(modifier = Modifier.onGloballyPositioned {
              columnAWidth = it.size.width.toDp()
      }) {
          contentA()
      }

        val weightForB = if(columnAWidth>minWidthA) 3f else 1f
        //计算一个合适的weightForA

           val weightForA =  if(columnAWidth>minWidthA)  1f else {
               //简单换算一下:
                 (minWidthA/columnAWidth).takeIf { it.isFinite() } ?: 1f
           }
          //这里使用了weight。也可以按照方法1,方法2进行布局处理。
        Box(modifier = Modifier.weight(weightForB *weightForA )){
              contentB()
        }
    }
}

这个方法,用onGloballyPositioned获取到第一个box的实际宽度,然后据此计算合适的比例应用到第二个box的weight参数上。需要留意处理计算可能出现的除0异常情况。

  • 额外提示:
    这种方案不一定通用,需要按需调整。好处是可以继续使用weight

总结

要实现 weight 和最小宽度的组合效果,单纯依赖传统的属性设置是比较困难的。灵活利用ConstraintLayout、自定义 Layout 或者根据测量值动态调整,可以更好地应对这类布局需求。 这几种方法,从简单到复杂,提供了不同的解决问题的思路,具体使用哪个,根据你的具体需求决定就好。