本文字数:10501

预计阅读时间:27 分钟

Compose自定义布局的使用

我们知道,在Android View体系下,自定义布局需要继承ViewGroup重写onMeasure、onLayout方法,那么在Compose UI框架中该如何实现自定义布局呢?

今天我们就来学习下Compose UI中自定义布局的具体使用。

实现目标

项目中有一个房源展示页面,用来展示一栋楼的所有房间信息,布局要求如下:

  1. 每个房间的宽高尺寸固定,水平方向需要动态计算可以显示的房间数量,内容水平方向居中;

  2. 房源数量较少,无法充满屏幕时,房源在屏幕中竖直方向居中显示;

  3. 房源数量超过屏幕时,从上向下布局,竖直方向可以滑动查看。

我们以此页面为目标,学习Compose自定义布局的使用。

效果图

效果一:上下、左右居中

效果二:上下滑动

Compose 自定义布局实现方式

在编写代码前,我们先来了解下Compose 中自定义布局的实现方式。

Compose中使用Layout 可组合项来实现自定义布局,在Layout函数中完成子元素的测量和放置。以下是 Layout 可组合项的函数签名:

@Composable inline fun Layout(content: @Composable () -> Unit,modifier: Modifier = Modifier,measurePolicy: MeasurePolicy
) {}

一个自定义布局的Layout代码结构通常如下代码所示:

@Composable
fun MyBasicColumn(modifier: Modifier = Modifier,content: @Composable () -> Unit
) {Layout(modifier = modifier,content = content) { measurables, constraints ->// 1. 使用给定的约束条件constraints测量childrenval placeables = measurables.map { measurable ->measurable.measure(constraints)}// 2. 设置布局的尺寸,放置子元素layout(constraints.maxWidth, constraints.maxHeight) {var yPosition = 0//在父布局中放置childrenplaceables.forEach { placeable ->placeable.placeRelative(x = 0, y = yPosition)yPosition += placeable.height}}}
}

从上面代码可以看到,Compose实现自定义布局,主要有两步:

  1. 测量每个子元素在父布局约束下的大小

  2. 确定布局尺寸,放置子元素

注意:Compose 界面不允许多遍测量。这意味着,布局元素不能为了尝试不同的测量配置而多次测量任何子元素。

主要参数介绍

Layout函数中,有三个主要参数:

1. modifer

由外部传入的修饰符,用来修饰我们自定义的这Layout 组件的一些属性或约束 Constraints;

2. content

自定义布局 Layout 组件中所包含的子元素 children;

3. measurePolicy

mearsurePolicy 参数是 MeasurePolicy 类型,它是一个函数式接口,指定了布局测量和放置项目的方式。我们通常在Layout函数中以尾随 Lambda 的形式提供 MeasurePolicy 作为参数,从而实现所需的 MeasureScope.measure 函数。

fun interface MeasurePolicy {fun MeasureScope.measure(measurables: List<Measurable>,constraints: Constraints): MeasureResult
}

measure函数接受一个 Constraints 对象来告知 Layout 它的尺寸限制。Constraints 是一个简单类,用于限制 Layout 的最大和最小宽度与高度, constraints中提供的maxWidth和maxHeight是计算过modifier中padding之后的值, 所以布局中不需要再考虑padding:

class Constraints {val minWidth: Intval maxWidth: Intval minHeight: Intval maxHeight: Int
}

measure 函数还会接受 List<Measurable> 作为参数,表示的是传入的子元素, Mesurealbe中拥有测量元素尺寸的函数Mesurealbe.measure(constraints: Constraints),使用此函数完成子元素的测量工作,获取子元素的布局尺寸。

在MeasurePolicy的measure函数中,完成测量和放置子元素的过程。

1)测量

遍历measureables, 调用measure(constrains:Constrains)方法进行测量。获取子元素的测量结果Placeable,Placeable包含测 量的宽度和高度

2)放置

