Jetpack Compose(八)-常用的布局组件

一、Column从上到下

Column是一个垂直线性布局组件,它能够将子项按照从上到下的顺序垂直排列。先看一下参数:

kotlin 复制代码
@Composable
inline fun Column(
    modifier: Modifier = Modifier,
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,   //垂直方向的排列
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,   //水平方向的排列
    content: @Composable ColumnScope.() -> Unit
) {...}

verticalArrangmenthorizontalAlignment参数分别可以帮助我们安排子项的垂直/水平位置,在默认的情况下,子项会以垂直方向上靠上(Arrangment. Top),水平方向上靠左(Alignment. Start)来布置。

下面是一个例子:

kotlin 复制代码
@Composable
fun Greeting() {
    Column(
        modifier = Modifier
            .border(1.dp, Color.Blue)
            .size(300.dp),
        verticalArrangement = Arrangement.Center,    //垂直居中
        horizontalAlignment = Alignment.CenterHorizontally    //水平居中
    ) {
        Text(text = "Hello,Compose", modifier = Modifier.align(Alignment.Start))   //水平居左
        Text(text = "Hello,Android")
    }
}

UI效果

对于垂直布局中的子项,Modifier.align只能设置自己在水平方向的位置,反之水平布局的子项,只能设置自己在垂直方向的位置,并且Modifier.align修饰符会优先于ColumnhorizontalAlignment参数。

Column的参数verticalArrangement 有多个值,在Jetpack Compose 博物馆上有一张图描述的很清楚,如下:

二、Row从左到右

Row组件能够将内部子项按照从左到右的方向水平排列。参数如下:

kotlin 复制代码
@Composable
inline fun Row(
    modifier: Modifier = Modifier,
    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
    verticalAlignment: Alignment.Vertical = Alignment.Top,
    content: @Composable RowScope.() -> Unit
) {...}

下面是一个例子:

kotlin 复制代码
@Composable
fun Greeting() {
    Row(
        modifier = Modifier
            .border(1.dp, Color.Blue)
            .size(300.dp),
        verticalAlignment = Alignment.CenterVertically,   //整体垂直居中
        horizontalArrangement = Arrangement.Center    //整体水平居中
    ) {
        Text(text = "Hello,Compose", modifier = Modifier.align(Alignment.Bottom))   //单独定义子项的位置
        //增加竖直实线更好观察二个Text
        Divider(color = Color.Blue, modifier = Modifier
            .width(1.dp)
            .fillMaxHeight())
        Text(text = "Hello,Android")
    }
}

UI效果

Row的参数horizontalArrangement 有多个值,在Jetpack Compose 博物馆上有一张图描述的很清楚,如下:

三、Box重叠

Box组件是一个能够将里面的子项依次按照顺序堆叠的布局组件,在使用上类似于传统视图中的FrameLayout。参数如下:

kotlin 复制代码
@Composable
inline fun Box(
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.TopStart,
    propagateMinConstraints: Boolean = false,    //是否传入的最小约束应该传递给content
    content: @Composable BoxScope.() -> Unit
) {...}

在显示上,下面的组件盖在上面的组件上。Box经常用来让文本在一定范围内居中:

kotlin 复制代码
@Composable
fun Greeting() {
    Box(
        modifier = Modifier
            .border(1.dp, Color.Blue)
            .size(300.dp),
        contentAlignment = Alignment.Center
    ) {
        Text(text = "Hello,Compose")
    }
}

UI效果

四、Spacer留白

在很多时候,需要让两个组件之间留有空白的间隔,这个时候就可以使用Spacer组件。参数如下:

kotlin 复制代码
@Composable
@NonRestartableComposable
fun Spacer(modifier: Modifier) {
    Layout({}, measurePolicy = SpacerMeasurePolicy, modifier = modifier)
}

如果想设置横向或者纵向的留白,只需要设置modifier的宽高即可。下面是一个例子:

