Compose 布局。Compose 布局的目标:1)实现高性能;2)让开发者能够轻松编写自定义布局;3)在 Compose 中,通过避免多次测量布局子级可实现高性能。如果需要进行多次测量,Compose 具有一个特殊系统,即固有特性测量。
使用 Column 可将多个像垂直地放置在屏幕上;
使用 Row 可将多个项水平地放置在屏幕上;
使用 Box 可将一个元素放在另一个元素上。类似于 FrameLayout 布局。
修饰符的作用类似于xml 布局中的布局参数。借助修饰符,可以修饰或扩充可组合项。我们可以使用修饰符来执行以下操作:
更改可组合项的大小、布局、行为和外观;
添加信息,如无障碍标签;
处理用户输入;
添加高级互动,如使元素可点击、可滚动、可拖动或可缩放。
示例:通过标准布局组件与修饰符实现如下布局效果
1. 新建一个 PhotoGrapherCard.kt 文件,进行布局
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
/**
* @Author HL
* @Date 2023/12/26 20:21
* @Version 1.0
*/
@Composable
fun PhotographerCard(modifier: Modifier = Modifier){
// TODO 水平放置,从左到右
Row(
modifier = Modifier
.clip(RoundedCornerShape(4.dp)) // 设置圆角
.background(color = MaterialTheme.colors.surface) // surface 是白色
.clickable(onClick = {})
.padding(all = 16.dp) // 设置点击时,水波纹效果
) {
Surface (
modifier = Modifier.size(50.dp),//设置图片大小
shape = CircleShape, // 设置圆形形状
// onSurface 是黑色
color = MaterialTheme.colors.onSurface.copy(alpha = 0.2f) // 设置颜色。copy()把这个颜色复杂过来,然后设置一个透明度
) {
Image(
painter = painterResource(id = R.drawable.beauty), // 设置图片加载位置
contentDescription = null
)
}
// TODO 垂直放置,从上到下
Column (
modifier = Modifier
.padding(start = 8.dp) // 左边设置填充 8dp
.align(Alignment.CenterVertically) // 让文字垂直居中
) {
Text(
text = "Alfred Sisley",
fontWeight = FontWeight.Bold // 设置字体,bold: 粗体
)
//LocalContentAlpha provides ContentAlpha.medium == LocalContentAlpha.provides(ContentAlpha.medium)
// provides 是一个被 infix 修饰的函数。可以省略链式调用的“." 以及一对圆括号"()"
// 隐式传参
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text(
text = "3 minutes ago",
style = MaterialTheme.typography.body2 // 设置字体
)
}
}
}
}
2. 在 MainActivity.kt 里调用 PhotoGrapherCard
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
JetpackComposeLayoutsTheme {
PhotographerCard()
}
}
}
}
由于每个函数都会对上一个函数返回的 Modifier 进行更改,因此顺序会影响最终结果。
Material 组件大量使用槽位 API,这是 Compose 引入的一种模式,它在可组合项之上带来一层自定义设置。这种方法使组件变得更灵活,因为它们接受 可以自行配置的子元素,而不必公开子元素的每个配置参数。槽位会在界面中留出空白区域,让开发者按照自己的意愿来填充。
Scaffold 可以让我们实现具有基本 Material Design 布局结构的界面。Scaffold 可以为最常见的顶级 Material 组件(如 TopAppBar、BottomAppBar、FloatingActionButton 和 Drawer)提供槽位。通过使用 Scaffold,可轻松确保这些组件得到适当放置且正确地协同工作。
示例:
创建 LayoutStudy.kt,完成布局
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
/**
* @Author HL
* @Date 2023/12/26 21:15
* @Version 1.0
*/
@Composable
fun LayoutStudy(){
Scaffold (
// topBar 布局顶部
topBar = {
TopAppBar(
// 设置标题
title = {
Text(text = "LayoutStudy")
},
// 设置图标
actions = {
IconButton(onClick = {}) {
Icon(imageVector = Icons.Filled.Favorite, contentDescription = null)
}
}
)
}
) { innerPadding ->
// 布局 body 部分
BodyContent(Modifier.padding(innerPadding))
}
}
@Composable
fun BodyContent(modifier : Modifier = Modifier){
Column(modifier = Modifier.padding(8.dp)) {
Text(text = "Hi there!")
Text(text = "Thanks for going through the LayoutStudy")
}
}
在 MainActivity.kt 里调用
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
JetpackComposeLayoutsTheme {
//PhotographerCard()
LayoutStudy()
}
}
}
}
如果我们知道用例不需要任何滚动,可以使用简单的 Column 或 Row。如果需要显示大量列表项(或长度未知的列表),可以使用 LazyColumn 或 LazyRow。
示例:设置一个可滚动的列表。可以通过 LayColumn 实现上下滚动的列表。
1. Lists.kt
@Composable
fun ScrollingList(){
val listSize = 100
val scrollState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
// 放一个包含2个按钮的顶部布局,再放一个 List,从上到下
Column() {
//TODO 放两个按钮,从左到右
Row {
// 第一个按钮
Button(
modifier = Modifier.weight(1f), // 设置权重
onClick = {
//Kotlin 携程
coroutineScope.launch {
// 滚到最顶部
scrollState.animateScrollToItem(0)
}
}
) {
Text(text = "Scroll to the top")
}
// 第二个按钮
Button(
modifier = Modifier.weight(1f),
onClick = {
coroutineScope.launch {
// 滚动最底部
scrollState.animateScrollToItem(listSize - 1)
}
}
) {
Text(text = "Scroll to the end")
}
}
//TODO 放列表项
LazyColumn(state = scrollState){
items(listSize){
// 列表项
ImageListItem(index = it)
}
}
}
}
@Composable
fun ImageListItem(index : Int){
Row(
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = R.drawable.beauty),
contentDescription = null,
modifier = Modifier.size(40.dp)
)
Spacer(modifier = Modifier.width(10.dp))
Text(text = "Item $index", style = MaterialTheme.typography.subtitle1)
}
}
2. MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
JetpackComposeLayoutsTheme {
ScrollingList()
}
}
}
}
在 Compose 中,界面元素由可组合函数表示,此类函数在被调用后会发出一部分界面,这部分界面随后会被添加到呈现在屏幕上的界面树中。每个界面元素都有一个父元素,还可能有多个子元素。此外,每个元素在其父元素中都有一个位置,指定为(x, y)位置;也都有一个尺寸,指定为 width 和 height。
使用布局修饰符
你可以使用 layout 修饰符来修改元素的测量和布局方式。Layout 是一个 lambda;它的参数包括你可以测量的元素(以 measurable 的形式传递)以及该组合项的传入约束条件(以 constraints 的形式传递)。
示例:我们有如下一个需求:有一个文本 “Hi threr!”,它有一个 firstbaseline(图中红框),正常情况下,firstbaseline 是到图中 1 处的距离。现在的需求是 firstbaseline 是和父元素的距离,也就是下图中 2 处的距离。
FirstBaselineToTop.kt
// firstBaselineToTop 为 Modifier 的扩展函数
fun Modifier.firstBaselineToTop(firstBaselineToTop : Dp) = this.then(// this.then() 的 放回值就是一个 Modifier
// TODO 用 layout 修饰符来修改元素的测量和布局位置
// measurable 测量元素;constraints 约束条件
layout { measurable, constraints ->
// 测量元素
val placeable = measurable.measure(constraints)
// 测量之后,获取元素的基线值,即 firsrBaseline 到元素顶部的值
val firstBaseline = placeable[FirstBaseline]
// 元素左上角新的 Y 坐标值 = 新基线值 - 旧基线值;firstBaselineToTop 方法传进来的值
// roundToPx() 转为像素
val Y = firstBaselineToTop.roundToPx() - firstBaseline
// 设置元素的宽和高。元素的高是指,元素底部到父容器顶部的位置
val height = placeable.height + Y
layout(placeable.width, height){
//设置元素的新位置
placeable.placeRelative(0, Y)
}
}
)
@Composable
fun TextWithPaddingToBaseline(){
JetpackComposeLayoutsTheme {
// 这里我们要 firstBaselineToTop 设置值为 baseline 到 父容器顶部为 24dp,
// TODO 这个值由我们自己设定
Text(text = "Hi There!",
Modifier
// 指定 firstBaselineToTop 的值
.firstBaselineToTop(24.dp) // 这里需要返回一个 Modifier 才能继续下面的链式调用
.background(Color.Red))
}
}
右边的布局中, firstBaselineToTop 值是到父容器顶部的距离,是我们上面代码中设定的 24dp, 然后 measure 可以帮我们测出 Text 的基线高度(firstBaseline),那么 Text 左上角黑点的位置坐标为(0, firstBaselineToTop - firstBaseline)
完成如下布局,模仿 Column 布局,给了4个文本,一列4行的方式排列。
示例代码:
@Composable
fun MyOwnColumn(
modifier: Modifier = Modifier,
content : @Composable ()-> Unit){
// 有多个元素,所以用 Layout()
Layout(
modifier = modifier,
content = content){ measurables, constraints ->
// 测量多个元素
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
var positionY = 0
// 元素布局的大小,设置为父容器的最大高度和宽度
layout(constraints.maxWidth, constraints.maxHeight){
placeables.forEach { placeable ->
//placeable 就是我们测量的每一个子容器
placeable.placeRelative(x = 0, y = positionY)
// positionY 更新高度,第一个元素的顶部到父容器顶部的距离为0;
// 第二个元素顶部到父容器顶部的距离为 第一个元素的高度
// 第三个元素顶部到父容器顶部的距离为 第一个元素的高度 + 第二个元的的高度
positionY += placeable.height
}
}
}
}
@Composable
fun MyOwnColumnSample(){
JetpackComposeLayoutsTheme() {
MyOwnColumn {
Text(text = "MyOwnColumn")
Text(text = "places items")
Text(text = "vertically.")
Text(text = "We've done it by hand!")
}
}
}
通过 StaggeredGrid 自定义如下布局
StaggeredGrid.kt
// 话题列表
val topics = listOf(
"Arts & Crafts",
"Beauty",
"Books",
"Business",
"Comics",
"Culinary",
"Design",
"Fashion",
"Film",
"History",
"maths"
)
@Composable
fun StaggeredGrid(
modifier: Modifier = Modifier,
rows : Int = 3, //指定元素最终传进来,显示多少行。默认3行
content : @Composable () -> Unit
){
Layout(
modifier = modifier,
content = content
){ measurables, constraints ->
// rowWidths 用于保存每行的宽度值
val rowWidths = IntArray(rows){ 0 }
// rowHeights 用于保存每行的高度值
val rowHeights = IntArray(rows){ 0 }
// 测量元素,带索引
val placeables = measurables.mapIndexed{index, measurable ->
// 得到每一个元素
val placeable = measurable.measure(constraints)
// 计算当前元素该去到数组的那个位置,row = 0, 1, 2
var row = index % rows
// 每一行的宽度由每一个元素的宽度累加
rowWidths[row] += placeable.width
// 每一行的高度由每一行中最高的元素代表
rowHeights[row] = max(rowHeights[row], placeable.height)
placeable
}
// 计算表格的宽度,即为 rowWidth 中最宽的那一行
// 计算表格的高度,即为 rowHeight 中元素之和。也就是rows列之和
val gridWidth = rowWidths.maxOrNull() ?: constraints.minWidth
val gridHeight = rowHeights.sumOf { it } // rowHeights 里所有元素相加
// 设置每一行的 Y 坐标,每一行中的每一个元素的 Y 坐标是一样的
val rowY = IntArray(rows){ 0 }
// 第一列元素的 Y 坐标都0,第i行(i > 0)元素的 Y 坐标为前面 i - 1 行的高度之和
for(i in 1 until rows){
rowY[i] = rowY[i - 1] + rowHeights[i - 1]
}
// TODO 设置表格的宽高
layout(gridWidth, gridHeight){
//TODO 布局每一个元素,那么就要计算每一个元素的(x, y)坐标
// 上面我们已经计算了每个元素的 Y坐标,TODO 这里还需计算每个元素的 X 坐标
val rowX = IntArray(rows){ 0 }
// 遍历每一个元素
placeables.forEachIndexed{ index, placeable ->
val row = index % rows
// 每一列的第一个元素的 X 值都是0,所以直接先放置,然后再修改 rowX[row]
placeable.placeRelative(
x = rowX[row],
y = rowY[row]
)
// 修改 rowX[row]位置的值,为同一列中下一个元素计算 X 值,即前面元素的 宽度之和
rowX[row] += placeable.width
}
}
}
}
// TODO 每一个条目
@Composable
fun Chip(
modifier: Modifier = Modifier,
text : String
){
// 一个卡片,圆角。里面包含一个 Row,第一列是 Box,第二列是 Text
Card (
modifier = modifier,
border = BorderStroke(color = Color.Black, width = Dp.Hairline),// 设置边框线,Hairline默认1dp
shape = RoundedCornerShape(8.dp) // 设置4个圆角
) {
Row(
modifier = Modifier.padding(start = 8.dp, top = 4.dp, end = 8.dp, bottom = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier = Modifier
.size(16.dp, 16.dp)
.background(color = MaterialTheme.colors.secondary)
)
Spacer(modifier = Modifier.width(8.dp))
Text(text = text)
}
}
}
@Composable
fun StaggeredGridBodyContent(){
Row(
modifier = Modifier
.background(color = Color.LightGray)
.padding(16.dp)
.horizontalScroll(rememberScrollState()), // 可以水平滚动
content = {
StaggeredGrid(modifier = Modifier) {
// 放元素
for(topic in topics){
Chip(modifier = Modifier.padding(8.dp), text = topic)
}
}
}
)
}
Activity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
JetpackComposeLayoutsTheme {
//PhotographerCard()
//LayoutStudy()
//SampleList()
//LazyList()
//ScrollingList()
//TextWithPaddingToBaseline()
//MyOwnColumnSample()
//StaggeredGrid()
StaggeredGridBodyContent()
}
}
}
}
在实现对齐要求比较复杂的较大布局时,ConstraintLayout 很有用。
引用是使用 createRefs() 或 createRefFor() 创建的,ConstraintLayout 中的每个可组合项都需要有与之关联的引用。
约束条件是使用 constrainAs() 修饰符提供的,该修饰符将引用作为参数,可让你在主体 lambda 中指定其约束条件。
约束条件是使用 linkTo() 或其它有用的方法指定的。parent 是一个现有的引用,可用于指定对 ConstraintLayout 可组合项本身的约束条件。
案例1:用 ConstraintLayout 布局出如下效果。
1. 导入 ConstraintLayout 的依赖
implementation 'androidx.constraintlayout:constraintlayout-compose:1.0.0-alpha08'
2. ConstraintLayout.kt
@Composable
fun ConstraintLayoutContent(){
// TODO 需要在 build.gradle 中引入依赖
ConstraintLayout {
//TODO 1. 通过 createRefs 创建引用,ConstraintLayout 中的每个元素都需要关联一个引用
val (button, text) = createRefs()
Button(
onClick = { /*TODO*/ },
//TODO 2.使用 Modifier.constrainAs 来提供约束,引用作为它的第一个参数
// 在 lambda 表达式中指定约束规则
modifier = Modifier.constrainAs(button){
// TODO 3.约束条件是使用 linkTo(), parent.top表示在父容器顶部,有一个外边距 margin
top.linkTo(parent.top, margin = 16.dp)
}
) {
Text(text = "Button")
}
Text(text = "Text", modifier = Modifier.constrainAs(text) {
// 约束条件是使用 linkTo(), button.bottom表示位于button 的底部,有一个外边距 margin
top.linkTo(button.bottom, margin = 16.dp)
// 在 ConstraintLayout 中水平居中
centerHorizontallyTo(parent)
})
}
}
案例2:
@Composable
fun ConstraintLayoutContent2(){
ConstraintLayout {
val (button1, button2, text) = createRefs()
Button(
onClick = {},
modifier = Modifier.constrainAs(button1){
top.linkTo(parent.top, margin = 16.dp)
}
) {
Text(text = "Button1")
}
Text(text = "Text", modifier = Modifier.constrainAs(text) {
// 约束条件是使用 linkTo(), button.bottom表示位于button 的底部,有一个外边距 margin
top.linkTo(button1.bottom, margin = 16.dp)
//TODO 在 text 的中间位置位于 boton1 的 end 位置
centerAround(button1.end)
})
// 将 button1 和 text 组合起来,建立一个屏障
val barrier = createEndBarrier(button1, text)
Button(
onClick = { },
modifier = Modifier.constrainAs(button2){
//TODO top: 给顶部设置约束,start: 给左边设置约束
top.linkTo(parent.top, margin = 16.dp)
start.linkTo(barrier)
}
) {
Text(text = "Button1")
}
}
}
在某些情况下,最好将约束条件与应用它们的布局分离开来。例如,我们可能会希望根据屏幕配置来更改约束条件,或在两个约束条件集之间添加动画效果。
将 ConstraintSet 作为参数传递给 ConstraintLayout。
使用 layoutId 修饰符将在 ConstraintSet 中创建的引用分配给可组合项。
如下代码所示:我们系统代码中 margin 是根据屏幕旋转时,动态设置的值,这种就不好弄了。
我们可以通过解耦的方式,将 Button 和 Text 组件之间的约束关系在外部定义,然后传入到 ConstraintLayout 中。而 margin 作为一个其它约束,就作为参数传递。如下代码所示
@Composable
fun DecoupledConstraintLayout2(){
// 打包
BoxWithConstraints {
// maxWidth 和 maxHeight 是 BoxWithConstraints 中的属性
val constrains = if(maxWidth < maxHeight){
//TODO 返回的是一个 ConstraintSet
DecoupledConstraints(16.dp) // 竖屏情况下
}else{
DecoupledConstraints(64.dp)// 横屏情况下
}
ConstraintLayout (constrains) {
Button(
onClick = { },
modifier = Modifier.layoutId("button")
) {
Text(text = "Button")
}
Text(text = "Text", modifier = Modifier.layoutId("text"))
}
}
}
// TODO 将Button 和 Text 的约束抽离到外部,
private fun DecoupledConstraints(margin : Dp) : ConstraintSet{
return ConstraintSet{
val button = createRefFor("button")
val text = createRefFor("text")
constrain(button){
top.linkTo(parent.top, margin)
}
constrain(text){
top.linkTo(button.bottom, margin)
}
}
}
Compose 只测量子元素一次,测量两次会引发运行时异常。但是,有时在测量子元素之前,我们需要一些有关子元素的信息。
Intrinsics 允许你在实际测量之前查询子项。1)(min|max)intrinsicWidth:鉴于此高度,你可以正确绘制内容的最小/最大宽度是多少。2)(min|max)intrinsicHeight:鉴于此宽度,你可以正确绘制内容的最小/最大高度是多少。
示例:如下布局。要在中间做一个分割线,它的高度与左右文本的高度一致
@Composable
fun TwoTexts(modifier: Modifier = Modifier){
// 将 Row 的高度限制为文本的高度,通过 Intrinsic 在测量之前来获取文本的高度信息
Row (modifier = Modifier.height(IntrinsicSize.Min)) {
Text(
text = "Hi",
modifier = Modifier
.padding(start = 4.dp)
.weight(1f)
.wrapContentWidth(Alignment.Start)
)
// 两个文本中间的线
Divider(
color = Color.Black,
modifier = Modifier
.fillMaxHeight() //fillMaxHeight()获取父容器最大高度
.width(1.dp))
Text(
text = "There",
modifier = Modifier
.padding(start = 4.dp)
.weight(1f)
.wrapContentWidth(Alignment.End)
)
}
}