这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战
概述
本篇笔记继续学习Compose
中布局的相关知识。点击连接可以直接查阅官方文档。
通过本篇学习笔记,可以学习到如下知识:
- 自适应布局
- 自定义布局 — 使用布局修饰符
- 自定义布局 — 使用Layout自定义排列子项
- 自定义布局 — 实现一个类似于
Flow
的自定义布局
自适应布局
由于现在Android设备的种类比较多,屏幕的尺寸和类型也比较多,在开发的时候应该考虑不同的屏幕方向和设备尺寸,使用约束条件可以了解父项的约束条件并相应地设计布局,通过使用BoxWithConstraints
可以在内容作用域内找到测量条件,然后使用这些测量条件设计不同的布局。如下所示:
@Composable
private fun LayoutWithBoxWithConstrains(){
Box(modifier = Modifier
.fillMaxSize()
.background(color = Color.LightGray)) {
BoxWithConstraints() {
if(maxHeight > maxWidth){
//纵向比较大
Column(modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "纵向排列")
Spacer(modifier = Modifier.height(10.dp))
Text(text = "纵向排列")
Spacer(modifier = Modifier.height(10.dp))
Text(text = "纵向排列")
}
}else{
//横向比较大
Row(modifier = Modifier.fillMaxSize()) {
//左边标题,右边内同
Column(modifier = Modifier
.weight(1f)
.fillMaxHeight(),
verticalArrangement = Arrangement.Center
) {
Text(text = "左边显示标题列表")
}
Column(modifier = Modifier
.weight(3f)
.background(color = Color.Green)
.fillMaxHeight(),
verticalArrangement = Arrangement.Center
) {
Text(text = "右边显示内容区域")
}
}
}
}
}
}
复制代码
在上面的代码中,我们判断了当前可用的最大宽度和最大高度,如果宽度比较大,我们认为是横屏,此时屏幕分成左右两边,如果高度比较大,此时认为是竖屏,则只显示一列内容,运行后如下所示(注意打开自动旋转):
自定义布局
使用布局修饰符
通过使用layout
修饰符来修改元素的测量和布局方式,这是一个lambda,它包含两个参数,一个是可以测量的元素,这是参数measurable
代表的内容,另一个是该组合项的约束条件,这是参数constraints
所代表的内容。
下面的代码演示了Text
中paddingFromBaseline
的效果:
在此之前,我们需要首先了解我们要实现的具体效果:
从上面的效果图可以看出,当我们设置了基于文本基线的距离的时候,我们的布局会发生相应的变化:因为文本基线和padding
是不一样的,padding
只需要给当前元素的高度加上我们设置的padding
值即可,但是文字基线本身是存在于元素的高度之中的,所以当我们设置了基于文字基线的高度以后,我们实际的元素高度变为了:
设置的距离 – 文字基线的高度 + 文字的高度(原元素的高度)
fun Modifier.firstBaselineToTop(distance: Dp) = layout { measurable, constraints ->
//通过此方法使用给定的约束测量布局,返回具有新大小的可放置布局
val placeable = measurable.measure(constraints)
//检查是否包含文字基线的对齐方式,Text中会提供此对齐方式
check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
//如果已经提供了基于文字基线的对齐方式,则返回文字基线的位置
val firstBaseline = placeable[FirstBaseline]
//使用我们传递进来的高度减去文字基线的高度得到当前元素的顶部距离顶部的高度
val placeableY = distance.roundToPx() - firstBaseline
//设置当前元素的新的高度
val height = placeableY + placeable.height
//使用原有的宽度和新的高度进行布局
layout(placeable.width, height) {
placeable.placeRelative(0, placeableY)
}
}
复制代码
当我们创建完新的自定义布局之后,我们就可以在Text
中使用此布局,如下所示:
@Composable
private fun MyLayoutFirstBaselineTopTop() {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Row(modifier = Modifier.fillMaxWidth()) {
Text(
text = "这是左边的文字",
textAlign = TextAlign.End,
modifier = Modifier
.weight(1f)
.background(color = Color.Green)
.firstBaselineToTop(32.dp)
)
Text(
text = "这是右边的文字",
modifier = Modifier
.weight(1f)
.background(color = Color.Cyan)
.paddingFromBaseline(top = 32.dp)
)
}
}
}
复制代码
运行效果如下:
从上面的效果可以看出,我们自定义的布局和使用paddingFromBaseline
的效果是大致一样的。
之所以说是大致一样,是因为我们设置的基于文字基线的高度很可能小于文字本身的基线高度,这样使用上面的公式 设置的距离 - 文字基线的高度 + 文字的高度(原元素的高度)
就会获得比原来的文字高度更小的值,比如将上面的32.dp
均设置为5.dp
就会得到下面的效果:
可以看到:左边的文字上面已经不能显示在背景里面了,右边应该是有对这种情况做特殊的处理,所以仍然保持原始的高度。我们也可以在这里做一些判断,如果算出来的高度不符合要求则可以重新设置之前的高度。或者计算的placeableY
为负数则强制设置其为0.
创建自定义布局
如果需要测量多个子项,上面的layout
元素则无法做到,此时可以使用Layout
可组合项,使用这个可组合项允许自动测量和布置子项,Column
和Row
等较高级别的布局都是用Layout
可组合项构建而成的。在之前的View
系统中,创建这种类型的自定义布局必须扩展ViewGroup
并实现测量和布局函数。
下面使用这种方式构建一个自定义布局,这种布局当子项扩充到界面最右边的时候会自动跳转到下一行:
@Composable
fun WrapLayoutItem(
modifier: Modifier,
content: @Composable () -> Unit
) {
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
//对每一个子项遍历测量
val placeables = measurables.map { measurable ->
//根据当前的约束条件对每一个子项进项测量
measurable.measure(constraints)
}
// Set the size of the layout as big as it can
layout(constraints.maxWidth, constraints.maxHeight) {
//当前子项在y轴的坐标
var yPosition = 0
//当前子项在x轴的坐标
var xPosition = 0
//每一行的最大子项的高度
var maxHeightRow = 0
//换行后的总高度
var maxHeightAll = 0
// Place children in the parent layout
placeables.forEach { placeable ->
//首先判断当前是否需要换行
if (xPosition + placeable.width > constraints.maxWidth) {
//需要换行
maxHeightAll += maxHeightRow
maxHeightRow = 0
xPosition = 0
yPosition = maxHeightAll
}
//如果当前子项的高度大于这一行中最大子项的高度,则将最大子项的高度设置为这个子项的高度
if (maxHeightRow < placeable.height) {
maxHeightRow = placeable.height
}
// Position item on the screen
placeable.placeRelative(x = xPosition, y = yPosition)
// Record the y co-ord placed up to
xPosition += placeable.width
//yPosition += placeable.height
}
}
}
}
复制代码
在上面的代码中,我们首先会对每一个子项进行测量,测量完成后我们会开始进行布局,循环遍历每一个子项排列它的位置。我们希望子项能够在填满一行之后自动扩充到下一行中,然后我们在代码中使用这个布局:
@Composable
private fun LayoutWrapItem() {
WrapLayoutItem(
modifier =
Modifier
.padding(top = 10.dp)
.fillMaxWidth()
) {
Text(text = "测试自定义布局1", modifier = Modifier
.size(80.dp)
.background(color = Color.Green))
Text(text = "测试自定义布局2", modifier = Modifier
.size(150.dp)
.background(color = Color.Blue))
Text(text = "测试自定义布局3", modifier = Modifier
.size(130.dp)
.background(color = Color.Gray))
Text(
text = "测试自定义布局4",
modifier = Modifier
.size(280.dp)
.background(color = Color.Black)
)
}
}
复制代码
在上面的代码中,我们给自定义布局的每一个子项都设置了固定的大小,并且大小不同,最后运行结果如下:
可以看到,最终的结果并不是我们想的那样,高度是按照我们设置尺寸,但是宽度则很明显是按照其父项的宽度来进行设置的。出现这个问题是因为测量的时候我们直接传递了constraints
,这是父项的尺寸约束,而我们直接使用父项的尺寸约束去测量子项,导致子项的宽度使用了父项尺寸。
知道了问题所在,我们就可以针对性地设置子项的布局约束,下面的代码中我们测量子项的时候强制设置子项的布局约束,如下所示:
@Composable
fun WrapLayoutItem(
modifier: Modifier,
content: @Composable () -> Unit
) {
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
//对每一个子项遍历测量
val placeables = measurables.map { measurable ->
//根据当前的约束条件对每一个子项进项测量
measurable.measure(constraints = constraints)
}
// Set the size of the layout as big as it can
layout(constraints.maxWidth, constraints.maxHeight) {
//当前子项在y轴的坐标
var yPosition = 0
//当前子项在x轴的坐标
var xPosition = 0
//每一行的最大子项的高度
var maxHeightRow = 0
//换行后的总高度
var maxHeightAll = 0
// Place children in the parent layout
placeables.forEach { placeable ->
//首先判断当前是否需要换行
if (xPosition + placeable.width > constraints.maxWidth) {
//需要换行
maxHeightAll += maxHeightRow
maxHeightRow = 0
xPosition = 0
yPosition = maxHeightAll
}
//如果当前子项的高度大于这一行中最大子项的高度,则将最大子项的高度设置为这个子项的高度
if (maxHeightRow < placeable.height) {
maxHeightRow = placeable.height
}
// Position item on the screen
placeable.placeRelative(x = xPosition, y = yPosition)
// Record the y co-ord placed up to
xPosition += placeable.width
//yPosition += placeable.height
}
}
}
}
复制代码
重新运行上面的代码,如下所示:
可以看到,已经能够达到我们想要的效果。