kotlin 复制代码
Spacer(modifier = Modifier.height(30.dp))

五、ConstraintLayout约束布局

在构建嵌套层级复杂的视图界面时,使用约束布局可以有效降低视图树高度,使视图树扁平化。约束布局在测量布局耗时上,比传统的相对布局具有更好的性能表现,并且约束布局可以根据百分比自适应各种尺寸的终端设备。因为约束布局ConstraintLayout十分好用,所以官方为我们迁移到了Compose平台。

使用之前需要导包,如下:

kotlin 复制代码
implementation "androidx.constraintlayout:constraintlayout-compose:1.0.1"

下面是ConstraintLayout的参数:

kotlin 复制代码
@Composable
inline fun ConstraintLayout(
    modifier: Modifier = Modifier,
    optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
    crossinline content: @Composable ConstraintLayoutScope.() -> Unit
) {...}

kotlin 复制代码
@OptIn(ExperimentalMotionApi::class)
@Suppress("NOTHING_TO_INLINE")
@Composable
inline fun ConstraintLayout(
    constraintSet: ConstraintSet,
    modifier: Modifier = Modifier,
    optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
    animateChanges: Boolean = false,
    animationSpec: AnimationSpec<Float> = tween<Float>(),
    noinline finishedAnimationListener: (() -> Unit)? = null,
    noinline content: @Composable () -> Unit
) {...}

1、创建与绑定引用

XML文件中可以为View组件设置资源ID,并将资源ID作为索引来声明组件应当摆放的位置。在Compose版本的ConstraintLayout中,可以主动创建引用并绑定至某个具体组件上,从而实现资源ID相似的功能。每个组件都可以利用其他组件的引用获取到其他组件的摆放位置信息,从而确定自己应摆放的位置。

Compose中有两种创建引用的方式:createRef()createRefs()。字面意思非常清楚,createRef()每次只会创建一个引用,而createRefs()每次可以创建多个引用(最多16个)。

kotlin 复制代码
ConstraintLayout {     //ConstraintLayoutScope作用域内才写的出来
    //createRef
    val portraitImageRef = remember {
        createRef()
    }
    val userNameTextRef = remember {
        createRef()
    }
    //createRefs
    val (portraitImageRef,userNameTextRef) = remember{ createRefs()}
}

接下来可以使用Modifier.constrainAs()修饰符将前面创建的引用绑定到某个具体组件上。可以在constrainAs尾部Lambda内指定组件的约束信息。值得注意的是,我们只能在ConstraintLayout尾部的Lambda中使用createRef()createRefs()创建引用,并使用Modifier.constrainAs()来绑定引用,这是因为ConstrainScope尾部LambdaReciever是一个ConstraintLayoutScope作用域对象。

Modifier.constrainsAs()尾部Lambda是一个ConstrainScope作用域对象,可以在其中获取到当前组件的parenttopbottomstartend等信息,并使用linkTo指定组件约束。

下面是一个简单的例子,让图片二排列在图片一的右下角:

kotlin 复制代码
@SuppressLint("RememberReturnType")
@Composable
fun Greeting() {
    ConstraintLayout(modifier = Modifier.fillMaxSize()) {
        //createRef
        val imageOneRef = remember {   //创建图片一的id
            createRef()
        }
        val imageTwoRef = remember {   //创建图片二的id
            createRef()
        }
        //图片一
        Image(
            painter = painterResource(id = R.mipmap.rabit2),
            contentDescription = null,
            modifier = Modifier
                .size(150.dp)
                .constrainAs(imageOneRef) {   //图片一组件绑定id
                    //ConstrainScope作用域内
                    top.linkTo(parent.top)        //定义位置,这里还能加margin
                    start.linkTo(parent.start)    //定义位置,这里还能加margin
                })

        //图片二
        Image(
            painter = painterResource(id = R.mipmap.rabit2),
            contentDescription = null,
            modifier = Modifier
                .size(150.dp)
                .constrainAs(imageTwoRef) {    //图片二组件绑定id
                    //ConstrainScope作用域内
                    top.linkTo(imageOneRef.bottom)    //定义位置,这里还能加margin
                    start.linkTo(imageOneRef.end)     //定义位置,这里还能加margin
                })
    }

}

