上次说到 Jetpack Compose 基础 | 混个脸熟篇 今天我们来继续说说JetPack Compose 基础——布局
本文主要内容:
了解Modifier的作用。
Box、Colum、Row、ConstraintLayout的使用。
重点介绍了 Box 和 Column 参数的使用。
搞懂 Column 中的 Arrangement和 Alignment 的区别。
说布局前,先来简单了解一下修饰符——Modifier
修饰符可以控制 组件的行为和外观 如大小,背景等,还可以添加一些交互,如点击、滑动等。
Text(
"Android",
modifier = Modifier
.padding(10.dp)//设置padding
.size(100.dp)//设置大小
.background(Color.Gray)//设置背景
.clickable(onClick = {
})//添加点击事件
)
Modifier 本身并没有很多方法,都是利用 kotlin 扩展函数实现的,每个方法的返回值都是Modifier 可以实现链式调用。
利用 kotlin 扩展函数将Modifier 不同类别的方法划分到不同文件中,例如设置大小的 androidx/compose/foundation/layout/Size.kt 中
Modifier 方法太多太多,慢慢探索吧。
Modifier 链式调用,调用顺序不同,可能产生的效果也会不同
Text(
"Android",
modifier = Modifier
//在设置size之前设置padding相当于外边距
.padding(10.dp)
//此时组件占据空间大小100.dp+外边距 即大小为120.dp*120.dp
.size(100.dp)
//在设置size之后设置相当于内边距,组件大小不变
.padding(10.dp)
//设置背景,对应背景来说,在它之前设置的padding 就相当于外边距,所以背景的绘制大小只有90.dp*90.dp
.background(Color.Gray)
.padding(20.dp)//内边距,背景大小不变
//添加点击事件,同理点击区域的大小90.dp-20.dp 所以可点击局域大小只有70.dp*70.dp
.clickable(onClick = {
})
)
例如上面Text 在设置size之前设置padding 和之后设置 padding 是效果不一样的,先设置padding 再设置 size 那么先设置的padding就相当外边距,在size之后设置的padding 相当于内边距。具体执行演示可以看下面演示效果图。
类似于Android View 体系中的 FrameLayout
@Composable
fun BoxDemo() {
Box(modifier = Modifier
.size(100.dp)) {
Box(modifier = Modifier.size(30.dp).background(Color.Blue))
Box(modifier = Modifier.size(20.dp).background(Color.Red))
}
}
@Composable
inline fun Box(
modifier: Modifier = Modifier,//修饰符
contentAlignment: Alignment = Alignment.TopStart,//对齐方法
propagateMinConstraints: Boolean = false,
content: @Composable BoxScope.() -> Unit
) {
……Box实现代码
}
从参数可以看出Box可以设置内容对齐方式(contentAlignment)这种方式是给Box的所有 child 设置对齐方式,如果只想给单个child 设置怎么操作呢?
@Composable
fun BoxAlignDemo() {
Box(
modifier = Modifier
.size(100.dp)
) {
Box(
modifier = Modifier
.size(30.dp)
.background(Color.Blue)
//设置此child 在Box的位置
.align(Alignment.TopStart)
)
Box(
modifier = Modifier
.size(20.dp)
.background(Color.Red)
.align(Alignment.BottomEnd)
)
}
}
想对单个 child 设置对齐方式只需要在对应的修饰符调用 align
方法即可,此 align
方法只有在 Box
里面才行,在外面是没法调用到这个方法的,这就是 Kotlin 扩展函数那部分的知识了。
Box
的参数 content
是 BoxScope
的扩展函数类型,那么此函数作用域 this
就是 BoxScope
的对象,而 Modifier
的扩展函数 align
是定义在 BoxScpoe
中的,所有想调用这个方法必须在BoxScope作用域下。
Kotlin 扩展函数如果直接成顶层函数的形式,不属于任何任何类,那么任何地方都可以调用,如Size.kt 定义的那些 Modifier 扩展函数。
Kotlin 扩展函数如果写在某个类中,那么像调用此扩展函数就就必须在此类的作用域下,如 BoxScope 中定义的 Modifier 扩展函数。
如果对此地方不太懂得可以再去看看 Kotlin 文档关于这部分的介绍 kotlin 扩展函数 | Kotlin 语言中文站
一堆元素怎么排列,最简单的就是垂直排序或水平排序,在 Compose 的世界里垂直排序就是 Column
Column {
Text(text = "Android")
Text(text = "Android")
}
@Composable
inline fun Column(
modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
content: @Composable ColumnScope.() -> Unit
) {
...
}
布局最重要的作用就是控制内容的摆放位置,Column
有两个属性 verticalArrangement
和 horizontalAlignment
来控制 child 的摆放。
Column 的 children 是按照垂直方向依次排列,children在垂直方向如何分布?都靠近上方?靠近下方?每个child 之间在垂直空间上是否均匀分布?这些都由 verticalArrangement
这个属性来安排控制。
其内置实现的取值可以为:
Arrangement.Top
尽可能的靠近主轴顶部,垂直依次排列 。如果用数字表示child ,#表示间距,那么此情况示意图表示为:123####
Arrangement.Bottom
尽可能的靠近主轴底部,垂直依次排列。 示意图:#####123
Arrangement.Center
尽可能的靠近主轴中心,垂直依次排列。 示意图:###123###
Arrangement.SpaceBetween
剩余空间在child 与 child 均匀分布,第一个child 的上方和最后一个child的下方没有间距。示意图:1###2###3
Arrangement.SpaceEvenly
children 在主轴上均匀分布 示意图:##1##2##3##
Arrangement.SpaceAround
第一个child 的上方和最后一个child的下方有间距且相同,其间距记作 d 1 d1 d1 ,child与child 的间距也相同,其间距记作 d 2 d2 d2,且 d 2 d2 d2 是 d 1 d1 d1 的2倍。示意图#1##2##3##3#
除了上面定义好的系统值,Arrangement
还有一些方法帮助我自定义 children 排版
// 设置 children 的固定间距(可以是负数,就会叠在一起),及在主轴上的对齐方式
fun spacedBy(space: Dp, alignment: Alignment.Vertical): Vertical
//仅设置在主轴上的对齐方式
fun aligned(alignment: Alignment.Vertical): Vertical
影响 children 在主轴上排列因素是除了对齐方式还有间距的分布。所以 Column 在垂直方向是通过 Arrangement
控制而不是 Alignment
,Arrangement
可以控制对齐方式及间距。
horizontalAlignment 控制水平方向的对齐方式,其取值可以为 Alignment.Start
、Alignment.End
、 Alignment.CenterHorizontally
其具体效果就不演示了。
刚开始看 Column 的参数时候,其实我是不太明白为啥垂直搞个 Arrgngement 水平搞个 Alignment,直接搞一个像Box 那样的参数 contentAlignment 不香吗?
搞那么复杂。后来仔细分析一下这样搞是有它的道理的,其原因就是上面的分析,在垂直方向除了对齐方式,还有 children 的间距因素可以控制。
而水平方式,一行就一个不存在 children 的间距 所以一个对齐方式Alignment 就可以控制了。
话说View中 LInerLayout 就没有可以设置 children 间距的参数,但后来在ConstrainLayout 的 chain 弥补了这一缺陷。
Arrangement & Alignment区别
Arrangment 安排、排列的意思。它用于控制 children 在主轴方向的排列方式(对齐方式+间距)
Alignment 对齐的意思,它用于控制 children 的对齐方式。
上面都在说 Column 是如何控制 children 的排布的,下面介绍一下 children 如何说出自己的想法,根据它们心中所向来排列(前提是 Column 要同意)
Column 给自己的 children 添加了设置 权重 weight 和 align 对齐的方法。
/*
根据weight权重重新分配 Column 垂直方向的空间。
*/
fun Modifier.weight(
weight: Float, //权重
fill: Boolean = true //是否充满分配的空间
)
weight 这个在LinerLayout 中也有,指定weight 就可以让 children 按照比例分布。Column 的 weight 也是类似,但不同的是多了一个参数 fill。
fill 表示是否是否充满分配的空间
例如:child 指定的高度为50.dp 通过weight 分配到的大小为100.dp (分配到的值比原来大) fill=true的话,那么它最终的高度就是100.dp,如果fill=false 那么最终的高度为50.dp。
child 指定的高度为50.dp 通过weight 分配到的大小为 只有30.dp (分配到的值比原来小) 那么不管 fill的值是true 还是false 最终的高度都是30.dp
结论:用下面一段伪代码总结
var 最终高度:Dp
if(分配高度>指定高度){
最终高度=if(fill) 分配高度 else 指定高度
}else{
最终高度=分配高度
}
水平排序 Row
Row {
Text(text = "Android")
Text(text = "Android")
}
Row
的 children 是水平排列的,其用法和 Column
类似,这里就不介绍了。
查看Column 可以知道,Column 是会先空间分配给没有设置 weight 的 children ,剩下的空间再分给带weight 所以有时可分配的空间很小,即使 weight 很大也分配不到太多空间了。
在 View 体系中 ConstraintLayout 一个很强大的布局,对于复杂的布局可以轻松应对,还可以降低视图层级,提高性能。在 Compose 的世界 ConstraintLayout 当然也不能缺席。在Compose 中使用 ConstraintLayout 需要引入依赖
implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-alpha05"
在 Compose 中能够高效地处理较深的布局层次结构,所以一般布局都可以使用 Row 、Column 等这种基础布局嵌套实现,除非布局中对齐要求比较复杂,这是可以考虑使用ConstraintLayout
先看一个 View 体系中一个简单的 ConstraintLayout 布局
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"
android:layout_marginTop="10dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Android"
android:layout_marginTop="30dp"
app:layout_constraintTop_toBottomOf="@id/button"
app:layout_constraintEnd_toEndOf="@id/button"
app:layout_constraintStart_toStartOf="@id/button"
/>
androidx.constraintlayout.widget.ConstraintLayout>
ConstraintLayout(Modifier.fillMaxWidth()) {
//创建参考,Compose 没有id 就用这个代替
val button = createRef()
Button(onClick = {
}, modifier = Modifier.constrainAs(button/*关联参考*/) {
//作用域 this:ConstrainScope
//相当于 app:layout_constraintTop_toTopOf="parent" 还设置了距离顶部10.dp
top.linkTo(parent.top,10.dp)
//<=> app:layout_constraintTop_toTopOf="parent"
start.linkTo(parent.start)
//<=> app:layout_constraintEnd_toEndOf="parent"
end.linkTo(parent.end)
}) {
Text(text = "Button")
}
val text = createRef()
Text(text = "Android", Modifier.constrainAs(text) {
//app:layout_constraintTop_toBottomOf="@id/button" 还设置了距离button 底部30.dp
top.linkTo(button.bottom,30.dp)
start.linkTo(button.start)
end.linkTo(button.end)
})
}
都是 ConstraintLayout 不管是 View 还是 Compose 思想都是一样的,写法也很相似,类别学一学应该就差不多了,写此文时 Conpose 版的 ConstraintLayout 还是 alpha 阶段,还不太稳定,api 以后可能会变,在这里就简单介绍一下基础用法,其他的目前就再研究了。
Compose 中的布局 | Android Developers
Compose的标准布局组件
更多 JetPack Compose 系列文章,你查看我的 JetPack Compose 专栏
Jetpack Compose 基础 | 混个脸熟篇