调用layout(width: Int,height: Int,alignmentLines: Map<AlignmentLine, Int> = emptyMap(),placementBlock: Placeable.PlacementScope.() -> Unit)方法对子元素进行布局。

width, height指定可组合项的布局尺寸, placementBlock是具体的布局流程。

测量后的Placeable表示为可布局对象。通过placeable.placeRelative(x:Int,y:Int)方法对其进行摆放。x,y表示其距当前组件左上角的偏移量。另外还有一个place(x: Int, y: Int)方法,两个方法的区别是:placeRelative方法支持RTL布局,也就是从右向左的布局,place方法只支持LTR布局。

代码实现

接下来,正式开始编写代码。

我们预先定义一些布局条件:

  1. 自定义布局默认占满屏幕,宽高即为屏幕的宽高;

  2. 子元素尺寸固定,宽高为67dp * 36.dp;

  3. 子元素间的横向和竖向间距固定为5dp;

构建基本布局

在构建基本布局这一部分,我们先完成子元素在父布局中的基本展示,左右居中效果,后续步骤再分别添加上下居中效果和竖直滑动能力。

我们先向布局中添加一些子元素,子元素的宽高尺寸为固定值,如下:

CustomLayout() { //CustomLayout为我们要实现的自定义布局for (i in 1..100) {Box(modifier = Modifier.size(67.dp, 36.dp).background(color = Color(0xFFFF6633),shape = RoundedCornerShape(2.dp)), contentAlignment = Alignment.Center) {Text(text = "10${i}", fontSize = 16.sp, color = Color.White)}}
}
1.  测量子元素的尺寸

在Layout函数中,根据父布局提供的Constraints约束条件,测量子元素,生成placeable,获取子元素的尺寸

@Composable
fun CustomLayout(modifier: Modifier = Modifier,content: @Composable () -> Unit) {Layout(content = content, modifier = modifier) { measurables, constraints ->val placeables = measurables.mapIndexed { index, measurable ->// 测量子元素的尺寸val placeable = measurable.measure(constraints)placeable}
}
2. 计算

接下来,我们需要通过获取到的子元素尺寸,计算每行子元素的总宽度、左右两侧边距、子元素的总高度、每个子元素在父布局中的位置。

由于子元素尺寸、间距固定,我们可以先计算出每行可以容纳的子元素个数,然后根据子元素宽度、元素间距计算得到每行子元素的总宽度;再通过子元素的总数量计算出子元素的总行数,通过子元素总行数、子元素高度,竖直方向元素间距可计算得到子元素内容的总高度。

布局示意图,水平居中即左右两侧边距相等

1)计算每行子元素数量

用变量columns记录每行可以容纳的子元素个数,rowWidth初始为布局的最大宽度,当rowWidth大于等于子元素的宽度childWidth时,说明当前行还可继续容纳子元素,columns加1,rowWidth减去( childWidth + 间距space ),即为剩余的可用宽度,通过while循环计算,直到rowWidth小于childWidth,说明此时宽度已不够放下子元素。

var columns = 0var rowWidth = constraints.maxWidth
val childWidth = placeable.width
val childHeight = placeable.height// 根据父元素、子元素的宽度以及子元素间的间距,计算每行可以显示的子元素数量
while (rowWidth >= childWidth) {rowWidth -= (childWidth + space)columns++
}
2) 计算每行子元素总宽度、左右间距

有了每行子元素的数量后,就可以根据子元素宽度,间距计算出子元素的总宽度,然后再计算出左右两侧的边距,水平方向居中即左右边距相等,为父布局最大宽度减去子元素总宽度后剩余宽度的一半:

//每行子元素占据的总宽度,包括子元素间间距
val lineWidth = columns * childWidth + (columns - 1) * space
//计算左右两侧边距,为最大宽度减去子元素总宽度剩余宽度的一半
edgeStart = (constraints.maxWidth - lineWidth) / 2
3)然后,再通过子元素的总数量、每行的子元素数量,计算出总行数rows,有了总行数后即可计算出子元素的总高度。
//计算总行数
rows = (measurables.size + columns - 1) / columns
// 计算子元素的总高度
contentHeight = rows * childHeight + (rows - 1) * space
4)拿到行数、列数后,就可以计算出每个元素的在父布局中的坐标位置
//两个二维数组,分别存放 * 行 * 列元素的坐标位置
var childX = Array(rows) { IntArray(columns) } //子元素 X 方向的位置
var childY = Array(rows) { IntArray(columns) } //子元素 Y 方向的位置//当前元素是第几行,第几列,index为元素索引
val row = index / columns
val column = index % columns//计算元素位置坐标
childX[row][column] = column * (placeable.width + space) + edgeStart
childY[row][column] = row * (placeable.height + space)
3. 设置布局尺寸,放置子元素

最后,调用layout方法,设置布局尺寸,我们这里使用布局的最大宽度和最大高度,然后遍历测量生成的placeables来放置子元素。