UI效果

也可以在ConstrainScope作用域中指定组件的宽高信息,在ConstrainScope中直接设置widthheight即可,有几个可选值可供使用,如下表所示:

Dimension可选值 描述
wrapContent() 实际尺寸为根据内容自适应的尺寸
matchParent() 实际尺寸为铺满整父组件的尺寸
fillToConstraints() 实际尺寸为根据约束信息拉伸后的尺寸
preferredWrapContent() 如果剩余空间大于根据内容自适应的尺寸时,实际尺寸为自适应的尺寸。如果剩余空间小于内容自适应的尺寸时,实际尺寸则为剩余空间的尺寸
ratio (String) 根据字符串计算实际尺寸所占比率,例如"1:2"
percent (Float) 根据浮点数计算实际尺寸所占比率
value (Dp) 将尺寸设置为固定值
preferredValue (Dp) 如果剩余空间大于固定值时,实际尺寸为固定值。如果剩余空间小于固定值时,实际尺寸则为剩余空间的尺寸

下面是一个例子,当文本过长时可以通过设置end来指定组件最大所允许的宽度,并将width设置为preferredWrapContent,这意味着当文本较短时,实际宽度会随着长度进行自适应调整:

kotlin 复制代码
@SuppressLint("RememberReturnType")
@Composable
fun Greeting() {
    ConstraintLayout(modifier = Modifier.fillMaxSize()) {
        //createRef
        val imageRef = remember {
            createRef()
        }
        val textRef = remember {
            createRef()
        }
        Image(
            painter = painterResource(id = R.mipmap.rabit2),
            contentDescription = null,
            modifier = Modifier.constrainAs(imageRef) {
                top.linkTo(parent.top)
                start.linkTo(parent.start)
                width = Dimension.value(200.dp)
                height = Dimension.value(200.dp)
            })

        Text(
            text = "这是一个超长的文本一个超长的文本",
            fontSize = 14.sp,
            modifier = Modifier
                .size(150.dp)
                .constrainAs(textRef) {
                    //ConstrainScope作用域
                    top.linkTo(parent.top)
                    start.linkTo(imageRef.end)
                    end.linkTo(parent.end)
                    width = Dimension.preferredWrapContent    //宽度自适应
                }) 
    }
}

UI效果

Compose版本的ConstraintLayout同样也继承了一些优质特性,例如BarrierGuidelineChain等,方便我们完成各种复杂场景的布局需求,接下来将逐一进行介绍。

2、Barrier分界线

依托单个控件或多个控件的位置虚拟一条分界线,用于对齐等操作。下面是一个例子:

kotlin 复制代码
@SuppressLint("RememberReturnType")
@Composable
fun Greeting() {
    ConstraintLayout(modifier = Modifier.fillMaxSize()) {
        //createRef
        val (textOneRef, textTwoRef, imageRef) = remember {
            createRefs()
        }
        //依托单个控件或多个控件的位置虚拟一条分界线,用于对齐等操作
        //在二个Text中较长的那个的结尾创建分界线
        val barrier = createEndBarrier(textOneRef, textTwoRef)
        
        //较长文本
        Text(
            text = "三个字",
            fontSize = 14.sp,
            modifier = Modifier
                .constrainAs(textOneRef) {
                    //ConstrainScope作用域
                    top.linkTo(parent.top)
                    start.linkTo(parent.start)
                    width = Dimension.preferredWrapContent
                })

        //较短文本
        Text(
            text = "二字",
            fontSize = 14.sp,
            modifier = Modifier
                .constrainAs(textTwoRef) {
                    //ConstrainScope作用域
                    top.linkTo(textOneRef.bottom)
                    start.linkTo(parent.start)
                    width = Dimension.preferredWrapContent
                })

        Image(
            painter = painterResource(id = R.mipmap.rabit2),
            contentDescription = null,
            modifier = Modifier
                .size(200.dp)
                .constrainAs(imageRef) {
                    //ConstrainScope作用域
                    top.linkTo(parent.top)
                    start.linkTo(barrier)    //依托分界线对齐
                    width = Dimension.preferredWrapContent
                }
        )
    }
}

