Jetpack Compose 使用 SubcomposeLayout 动态地将兄弟 Composables 的宽度设置为最长的宽度

Posted

技术标签:

【中文标题】Jetpack Compose 使用 SubcomposeLayout 动态地将兄弟 Composables 的宽度设置为最长的宽度【英文标题】:Jetpack Compose set sibling Composables' width to longest one dynamically with SubcomposeLayout 【发布时间】:2022-01-18 11:40:08 【问题描述】:

如何设置一组组合的宽度,从上到下的兄弟布局,最长的宽度?

我尝试构建的内容与上面的图片完全相同。为简单起见,假设 quote 顶部的组件和 message box 包含消息和另一个存储日期和消息状态的容器。

quote 和 message box 中最长的一个必须设置为父宽度,另一个必须设置为与最长的宽度相同的宽度,这需要重新测量我假设的短的。

此外,如果 message box 调整大小,则需要一个内部参数传递此宽度以设置存储日期和状态的容器的位置。可以清楚地看到,当 quote 长于 message box 时,消息文本移动到开始,而状态结束。当消息有多于一行时,消息框的宽度和高度是通过电报或whatsapp的计算来设置的。

最初使用Layout 构建它

@Composable
private fun DynamicLayout(
    modifier: Modifier = Modifier,
    quote: @Composable () -> Unit,
    message: @Composable () -> Unit
) 

    val content = @Composable 
        quote()
        message()
    

    Layout(content = content, modifier = modifier)  measurables, constraints ->


        val placeableQuote = measurables.first().measure(constraints)
        val quoteWidth = placeableQuote.width

        val placeableMessage =
            measurables.last()
                .measure(Constraints(minWidth = quoteWidth, maxWidth = constraints.maxWidth))
        val messageWidth = placeableMessage.width

        val maxWidth = quoteWidth.coerceAtLeast(messageWidth)
        val totalHeight = placeableQuote.height + placeableMessage.height

        layout(maxWidth, totalHeight) 
            placeableQuote.placeRelative(x = 0, y = 0)
            placeableMessage.placeRelative(x = 0, y = placeableQuote.height)
        
    

在我使用 quote 约束的宽度测量 message box 的情况下,它可以工作,但仅在quote 较长时才有效。

DynamicLayout(
    quote = 
        Text(
            "QUOTE with a very long text",
            modifier = Modifier
                .background(Color(0xffF44336))
                .height(60.dp),
            color = Color.White
        )
    ,
    message = 
        Text(
            "MESSAGE Content",
            modifier = Modifier
                .background(Color(0xff9C27B0)),
            color = Color.White
        )
    
)

DynamicLayout(
    quote = 
        Text(
            "QUOTE",
            modifier = Modifier
                .background(Color(0xffF44336))
                .height(60.dp),
            color = Color.White
        )
    ,
    message = 
        Text(
            "MESSAGE with very long Content",
            modifier = Modifier
                .background(Color(0xff9C27B0)),
            color = Color.White
        )
    
)

由于必须重新测量,我认为应该使用SubComposeLayout 解决这个问题,但无法弄清楚如何将其用于此设置?

@Composable
private fun SubComponentLayout(
    modifier: Modifier = Modifier,
    mainContent: @Composable () -> Unit,
    dependentContent: @Composable (Int) -> Unit
) 

    SubcomposeLayout(modifier = modifier)  constraints ->

        val mainMeasurables: List<Measurable> = subcompose(SlotsEnum.Main, mainContent)

        val mainPlaceables: List<Placeable> = mainMeasurables.map 
            it.measure(constraints)
        

        val maxSize =
            mainPlaceables.fold(IntSize.Zero)  currentMax: IntSize, placeable: Placeable ->
                IntSize(
                    width = maxOf(currentMax.width, placeable.width),
                    height = maxOf(currentMax.height, placeable.height)
                )
            

        var maxWidth =
            mainPlaceables.maxOf  it.width 


        layout(maxSize.width, maxSize.height) 

            println("???? SubcomposeLayout-> layout() maxSize width: $maxSize.width, height: $maxSize.height")

            val dependentMeasurables: List<Measurable> = subcompose(
                slotId = SlotsEnum.Dependent,
                content = 
                    println("???? SubcomposeLayout-> layout()->subcompose() mainWidth ZERO")
                    dependentContent(0)
                
            )

            val dependentPlaceables: List<Placeable> = dependentMeasurables.map 
                it.measure(constraints)
            

            maxWidth = maxWidth.coerceAtLeast(
                dependentPlaceables.maxOf  it.width 
            )

            subcompose(SlotsEnum.NEW) 
                println("???? SubcomposeLayout-> layout()->subcompose() maxWidth: $maxWidth")
                dependentContent(maxWidth)
            

            mainPlaceables.forEach  it.placeRelative(0, 0) 
            dependentPlaceables.forEach  it.placeRelative(0, 150) 
        
    

为什么不能用相同的 id 第二次重新测量相同的组件?当我尝试使用 SlotsEnum.Dependent 调用 subCompose 时,它会引发异常

subcompose(SlotsEnum.NEW) 
    println("???? SubcomposeLayout-> layout()->subcompose() maxWidth: $maxWidth")
    dependentContent(maxWidth)

调用后仍然无法正确重新测量? SubcomposeLayout如何解决设置兄弟的问题?