//布局的宽高默认为约束的最大宽高,这里即为屏幕的宽高
val layoutWidth = constraints.maxWidth
val layoutHeight = constraints.maxHeightlayout(layoutWidth, layoutHeight) {placeables.forEachIndexed { index, placeable ->//当前是第几行,第几列val row = index / columnsval column = index % columns//放置元素placeable.placeRelative(x = childX[row][column],y = childY[row][column],)}
}

到这里,基本的布局就完成了,效果如下

实现竖直方向内容居中效果

完成了基本布局后,我们再来实现竖直方向内容居中的效果。和水平居中一样,竖直居中即上下两侧边距相等。

在上面计算过程中,我们获取了子元素内容的总高度,结合父布局的最大高度即可计算出上下侧的边距。

需要注意的是:只有当布局内容的高度小于布局的最大高度时,我们才来设置竖直居中。

//top 上侧的边距, 当子元素的高度超过父容器高度时为0;子元素的高度小于父布局高度时进行计算(父容器高度-子元素总高度)/ 2var edgeTop = 0
if (contentHeight < layoutHeight) {edgeTop = (layoutHeight - contentHeight) / 2
}

有了上侧边距后,在对子元素布局时,y坐标加上侧的间距,即可实现内容上下居中

layout(layoutWidth, layoutHeight) {placeables.forEachIndexed { index, placeable ->//当前是第几行,第几列val row = index / columnsval column = index % columnsplaceable.place(x = childX[row][column],y = childY[row][column] + edgeTop,)}
}

效果如图

这里我们需要注意,当我们给父元素添加height()或fillMaxSize(), fillMaxHeight()尺寸修饰符后,子元素的尺寸测量会出现异常。

CustomLayout(modifier = Modifier.background(Color.Yellow).padding(12.dp).fillMaxSize() //设置布局尺寸,占满所有可用空间,即和屏幕宽高一致
) {for (i in 1..20) {Box(modifier = Modifier.size(67.dp, 36.dp).background(color = Color.Green,shape = RoundedCornerShape(2.dp)), contentAlignment = Alignment.Center) {Text(text = "10${i}", fontSize = 16.sp, color = Color.Black)}}
}

添加fillMaxSize修饰符后的效果是这样的,单个子元素占满了整个布局。

出现上述问题的原因是:父布局传入的Constraints约束条件发生了变化。

打印日志,可以看到,在添加尺寸修饰符后,约束条件为:

//添加尺寸条件后的约束
Constraints(minWidth = 1146, maxWidth = 1146, minHeight = 1620, maxHeight = 1620)

而无尺寸修饰符时的约束为:

//没有尺寸条件时的约束
Constraints(minWidth = 0, maxWidth = 1146, minHeight = 0, maxHeight = 2381)

对比发现,设置尺寸修饰后,约束条件发生了变化,minWidth和maxWidth、minHeight和maxHeight分别为同一个值。

子元素在测量时,由于约束条件中宽、高最小值,最大值为固定值,限定了子元素的宽高为约束的宽高,测量后的尺寸不再是我们期望的值。

此时,我们需要重新创建子元素的约束条件:

//设置约束最小宽度和最小高度为0
val childConstraints = Constraints(0, constraints.maxWidth, 0, constraints.maxWidth)

使用重新定义的约束条件进行测量,子元素的测量尺寸恢复正常。

// 使用新的约束条件测量子元素的尺寸
val placeable = measurable.measure(childConstraints)

添加竖直方向滚动能力

最后,给布局添加竖直方向的滚动能力,需要明确的是:竖直方向可滑动的前提条件是布局内容的高度大于布局的最大高度。

Compose给我们提供的verticalScrollhorizontalScroll 修饰符提供了一种最简单的方法,可让用户在元素内容边界大于最大尺寸约束时滚动元素。

我们使用verticalScroll修饰符来给布局添加竖直滚动能力。

CustomLayout(modifier = Modifier.background(Color.Gray).fillMaxSize().padding(12.dp).verticalScroll(rememberScrollState()) //添加滚动修饰符
) {for (i in 1..100) { // 增加子元素的数量,使内容高度超过布局的高度Box(modifier = Modifier.size(67.dp, 36.dp).background(color = Color.Green,shape = RoundedCornerShape(2.dp)), contentAlignment = Alignment.Center) {Text(text = "10${i}", fontSize = 16.sp, color = Color.Black)}}
}

然而,添加verticalScroll后,屏幕中没有展示出内容。

给父布局添加背景色后,可以看到屏幕中显示的仍然是布局的一部分。

我们来看下此时的约束条件,最大高度maxHeight为Infinity(无限大),我们上边距的计算方式为(maxHeight - contentHeight)/2,此时计算得到的上边距近似为Infinity/2(无限大),且我们在layout布局方法中传入的高度参数也为maxHeight,即我们给布局设置的布局内容高度为Infinity(无限大),此时屏幕中显示的是布局的上边距部分。

//最大高度约束为Infinity(无限大)
Constraints(minWidth = 1146, maxWidth = 1146, minHeight = 2381, maxHeight = Infinity)val layoutWidth = constraints.maxWidth
val layoutHeight = constraints.maxHeight//上边距为无限大的一半,即无限大
if (contentHeight < layoutHeight) {edgeTop = (layoutHeight - contentHeight) / 2
}//layout布局高度为无限大
layout(layoutWidth, layoutHeight);

重新确定布局高度计算方式,布局高度取子元素总高度与约束最小高度的最大值

val layoutHeight = max(contentHeight, constraints.minHeight)

最终效果如图

到这里,我们已经完成了一个简单自定义布局的实现。当然,后续我们可以继续来优化布局方式,如:通过定义参数来控制子元素的对齐方式,子元素的水平、竖直方向间距等。

小结

Compose使用Layout可组合函数实现自定义布局,整体流程和View中自定义布局大致相同,都需要两个主要步骤:

  1. 测量子元素在父布局约束下的大小;

  2. 在父布局中放置子元素;

需要注意的是:

  • Compose只允许测量一次,对某个元素进行多次测量,会抛出异常;

  • 父布局的约束条件会随修饰符的不同而变化,测量子元素时根据需要创建合适的约束条件;

  • layout(小写开头)方法设置布局尺寸时要选用正确的宽度和高度;

完整代码如下:

@Composable
fun CustomScreen() {Surface(color = MaterialTheme.colors.background) {CustomLayout(modifier = Modifier.background(Color.Gray).fillMaxSize().padding(12.dp).verticalScroll(rememberScrollState())) {for (i in 1..100) {Box(modifier = Modifier.size(67.dp, 36.dp).background(color = Color(0xFFFF6633),shape = RoundedCornerShape(2.dp)), contentAlignment = Alignment.Center) {Text(text = "10${i}", fontSize = 16.sp, color = Color.White)}}}}
}@Composable
fun CustomLayout(modifier: Modifier = Modifier, content: @Composable () -> Unit) {Layout(content = content, modifier = modifier) { measurables, constraints ->//每行的高度是固定的//每个块的宽、高是固定的//内容总高度小于容器高度时居中,大于总高度时,可以向下滑动//从上向下布局println("约束条件:$constraints")//总行数var rows = 0//总列数-每行最多显示的子元素数量var columns = 0//start方向的paddingvar edgeStart = 0//top 方向的间距, 当子元素的高度超过父容器高度时为0;子元素的高度小于父容器高度时进行计算(父容器高度-子元素总高度)/ 2var edgeTop = 0// 子元素的总高度,包括子元素自身高度和子元素之间的间距var contentHeight = 0var isCalculated = falseval space = 5.dp.roundToPx() // 水平和竖直方向间距固定为5dpvar childX = Array(rows) { IntArray(columns) } //子元素 X 方向的位置var childY = Array(rows) { IntArray(columns) } //子元素 Y 方向的位置//重新创建子控件的约束//当父布局设置高度时,默认约束最小高度和最大高度相同,为设置的高度//直接使用默认约束条件会导致测量出来的子控件高度与父控件一样val childConstraints = Constraints(0, constraints.maxWidth, 0, constraints.maxWidth)val placeables = measurables.mapIndexed { index, measurable ->// 测量子元素的尺寸val placeable = measurable.measure(childConstraints)if (!isCalculated) {isCalculated = truevar rowWidth = constraints.maxWidthval childWidth = placeable.widthval childHeight = placeable.height// 根据父元素、子元素的宽度以及子元素间的间距,计算每行可以显示的子元素数量while (rowWidth > childWidth) {rowWidth -= (childWidth + space)columns++}//一行子元素占据的总宽度,包括子元素间间距val lineWidth = columns * childWidth + (columns - 1) * space//计算左右两侧的间距edgeStart =(constraints.maxWidth - lineWidth) / 2rows = (measurables.size + columns - 1) / columns// 计算子元素的总高度contentHeight = rows * childHeight + (rows - 1) * space//有了行数,列数后重新构造二维数组childX = Array(rows) { IntArray(columns) { 0 } }childY = Array(rows) { IntArray(columns) { 0 } }}//当前是第几行,第几列val row = index / columnsval column = index % columnschildX[row][column] = column * (placeable.width + space) + edgeStartchildY[row][column] = row * (placeable.height + space)placeable}val layoutWidth = constraints.maxWidthvar layoutHeight = max(contentHeight, constraints.minHeight)
//         layoutHeight = constraints.maxHeightif (contentHeight < layoutHeight) {edgeTop = (layoutHeight - contentHeight) / 2}layout(layoutWidth, layoutHeight) {placeables.forEachIndexed { index, placeable ->//当前是第几行,第几列val row = index / columnsval column = index % columnsval x = childX[row][column]val y = childY[row][column] + edgeTopplaceable.place(x,y,)}}}
}

Compose自定义布局的使用相关推荐

  1. Jetpack Compose 自定义绘制——高仿Keep周运动数据页面

    废话之前先上图吧,如果不是有人告诉,你可以一眼看出哪个是真哪个是假吗? 仿制整个页面(仅仅页面)大概花了我两个小时,不过仅仅是静态的.不可点击的.图有形似而无功能. 自定义绘制 Jetpack Com ...

  2. Compose基础布局

    Compose基础布局 常规布局 ConstraintLayout 常规布局 Compose 可以有效地处理嵌套布局,堪称设计复杂界面的绝佳工具.这与 Android Views 相比是一个进步:在 ...

  3. ViewGroup1——自定义布局

    平时开发时,系统提供的几个布局基本就能满足我们的需求了.如果系统提供的布局无法满足需求,我们可以扩展ViewGroup类来实现自定义布局控件.先看下ViewGroup的继承图 由上图可知,ViewGr ...

  4. UICollectionView自定义布局(二)

    这是UICollectionView自定义布局的第二篇,实现类似UltravisualApp的视差效果,同样这篇文章的教程来自Ray家的Swift Expanding Cells in iOS Col ...

  5. iOS开发学无止境 - UICollectionView自定义布局之风火轮[译]

    现在有许多极具创造力的网站,几周前我碰巧浏览到一个名为Form Follows Function的网站,上面有各种交互动画.其中最吸引我的是网站上的导航转轮,转轮由各种交互体验海报组成. 原文:UIC ...

  6. 从自定义TagLayout看自定义布局的一般步骤[手动加精]

    从自定义TagLayout看自定义布局的一般步骤[手动加精] 我们常用的布局有LinearLayout,FrameLayout,RelativeLayout,大多数情况下都能满足我们的需求,但是也有很 ...

  7. Swift - 使用网格(UICollectionView)的自定义布局实现复杂页面

    网格UICollectionView除了使用流布局,还可以使用自定义布局.实现自定义布局需要继承UICollectionViewLayout,同时还要重载下面的三个方法: 1 2 3 4 5 6 7 ...

  8. 【Android 性能优化】布局渲染优化 ( 过渡绘制 | 背景设置产生的过度绘制 | Android 系统的渲染优化 | 自定义布局渲染优化 )

    文章目录 一. 背景设置产生的过度绘制 二. Android 系统的渲染优化 1. 透明组件数据传递 2. GPU 存储机制 3. Android 7.0 之后的优化机制 三. 自定义布局渲染优化 一 ...

  9. android 前台服务自定义布局不显示_Android自定义LinearLayout布局显示不完整的解决方法...

    发现问题 原需求,在一个伸缩列表中,自定义LinearLayout继承LinearLayout动态添加布局. 然而实现的时候:一共遍历了30条数据,却只显示了一条 断点查看代码:遍历addView() ...

最新文章

  1. Linux五种IO模型性能分析
  2. 一个参数一个Excel表,让你玩转Pandas中read_excel()表格读取!
  3. 一款强大的Kubernetes API流量查看神器
  4. 使用 ramda 解析 .yarnrc/.npmrc 配置文件的例子
  5. db2 dec函数oracle,DB2常用函数和Oracle的比较
  6. 如何防止网页被Demo
  7. Docker中常用的命令
  8. lua协程的使用列子分析
  9. 苹果电脑怎么看html5,苹果Mac系统看HTML5视频教程介绍
  10. Redis Nosql数据库
  11. 利用低代码从0到1开发一款小程序
  12. Arcmap出了问题--显示“ArcGIS Initializing Application”
  13. Tecplot 360 EX 2020 R1中文版
  14. CMOS模拟集成电路笔记(第一部分)
  15. ZOJ3332 Strange Country II java
  16. mysql dump hbase_导入mysqldump表结构
  17. js判断手机号码归属地查询接口--精确到地市
  18. 用MLX90614红外温度传感器制作非接触式红外测温仪
  19. wuauclt1.exe mshta.exe 病毒清理
  20. UE4冒泡排序蓝图、随机整数数组生成蓝图

热门文章

  1. 向导插件bootstrap wizard入门使用
  2. 【数据结构】基于二叉链表的二叉树结点个数的统计
  3. 还在借口美工差不会logo?一个方法教你快速设计logo,初学者必看
  4. 计算机网络中各层有哪些协议?这些协议的作用?
  5. 在KVM中使用ISO镜像安装虚拟机(命令行)
  6. 功能比较全的串口助手
  7. 人工智能各领域跨界能手——Transformer
  8. 双非本科毕业,七面阿里,终获27k*14offer,还原我的大厂面经
  9. mysql多表联查的几种方法_多表联查的几种方式
  10. 碰壁14次老前辈呕心沥血总结的软件测试面试题 面试成功率高达70% !!!入职必看!!!