UI效果

3、Guideline引导线

Barrier分界线是需要依赖其他控件,从而确定自身位置的。而Guideline不依赖任何引用,凭空创建出一条引导线。比如下面的例子,可以使用createGuidelineFromTop创建从顶部出发的引导线:

kotlin 复制代码
val guideline = createGuidelineFromTop(0.2F)

还有很多种其他方式,按需选用。

4、Chain链接约束

ConstraintLayout另一个非常好用的特性就是Chain链接约束,通过链接约束可以允许多个组件平均分配布局空间,这个功能类似于weight修饰符。下面是一个例子:

kotlin 复制代码
@SuppressLint("RememberReturnType")
@Composable
fun Greeting() {
    ConstraintLayout(modifier = Modifier.fillMaxSize()) {
        //createRef
        val (imageOneRef, imageTwoRef) = remember {
            createRefs()
        }
        //创建Chain,设置chainStyle
        createVerticalChain(imageOneRef, imageTwoRef, chainStyle = ChainStyle.Spread)

        Image(
            painter = painterResource(id = R.mipmap.rabit2),
            contentDescription = null,
            modifier = Modifier
                .size(200.dp)
                .constrainAs(imageOneRef) {  //设置id即可
                    //ConstrainScope作用域
                    start.linkTo(parent.start)
                    end.linkTo(parent.end)
                }
        )

        Image(
            painter = painterResource(id = R.mipmap.rabit2),
            contentDescription = null,
            modifier = Modifier
                .size(200.dp)
                .constrainAs(imageTwoRef) { //设置id即可
                    //ConstrainScope作用域
                    start.linkTo(parent.start)
                    end.linkTo(parent.end)
                }
        )
    }
}

UI效果

重点看一下三种ChainStyle

  • Spread:链条中每个元素平分整个parent空间。
  • SpreadInside:链条中首尾元素紧贴边界,剩下每个元素平分整个parent空间。
  • Packed:链条中所有元素聚集到中间。

六、Pager

Pager 即传统 View 体系中 ViewPager 的替代,但在使用上大大降低了复杂度。它包括 VerticalPagerHorizontalPager 两类,分别对应纵向和横向的滑动。Pager 的底层基于 LazyColumn/LazyRow 实现,在使用上也基本与它们等同。

1、HorizontalPager的参数

HorizontalPager为例说明一下参数:

kotlin 复制代码
@Composable
@ExperimentalFoundationApi
fun HorizontalPager(
    pageCount: Int,    //页面的数量
    modifier: Modifier = Modifier,
    state: PagerState = rememberPagerState(),   //Pager的状态,通常使用`rememberPagerState`来创建
    contentPadding: PaddingValues = PaddingValues(0.dp),   //内容的内边距
    pageSize: PageSize = PageSize.Fill,   //页面的大小,可以是PageSize.Fill(填充满Pager)或PageSize.WrapContent(根据内容自适应大小)
    beyondBoundsPageCount: Int = 0,   //在Pager边界之外预加载的页面数量
    pageSpacing: Dp = 0.dp,     //页面之间的间距
    verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,    //垂直对齐方式
    flingBehavior: SnapFlingBehavior = PagerDefaults.flingBehavior(state = state),   //滑动行为,用于定义Pager的滑动效果
    userScrollEnabled: Boolean = true,     //是否允许用户滚动Pager
    reverseLayout: Boolean = false,        //是否以反向布局显示页面
    key: ((index: Int) -> Any)? = null,    //用于为页面提供唯一键的函数
    pageNestedScrollConnection: NestedScrollConnection = PagerDefaults.pageNestedScrollConnection(
        Orientation.Horizontal
    ),     //页面嵌套滚动连接,用于处理嵌套滚动事件
    pageContent: @Composable (page: Int) -> Unit    //用于绘制每个页面的内容的Composable函数
) {...} 