【问题讨论】:

【参考方案1】:

我根据官方文档提供的样本和@chuckj的回答here做了一个样本。

橙色和粉红色的容器是 Columns,它们指导 DynamicWidthLayout 的子级,使用 SubcomposeLayout 重新测量。

@Composable
private fun DynamicWidthLayout(
    modifier: Modifier = Modifier,
    mainContent: @Composable () -> Unit,
    dependentContent: @Composable (IntSize) -> Unit
) 

    SubcomposeLayout(modifier = modifier)  constraints ->


        var mainPlaceables: List<Placeable> = subcompose(SlotsEnum.Main, mainContent).map 
            it.measure(constraints)
        

        var maxSize =
            mainPlaceables.fold(IntSize.Zero)  currentMax: IntSize, placeable: Placeable ->
                IntSize(
                    width = maxOf(currentMax.width, placeable.width),
                    height = maxOf(currentMax.height, placeable.height)
                )
            

        val dependentMeasurables: List<Measurable> = subcompose(SlotsEnum.Dependent) 
            // ?? Send maxSize of mainComponent to
            // dependent composable in case it might be used
            dependentContent(maxSize)
        

        val dependentPlaceables: List<Placeable> = dependentMeasurables
            .map  measurable: Measurable ->
                measurable.measure(Constraints(maxSize.width, constraints.maxWidth))
            

        // Get maximum width of dependent composable
        val maxWidth = dependentPlaceables.maxOf  it.width 


        println("? DynamicWidthLayout-> maxSize width: $maxSize.width, height: $maxSize.height")

        // If width of dependent composable is longer than main one, remeasure main one
        // with dependent composable's width using it as minimumWidthConstraint
        if (maxWidth > maxSize.width) 

            println("? DynamicWidthLayout REMEASURE MAIN COMPONENT")

            // !!! ?? CANNOT use SlotsEnum.Main here why?
            mainPlaceables = subcompose(2, mainContent).map 
                it.measure(Constraints(maxWidth, constraints.maxWidth))
            
        

        // Our final maxSize is longest width and total height of main and dependent composables
        maxSize = IntSize(
            maxSize.width.coerceAtLeast(maxWidth),
            maxSize.height + dependentPlaceables.maxOf  it.height 
        )


        layout(maxSize.width, maxSize.height) 

            // Place layouts
            mainPlaceables.forEach  it.placeRelative(0, 0) 
            dependentPlaceables.forEach 
                it.placeRelative(0, mainPlaceables.maxOf  it.height )
            
        
    



enum class SlotsEnum  Main, Dependent 

用法

@Composable
private fun TutorialContent() 

    val density = LocalDensity.current.density

    Column(
        modifier = Modifier
            .fillMaxSize()
            .verticalScroll(rememberScrollState())
    ) 


        var mainText by remember  mutableStateOf(TextFieldValue("Main Component")) 
        var dependentText by remember  mutableStateOf(TextFieldValue("Dependent Component")) 


        OutlinedTextField(
            modifier = Modifier
                .padding(horizontal = 8.dp)
                .fillMaxWidth(),
            value = mainText,
            label =  Text("Main") ,
            placeholder =  Text("Set text to change main width") ,
            onValueChange =  newValue: TextFieldValue ->
                mainText = newValue
            
        )

        OutlinedTextField(
            modifier = Modifier
                .padding(horizontal = 8.dp)
                .fillMaxWidth(),
            value = dependentText,
            label =  Text("Dependent") ,
            placeholder =  Text("Set text to change dependent width") ,
            onValueChange =  newValue ->
                dependentText = newValue
            
        )

        DynamicWidthLayout(
            modifier = Modifier
                .padding(8.dp)
                .background(Color.LightGray)
                .padding(8.dp),
            mainContent = 

                println("? DynamicWidthLayout-> MainContent  composed")

                Column(
                    modifier = Modifier
                        .background(orange400)
                        .padding(4.dp)
                ) 
                    Text(
                        text = mainText.text,
                        modifier = Modifier
                            .background(blue400)
                            .height(40.dp),
                        color = Color.White
                    )
                
            ,
            dependentContent =  size: IntSize ->


                // ? Measure max width of main component in dp  retrieved
                // by subCompose of dependent component from IntSize
                val maxWidth = with(density) 
                    size.width / this
                .dp

                println(
                    "? DynamicWidthLayout-> DependentContent composed " +
                            "Dependent size: $size, "
                            + "maxWidth: $maxWidth"
                )

                Column(
                    modifier = Modifier
                        .background(pink400)
                        .padding(4.dp)
                ) 

                    Text(
                        text = dependentText.text,
                        modifier = Modifier
                            .background(green400),
                        color = Color.White
                    )
                
            
        )
    

完整的源代码是here。

【讨论】:

以上是关于Jetpack Compose 使用 SubcomposeLayout 动态地将兄弟 Composables 的宽度设置为最长的宽度的主要内容,如果未能解决你的问题,请参考以下文章

Android Jetpack Compose学习—— Jetpack compose基础布局

Android Jetpack Compose学习—— Jetpack compose基础布局

Android Jetpack Compose学习—— Jetpack compose基础布局

什么是Jetpack Compose?带你走进Jetpack Compose~

Android Jetpack Compose学习—— Jetpack compose入门

Android Jetpack Compose学习—— Jetpack compose入门