4. ConstraintLayout 约束布局
众所周知,Android View 体系中官方最推荐的布局是约束布局 —— ConstraintLayout,以致于在默认新建布局时就给你初始化成 ConstraintLayout。当然,ConstraintLayout 确实可以解决 View 体系中多层嵌套的问题,那么在 Compose 中也可以使用吗?
答案是肯定的。Compose 中也可以使用 ConstraintLayout,是使用 Row、Column、Box 布局的另一种解决方案。在实现更大的布局以及有许多复杂对齐要求以及布局嵌套过深的场景下,ConstraintLayout 用起来更加顺手。使用前,得引入 Compose 中的 ConstraintLayout 依赖库:
// build.gradle implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-alpha07"
在 Compose 中使用 ConstraintLayout 有几点需要注意的:
- ConstraintLayout 中的子元素是通过 createRefs() 或 createRef() 方法初始化声明的,并且每个子元素都会关联一个ConstraintLayout 中的 Composable 组件;
- 子元素之间的约束关系是通过 Modifier.constrainAs() 的 Lambda 表达式来实现的,具体的可以看下面的 code 9;
- 约束关系可以使用 linkTo 或其他约束方法实现;
- parent 是一个默认存在的引用,代表 ConstraintLayout 父布局本身,也是用于子元素的约束关联。
下面是一个简单的例子:
// code 9 @Composable fun ConstraintLayoutDemo() { ConstraintLayout { // 初始化声明两个元素,如果只声明一个,则可用 createRef() 方法 // 这里声明的类似于 View 的 id val (button, text) = createRefs() Button( onClick = {}, // constrainAs() 将 Composable 组件与初始化的引用关联起来 // 关联之后就可以在其他组件中使用并添加约束条件了 modifier = Modifier.constrainAs(button) { // 熟悉 ConstraintLayout 约束写法的一眼就懂 // parent 引用可以直接用,跟 View 体系一样 top.linkTo(parent.top, margin = 20.dp) start.linkTo(parent.start, margin = 10.dp) } ){ Text("Button") } Text(text = "Text", Modifier.constrainAs(text) { top.linkTo(button.bottom, margin = 16.dp) start.linkTo(button.start) centerHorizontallyTo(parent) // 摆放在 ConstraintLayout 水平中间 }) } }
细心的同学可能会有疑问,为啥下面的 Text 设置了父布局水平居中,但是好像是在 Button 宽度的中间呢?这是因为父布局的 ConstraintLayout 的大小默认是尽量小的容纳它的子元素,这跟 wrap_content 一样。可以将开发者选项中的显示布局边界打开看看:
这样就直观多了。要把 Text 放在整个屏幕的水平居中的位置,需要在 ConstraintLayout 中设置 Modifier.fillMaxWidth() 即可。
当然,Compose 版本的 ConstraintLayout 也支持设置使用 guideline、barrier、chain 等。
4.1 Barrier 的用法
先来看看 Barrier 的用法,就是字面意思,给一些子元素设置栅栏,将栅栏两侧的子元素分隔开的作用:
// code 10 @Composable fun ConstraintLayoutDemo1() { ConstraintLayout { val (button1, button2, text) = createRefs() Button( onClick = {}, modifier = Modifier.constrainAs(button1) { top.linkTo(parent.top, margin = 20.dp) start.linkTo(parent.start, margin = 10.dp) } ){ Text("Button1") } Text(text = "Text文本", Modifier.constrainAs(text) { top.linkTo(button1.bottom) // 将 Text 的中心摆放在 button1 右边界的位置 centerAround(button1.end) }) // 设置一个 button1 和 text 右边的一个栅栏,将两者放在栅栏的左侧 val barrier = createEndBarrier(button1, text) Button( onClick = {}, modifier = Modifier.constrainAs(button2) { top.linkTo(parent.top, margin = 20.dp) // 将 button2 放在栅栏的右侧 start.linkTo(barrier) } ) { Text(text = "button2") } } }
创建栅栏的函数不仅有 createEndBarrier() 方法,类似用法的总结起来有:
- createTopBarrier()、createBottomBarrier() : 创建分隔上下组件的栅栏;
- createStartBarrier()、createEndBarrier() : 创建分隔左右组件的栅栏;
- createAbsoluteLeftBarrier()、createAbsoluteRightBarrier() : 创建分隔左右组件的栅栏,满足国际化的需求。
最后两个是用于国际化适配,因为有些语言是从右到左排列的,如阿拉伯语,所以如果要严格按照左右来区分的话,使用带 Absolute 的方法,这个跟 marginStart 和 marginLeft 概念差不多。此外,创建 Barrier 并设置组件在 Barrier 的相对位置时,需要满足客观逻辑的。不能创建一个分隔左右组件的栅栏,但是我又设置 top.linkTo(barrier) 或 bottom.linkTo(barrier)。这在客观逻辑上就不成立,当然代码也会报错。
4.2 Guideline 的用法
Compose 版本中的 Guideline 用法大同小异,还是先从例子说起:
// code 11 @Composable fun LargeConstraintLayout() { ConstraintLayout(Modifier.fillMaxHeight()) { val text = createRef() val guideline1 = createGuidelineFromStart(fraction = 0.5f) Text(text = "This text is very long", modifier = Modifier.constrainAs(text) { linkTo(start = guideline1, end = parent.end) } ) val text1 = createRef() val guideline2 = createGuidelineFromTop(fraction = 0.333f) Text(text = "我距离屏幕上方约三分之一处~", modifier = Modifier.constrainAs(text1) { top.linkTo(guideline2) } ) } }
guideline1 设置的是在父布局水平位置 50% 的地方,这里由于 ConstraintLayout 默认尺寸是 wrap_content,所以父布局的宽度会设置为 text 的两倍的宽度,这样就满足了 text 起始位置在父布局的中间,根据图中的布局分界线也可以看出。而 guideline2 是在竖直方向上距离屏幕高度三分之一的位置,需要把父布局的高度设置为屏幕高度才可以实现。
上面的例子只列举了 guideline 根据百分比来设置它的位置,其实也可以根据偏移量来设置。用法总结起来如下列所示:
- createGuidelineFromStart(offset: Dp):根据左侧距离父布局偏移量来设置 guideline 位置
- createGuidelineFromStart(fraction: Float):根据左侧距离父布局的百分比来设置 guideline 位置
- createGuidelineFromAbsoluteLeft(offset: Dp):国际化才使用
- createGuidelineFromAbsoluteLeft(fraction: Float)
- createGuidelineFromEnd(offset: Dp)
- createGuidelineFromEnd(fraction: Float)
- createGuidelineFromAbsoluteRight(offset: Dp)
- createGuidelineFromAbsoluteRight(fraction: Float)
- createGuidelineFromTop(offset: Dp)
- createGuidelineFromTop(fraction: Float)
- createGuidelineFromBottom(offset: Dp)
- createGuidelineFromBottom(fraction: Float)
看着挺多,其实就是上下左右加上国际化的情况。
ConstraintLayout 还有一个特性,就是当它的子元素过大时,ConstraintLayout 默认是可以允许子元素超出屏幕范围的,以上面的例子继续说,当横向的 Text 内容很多时,就会出现 Text 部分内容超出屏幕。。。。上代码:
// code 12 @Composable fun LargeConstraintLayout() { ConstraintLayout(Modifier.fillMaxHeight()) { val text = createRef() val guideline1 = createGuidelineFromStart(fraction = 0.5f) // very 单词有 10 个 Text(text = "This text is very very very very very very very very very very long", modifier = Modifier.constrainAs(text) { linkTo(start = guideline1, end = parent.end) } ) } }
注意看,Text 文本有 10 个 very,但是只展示出来 8个,而且明显 Text 左边界不是位于屏幕中间位置,所以在默认情况下,ConstraintLayout 允许子元素超出屏幕。怎么做才能达到我们想要的效果?在这里需要设置一下 Text 的 width 宽度的属性为 Dimension.preferredWrapContent。
// code 13 Text(text = "This text is very very very very very very very very very very long", modifier = Modifier.constrainAs(text) { linkTo(start = guideline1, end = parent.end) width = Dimension.preferredWrapContent } )
OK, 这个 Dimension 的属性一共有五种:
- preferredWrapContent:布局大小是根据内容所设置,并受布局约束的影响。这个例子中对 Text 右边界做了限制,所以使用这个属性可以控制 Text 右边界只能到达父布局右边界,不能超出屏幕;
- wrapContent:Dimension 的默认值,即布局大小只根据内容所设置,不受约束;
- fillToConstraints:布局大小将展开填充由布局约束所限制的空间。也就是说,这个属性是先看看布局约束所限制的空间有多大,然后再将该子元素填充到这个有约束的空间中;
- preferredValue:布局大小是一个固定值,并受布局约束的影响;
- value:布局大小是一个固定值,不受约束。
此外,Dimension 还可组合设置布局大小,例如:width = Dimension.preferredWrapContent.atLeast(100.dp)
可设置最小布局大小,同样还有 atMost()
可设置最大布局大小等等。
4.3 Chain 的用法
Chain 链,与 xml 中的用法一样,就是将一系列子元素按顺序打包成一行或一列。官方将这个 api 标记为可以改进的状态,可能后续会发生变化。api 只有两个,创建横向和纵向的链:
- createHorizontalChain()
- createVerticalChain()
第一个参数是需要打包在一起的所有子元素的id,第二个参数是链的类型,目前有三种类型:
- Spread:所有子元素平均分布在父布局空间中,是默认类型;
- SpreadInside:第一个和最后一个分布在链条的两端,其余子元素平均分布剩下的空间;
- Packed:所有子元素打包在一起,并放在链条的中间。
代码及效果如下:
// code 14 @Composable fun ConstraintLayoutChainDemo() { ConstraintLayout(modifier = Modifier.fillMaxSize()) { val (box1, box2, box3) = createRefs() createHorizontalChain(box1,box2,box3, chainStyle = ChainStyle.Spread) Box(modifier = Modifier.size(100.dp).background(Color.Red).constrainAs(box1){}) Box(modifier = Modifier.size(100.dp).background(Color.Green).constrainAs(box2){}) Box(modifier = Modifier.size(100.dp).background(Color.Blue).constrainAs(box3){}) } }
chainStyle 设置为 ChainStyle.Spread 的效果:
chainStyle 设置为 ChainStyle.SpreadInside的效果:
chainStyle 设置为 ChainStyle.SpreadInside的效果:
4.4 ConstraintSet 实现动态适配
上面谈论的都是静态设置各种约束布局的情况,没有考虑到横竖屏切换可能导致的布局适配问题。其实 ConstraintLayout 可以传入一个 ConstraintSet 类型的参数,根据这个参数可以设置不同的约束条件,可以进行灵活设置。
// code 15 @Composable fun DecoupledConstraintLayout() { BoxWithConstraints() { val constraints = if (maxWidth < maxHeight) { // 竖屏 decoupledConstraints(false) } else { // 横屏 decoupledConstraints(true) } ConstraintLayout(constraints) { Button( onClick = { /*TODO*/ }, // layoutId 必须与 ConstraintSet 中的一致 Modifier.layoutId("button") ) { Text(text = "Button") } Text( text = "Text", Modifier.layoutId("text") ) } } } private fun decoupledConstraints(isPad: Boolean): ConstraintSet { return ConstraintSet { val button = createRefFor("button") val text = createRefFor("text") if (isPad) { // 横屏模式 constrain(button) { top.linkTo(parent.top, 15.dp) start.linkTo(parent.start, 30.dp) } constrain(text) { top.linkTo(parent.top, 15.dp) start.linkTo(button.end, 20.dp) } } else { // 竖屏模式 constrain(button) { top.linkTo(parent.top, 30.dp) start.linkTo(parent.start, 15.dp) } constrain(text) { top.linkTo(button.bottom, 20.dp) start.linkTo(parent.start, 15.dp) } } } }
这里横竖屏的布局有所不同,就是通过设置不同的 ConstraintSet 来实现的,如果布局元素很多,可以分为两个 ConstraintSet 对象来分别设置。需要注意的是,ConstraintLayout 中子元素的 layoutId 是通过 Modifier 设置的,需要与 ConstraintSet 的 createRefFor 的参数保持一致。下面是横竖屏的显示效果:
竖屏:
横屏:
第二篇 Compose 学习笔记终于完成,Compose 的布局你学会了么?欢迎留言交流~也可关注 公众号:修之竹
参考文献
- developer.android.google.cn/codelabs/je…
- developer.android.google.cn/reference/k…
- compose.net.cn/design/them…
- compose.net.cn/elements/su…
- developer.android.google.cn/reference/k…
- 乐翁龙. 《Jetpack Compose - ConstraintLayout》blog.csdn.net/u010976213/…
ps. 赠人玫瑰,手留余香。欢迎转发分享加关注,你的认可是我继续创作的精神源泉。