2、最简单的使用案例

下面是一个简单使用的例子:

kotlin 复制代码
// 显示 3 个项目
HorizontalPager(pageCount = 3) { page ->
    // 每一页的内容,比如显示个文本
    Text(
        text = "Page: $page",
        modifier = Modifier
            .fillMaxWidth()
            .height(300.dp)
            .background(color = Color.LightGray)
    )
}

UI效果

3、自定义指示器

如果需要带指示器,下面是一个自定义指示器的例子:

kotlin 复制代码
enum class Page(val value: String) {
    LIFE("生活"),
    FOOD("美食"),
    SCIENCE("科技")
}

private val pages = Page.values()

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun Greeting() {
    val pagerState = rememberPagerState()

    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(color = MaterialTheme.colors.background)
    ) {
        HorizontalPager(
            pageCount = 3,
            state = pagerState
        ) { position ->
            Box(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(300.dp)
                    .background(Color.Cyan),
                contentAlignment = Alignment.Center
            ) {
                Text(text = pages[position].value)
            }
        }
        //自定义指示器
        CustomIndicator(
            pagerState = pagerState,
            modifier = Modifier
                .align(Alignment.CenterHorizontally)
                .padding(10.dp),
            indicatorCount = pages.size
        )
    }
}


@OptIn(ExperimentalFoundationApi::class)
@Composable
fun CustomIndicator(
    pagerState: PagerState,
    modifier: Modifier = Modifier,
    activeColor: Color = MaterialTheme.colors.primary,
    inactiveColor: Color = Color.LightGray,
    indicatorWidth: Dp = 10.dp,
    indicatorHeight: Dp = 5.dp,
    spacing: Dp = 5.dp,
    indicatorShape: Shape = CircleShape,
    indicatorCount: Int
) {
    val spacingPx = LocalDensity.current.run { spacing.roundToPx() }

    Box(
        modifier = modifier,
        contentAlignment = Alignment.CenterStart
    ) {
        Row(
            horizontalArrangement = Arrangement.spacedBy(spacing),
            verticalAlignment = Alignment.CenterVertically,
        ) {
            val indicatorModifier = Modifier
                .background(color = inactiveColor, shape = indicatorShape)
            //不能活动的索引的点
            repeat(indicatorCount) {
                Box(
                    indicatorModifier.size(
                        indicatorWidth,
                        indicatorHeight
                    )
                )
            }
        }
        //计算偏移量
        val scrollPosition = (pagerState.currentPage + pagerState.currentPageOffsetFraction)
            .coerceIn(
                0f, (pages.size - 1).coerceAtLeast(0).toFloat()
            )
        //可以活动的索引点
        Box(
            Modifier
                .offset {
                    IntOffset(
                        x = (spacingPx * scrollPosition + indicatorWidth.roundToPx() * scrollPosition).toInt(),
                        y = 0
                    )
                }
                .size(width = indicatorWidth, height = indicatorHeight)
                .background(
                    color = activeColor,
                    shape = indicatorShape,
                )
        )
    }
}

UI效果

4、使用TabRow构建指示器

TabRow用于创建水平的选项卡栏,它通常用作与HorizontalPager等组件配合当做指示器,用于显示和切换不同的页面。下面是一个案例:

kotlin 复制代码
enum class Page(val value: String) {
    LIFE("生活"),
    FOOD("美食"),
    SCIENCE("科技")
}

private val pages = Page.values()

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun Greeting() {
    val pagerState = rememberPagerState()
    val animationScope = rememberCoroutineScope()
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(color = MaterialTheme.colors.background)
    ) {
        //TabRow指示器
        TabRow(
            selectedTabIndex = pagerState.currentPage,
            modifier = Modifier.fillMaxWidth(),
            backgroundColor = Color.LightGray
        ) {
            pages.forEachIndexed { index, item ->
                Tab(
                    selected = index == pagerState.currentPage,
                    text = { Text(item.value) },
                    onClick = {
                        animationScope.launch {
                            pagerState.animateScrollToPage(index)
                        }
                    }
                )
            }
        }

        //HorizontalPager
        HorizontalPager(
            pageCount = 3,
            state = pagerState
        ) { position ->
            Box(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(300.dp)
                    .background(Color.Cyan),
                contentAlignment = Alignment.Center
            ) {
                Text(text = pages[position].value)
            }
        }
    }
}

UI效果

七、SubcomposeLayout

1、固有特性测量

(1)固有特性测量是什么

Compose 中的每个 UI 组件是不允许多次进行测量的,多次测量在运行时会抛异常,禁止多次测量的好处是为了提高性能,但在很多场景中多次测量子 UI 组件是有意义的。在 Jetpack Compose 代码实验室中就提供了这样一种场景,我们希望中间分割线高度与两边文案高的一边保持相等。下图所示:

为实现这个需求,官方所提供的设计方案是希望父组件可以预先获取到两边的文案组件高度信息,然后计算两边高度的最大值即可确定当前父组件的高度值,此时仅需将分割线高度值铺满整个父组件即可。

为了实现父组件预先获取文案组件高度信息从而确定自身的高度信息,Compose 为开发者们提供了固有特性测量机制,允许开发者在每个子组件正式测量前能获取到每个子组件的宽高等信息。

(2)在基础组件中使用固有特性测量

在上面所提到的例子中父组件所提供的能力使用基础组件中的 Row 组件即可承担,我们仅需为 Row 组件高度设置固有特性测量即可。我们使用 Modifier.height(IntrinsicSize.Min) 即可为高度设置固有特性测量。代码如下:

kotlin 复制代码
@Composable
fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) {
    Row(modifier = modifier.height(IntrinsicSize.Min)) {   //这里使用了固有特性测量
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 4.dp)
                .wrapContentWidth(Alignment.Start),
            text = text1,
            fontSize = 16.sp    //字体大小不一样,高度不一样
        )

        Divider(color = Color.Black, modifier = Modifier.fillMaxHeight().width(1.dp))  //高度为父布局最大高度
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(end = 4.dp)
                .wrapContentWidth(Alignment.End),
            text = text2,
            fontSize = 30.sp    //字体大小不一样,高度不一样
        )
    }
}

//使用
Surface(border = BorderStroke(1.dp, Color.Blue) ) {
    TwoTexts(text1 = "Hi,kotlin", text2 = "Hello,World")
}

UI效果

简单一句话总结就是:测量完所有的子再确定父。

2、SubcomposeLayout

SubcomposeLayout可以做到将某个子组件的合成过程延迟至他所依赖的组件测量结束后进行,这也说明这个组件可以根据其他组件的测量信息确定自身的尺寸。先看一下SubcomposeLayout的参数:

kotlin 复制代码
@Composable
fun SubcomposeLayout(
    modifier: Modifier = Modifier,
    measurePolicy: SubcomposeMeasureScope.(Constraints) -> MeasureResult
) {...}

简单一句话总结就是:测量完依赖的子再确定子。

让我们再看一个在多语言中容易碰到的例子,下面的需求要求三个Tab宽度相等,在中文语境下没有问题,如下图:

切换到其他语言是没有办法保证Tab内的文本长度是相等的,如下图:

这个时候我们就需要找到最宽的那个子控件,然后让其他的子控件也设置同样的宽度,代码如下所示:

kotlin 复制代码
@Composable
fun Greeting() {
    val items = listOf("Processing", "Complete", "End")

    ResizeHeightRow(
        Modifier
            .fillMaxWidth(), true, items.size
    ) {
        items.forEachIndexed { index, itemText ->
            Box(
                modifier = Modifier
                    .background(Color.Red)
            ) {
                Text(itemText)
            }
        }
    }
}


@Composable
fun ResizeHeightRow(
    modifier: Modifier,
    resize: Boolean,
    childSize: Int,
    mainContent: @Composable () -> Unit
) {
    //获取屏幕宽度,用于后续计算布局
    val screenWidthDp = LocalConfiguration.current.screenWidthDp.dp
    val screenWidthPx = LocalDensity.current.run {
        screenWidthDp.roundToPx()
    }

    SubcomposeLayout(modifier) { constraints ->
        //调用子组合函数测量子组件大小
        //这里通过SlotsEnum区分主要子组件和依赖组件
        //主要组件用于测量最大尺寸,依赖组件用于生成实际布局
        val mainPlaceables = subcompose(SlotsEnum.Main, mainContent).map {
            //这里测量子Composables的宽高
            it.measure(Constraints())
        }

        //这里找到子Composables的最大宽高
        val maxSize = mainPlaceables.fold(IntSize.Zero) { currentMax, placeable ->
            IntSize(
                width = maxOf(currentMax.width, placeable.width),
                height = maxOf(currentMax.height, placeable.height)
            )
        }

        val resizedPlaceables: List<Placeable> =
            subcompose(SlotsEnum.Dependent, mainContent).map {
                if (resize) {
                    //用测量到的最大宽度重新设置子组件的测量宽高
                    it.measure(
                        Constraints(
                            minWidth = maxSize.width
                        )
                    )
                } else {
                    //子组件测量自己的实际宽高
                    it.measure(Constraints())
                }
            }

        //使用layout()和place()函数将可组合对象放在屏幕上
        //测量完宽高后重新布局
        layout(constraints.maxWidth, constraints.maxHeight) {
            //组件之间的间隔空间
            val space =
                (screenWidthPx - maxSize.width * childSize) / (childSize - 1)
            //集合遍历
            resizedPlaceables.forEachIndexed { index, placeable ->
                val widthStart =
                    resizedPlaceables.take(index).sumOf {
                        //宽度加上需要的间距
                        it.measuredWidth + space
                    }
                //具体摆放位置
                placeable.place(widthStart, 0)
            }
        }
    }
}

//控件id
enum class SlotsEnum {
    Main,
    Dependent
}

UI效果

不知道有没有更好的实现方式,作为初学者在这方面积累还有限,如果有更好的方式欢迎留言评论!

参考内容:

Jetpack Compose博物馆

实体书 Jetpack Compose从入门到实战

Jetpack-Compose-Playground

相关推荐
深海呐11 分钟前
Android AlertDialog圆角背景不生效的问题
android
ljl_jiaLiang12 分钟前
android10 系统定制:增加应用使用数据埋点,应用使用时长统计
android·系统定制
花花鱼13 分钟前
android 删除系统原有的debug.keystore,系统运行的时候,重新生成新的debug.keystore,来完成App的运行。
android
落落落sss1 小时前
sharding-jdbc分库分表
android·java·开发语言·数据库·servlet·oracle
消失的旧时光-19434 小时前
kotlin的密封类
android·开发语言·kotlin
服装学院的IT男5 小时前
【Android 13源码分析】WindowContainer窗口层级-4-Layer树
android
CCTV果冻爽6 小时前
Android 源码集成可卸载 APP
android
码农明明6 小时前
Android源码分析:从源头分析View事件的传递
android·操作系统·源码阅读
秋月霜风7 小时前
mariadb主从配置步骤
android·adb·mariadb
Python私教8 小时前
Python ORM 框架 SQLModel 快速入门教程
android·java·python