布局
主要学习内容
- 如何使用 Material 组件可组合项
- 什么是修饰符以及如何在布局中使用它们
- 如何创建自定义布局
- 何时可能需要固有特性
修饰符
借助Modifier
,可以修饰可组合项。您可以更改其行为、外观,添加无障碍功能标签等信息,处理用户输入,甚至添加高级互动(例如使某些元素可点击、可滚动、可拖动或可缩放)。Modifier
是标准的 Kotlin 对象。您可以将它们分配给变量并重复使用,也可以将多个修饰符逐一串联起来,以组合这些修饰符。
下面我们通过实现下方的个人介绍布局进行学习
首先实现最基础的布局
@Preview(group = "2.1", widthDp = 160, backgroundColor = 0xFFFFFF, showBackground = true)
@Composable
fun UserInfoPreview() {
StudyOfJetpackComposeTheme {
UserInfoCard()
}
}
@Composable
fun UserInfoCard(name: String, time: Long = 1000 * 60 * 60 * 3) {
Row {
Image(painter = painterResource(id = R.drawable.user), contentDescription = null)
Column {
Text(text = name)
Text(text = "${TimeUnit.MILLISECONDS.toMinutes(time)} minutes ago")
}
}
}
很显然仅完成布局和预计实现的效果相差甚远,需要通过设置控件属性和Modifier
进行对应的修饰
@Composable
fun UserInfoCard(
name: String = "oddly",
time: Long = 1000 * 60 * 3
) {
Row(modifier = modifier.padding(4.dp)) {
Image(
painter = painterResource(id = R.drawable.user), contentDescription = null,
modifier = Modifier
.size(50.dp)
.clip(CircleShape)
.align(Alignment.CenterVertically),
//图像居中放大直至填充满控件
contentScale = ContentScale.Crop
)
Column(
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(start = 6.dp)
) {
Text(text = name, fontWeight = FontWeight.Bold)
Text(
text = "${TimeUnit.MILLISECONDS.toMinutes(time)} minutes ago",
style = MaterialTheme.typography.body2.copy(fontSize = 12.sp),
color = Color.Unspecified.copy(ContentAlpha.medium)
)
}
}
}
通过包含文本的 Column
上使用 Modifier.padding
,从而在可组合项的 start
上添加一些空间,用以分隔图像和文本
某些布局提供了仅适用于它们本身及其布局特性的修饰符。例如,Row
中的可组合项可以访问适用的修饰符(来自 Row 内容的 RowScope
接收者),例如 weight
或 align
。这种作用域限制具备类型安全性,因此您不可能会意外使用在其他布局中不适用的修饰符(例如,weight
在 Box
中就不适用),系统会将其视为编译时错误加以阻止
inline fun Row( modifier: Modifier = Modifier, horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, verticalAlignment: Alignment.Vertical = Alignment.Top, content: @Composable RowScope.() -> Unit ){...}
通过
Row
的构造函数可以看出我们传入的函数为RowScope
的扩展函数,RowScope
中规定了Modifier
适用的修饰符interface RowScope { @Stable fun Modifier.weight( weight: Float, fill: Boolean = true ): Modifier @Stable fun Modifier.align(alignment: Alignment.Vertical): Modifier @Stable fun Modifier.alignBy(alignmentLine: HorizontalAlignmentLine): Modifier @Stable fun Modifier.alignByBaseline(): Modifier @Stable fun Modifier.alignBy(alignmentLineBlock: (Measured) -> Int): Modifier }
修饰符的作用与 View 系统中 XML 属性类似,但特定于作用域的修饰符具备的类型安全性可帮助您发现和了解可用且适用于特定布局的内容。与之相比,XML 布局并不总是清楚某个布局属性是否适用于给定视图
<!-- 以下设置不会报错,只会警告:Invalid layout param in XXX --> <!-- 可以在RelativeLayout中设置orientation --> <!-- 可以在RelativeLayout的子控件中设置layout_weight --> <RelativeLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" android:background="@color/purple_200"> <TextView android:layout_weight="1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Fucking World!" /> </RelativeLayout>
大多数可组合项都接受可选的修饰符参数,以使其更加灵活,从而让调用方能够修改它们。如果您要创建自己的可组合项,不妨考虑使用修饰符作为参数,将其默认设置为 Modifier
(即不执行任何操作的空修饰符),并将其应用于函数的根可组合项。在本例中:
@Composable
fun UserInfoCard(
modifier: Modifier = Modifier,
name: String = "oddly",
time: Long = 1000 * 60 * 3
) {
Row(modifier) { ... }
}
按照
Goolgle
推荐,修饰符应该被指定为函数的第一个可选参数
修饰符顺序
串联修饰符时请务必小心,因为顺序很重要。由于修饰符会串联成一个参数,所以顺序将影响最终结果
例1:给UserInfoCard
添加一个背景颜色和padding
@Composable
fun UserInfoCard(
modifier: Modifier = Modifier,
name: String = "oddly",
time: Long = 1000 * 60 * 3
) {
Row(modifier = modifier
.padding(4.dp)
.background(Purple200)) {
...
}
}
可以看到实现的效果与想象的有些不一样,padding不应该是内边距吗?怎么这里padding设置的是外边距,因为 padding
是在 background
修饰符前面应用的
如果我们在 background
后面应用 padding
修饰符实现效果才是内边距
串联修饰符相当于是在前一个修饰符的基础上进行操作,所以在Compose
中没有margin
修饰符【串联方式下作用和padding
重复】
例2:添加圆角和背景
@Composable
fun UserInfoCard(
modifier: Modifier = Modifier,
name: String = "oddly",
time: Long = 1000 * 60 * 3
) {
Row(modifier = modifier
.padding(2.dp)
.background(Purple200)
.clip(RoundedCornerShape(4.dp))
.padding(4.dp)) {
...
}
}
此时会发现圆角效果并没有实现!
其实并不是没有实现,还是因为顺序问题导致background
先进行,然后进行clip
,如果在clip
后再新增一个background
就可以看到clip
实际上是有效的。
Row(modifier = modifier
.padding(2.dp)
.background(Purple200)
//为了效果明显,修改圆角半径大小
.clip(RoundedCornerShape(12.dp))
.background(Teal200)
.padding(4.dp)) {
...
}
clip
约束的区域只对之后的修饰符生效
所以正确的顺序应该为:
Row(modifier = modifier
.padding(2.dp)
.clip(RoundedCornerShape(4.dp))
.background(Purple200)
.padding(4.dp)) {
...
}
明确的顺序可帮助您推断不同的修饰符将如何相互作用。您可以将这一点与 View 系统进行比较。在 View 系统中,您必须了解盒模型;在这种模型中,在元素的“外部”应用外边距,而在元素的“内部”应用内边距,并且背景元素将相应地调整大小。修饰符设计使这种行为变得明确且可预测,并且可让您更好地进行控制,以实现您期望的确切行为
槽位API
槽位 API 是 Compose 引入的一种模式,它在可组合项的基础上提供了一层自定义设置,在本例中,提供的是可用的 Material 组件可组合项。
以Button
为例:
对一个按钮而言,可能会给按钮设置图标、文字,而针对于图标可能又有属性需要设置,如图标大小,位置等信息,文字也可能需要字体大小、字体样式等信息要设置,如果这些都需要在Button
中使用参数方式设置会导致参数爆炸,而且容易出现部分功能还无法实现
//伪代码,仅用于描述
Button(
text = "Button",
icon: Icon? = myIcon,
textStyle = TextStyle(...),
spacingBetweenIconAndText = 4.dp,
...
)
因此Compose
添加了槽位,槽位会在界面中留出空白区域,让开发者按照自己的意愿来填充
@Composable
fun Button(
modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null,
...
content: @Composable () -> Unit
)
命名为 content
的 lambda 是最后一个参数。这样,我们就能使用尾随 lambda 语法,以结构化方式将内容插入到 Button 中。
在更复杂的组件(如顶部应用栏)中,Compose 会大量使用槽位
在构建自己的可组合项时,您可以使用槽位 API 模式提高它们的可重用性
Material 组件
Compose 附带内置的 Material 组件可组合项,我们可以用它们创建应用。最高级别的可组合项是 Scaffold
。
Scaffold
可以使用 Scaffold
实现具有基本 Material Design 布局结构的界面。Scaffold
可以为最常见的顶层 Material 组件(例如 TopAppBar
、BottomAppBar
、FloatingActionButton
和 Drawer
)提供槽位。使用 Scaffold
时,您可以确保这些组件能够正确放置并协同工作
@Composable
fun Scaffold(
modifier: Modifier = Modifier,
scaffoldState: ScaffoldState = rememberScaffoldState(),
topBar: @Composable () -> Unit = {},
bottomBar: @Composable () -> Unit = {},
snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
floatingActionButton: @Composable () -> Unit = {},
floatingActionButtonPosition: FabPosition = FabPosition.End,
isFloatingActionButtonDocked: Boolean = false,
drawerContent: @Composable (ColumnScope.() -> Unit)? = null,
drawerGesturesEnabled: Boolean = true,
drawerShape: Shape = MaterialTheme.shapes.large,
drawerElevation: Dp = DrawerDefaults.Elevation,
drawerBackgroundColor: Color = MaterialTheme.colors.surface,
drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
drawerScrimColor: Color = DrawerDefaults.scrimColor,
backgroundColor: Color = MaterialTheme.colors.background,
contentColor: Color = contentColorFor(backgroundColor),
content: @Composable (PaddingValues) -> Unit
)
可以看到在Scaffold
中存在许多槽位
:topBar
、bottomBar
、snackbarHost
等
Scaffold API
中的所有参数都是可选的,但 @Composable (InnerPadding) -> Unit
类型的正文内容除外:lambda 会接收内边距作为参数。这是应该应用于内容根可组合项的内边距,用于在界面上适当限制各个项
@Composable
fun ScaffoldStudy(modifier: Modifier = Modifier) {
Scaffold(modifier) { paddingValue ->
Text(text = "Hi Scaffold", modifier = Modifier.padding(paddingValue))
}
}
如果我们想使用含有界面主要内容的 Column
,应该将修饰符应用于 Column
为了提高代码的可重用性和可测试性,我们应该将其构造为多个小的数据块
@Composable
fun ScaffoldStudy(modifier: Modifier = Modifier) {
Scaffold(modifier) { paddingValue ->
BodyContent(Modifier.padding(paddingValue))
}
}
@Composable
fun BodyContent(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
Text(text = "Hi Scaffold!")
Text(text = "There is BodyContent")
}
}
TopAppBar
通常情况下,Android 应用中会显示一个顶部应用栏,其中包含有关当前界面、导航和操作的信息
Scaffold
包含一个顶部应用栏的槽位,其 topBar
参数为 @Composable () -> Unit
类型,这意味着我们可以用任何想要的可组合项填充该槽位
@Composable
fun ScaffoldStudy(modifier: Modifier = Modifier) {
Scaffold(
modifier,
topBar = { TopBarContent() },
) { paddingValue ->
BodyContent(Modifier.padding(paddingValue))
}
}
@Composable
fun TopBarContent() {
Text(text = "TopAppBar",style = MaterialTheme.typography.h3)
}
不过,与大多数 Material 组件一样,Compose 附带一个 TopAppBar
可组合项,其包含用于标题、导航图标和操作的槽位。此外,它还包含一些默认设置,可以根据 Material 规范的建议(例如要在每个组件上使用的颜色)进行调整。
按照槽位 API 模式,我们希望 TopAppBar
的 title
槽位包含一个带有界面标题的 Text
:
@Composable
fun TopBarContent() {
TopAppBar(
title = {
Text(text = "MaterialDesign控件")
}
)
}
顶部应用栏通常包含一些操作项。在本示例中,我们将添加一个收藏夹按钮。当您觉得自己已学会一些内容时,可以点按该按钮。Compose 还带有一些您可以使用的预定义 Material 图标,例如关闭、收藏夹和菜单图标。
顶部应用栏中的操作项槽位为 actions
参数,该参数在内部使用 Row
,因此系统会水平放置多个操作。如需使用某个预定义图标,可结合使用 IconButton
可组合项和其中的 Icon
:
@Composable
fun TopBarContent() {
TopAppBar(
title = {
Text(text = "MaterialDesign控件")
},
actions = {
IconButton(onClick = { /* doSomething() */ }) {
Icon(Icons.Filled.Favorite, contentDescription = null)
}
}
)
}
放置修饰符
每当创建新的可组合项时,提高可组合项可重用性的一种最佳做法是使用默认为 Modifier
的 modifier
参数
当BodyContent
可组合项已经接受一个修饰符作为参数。如果要为 BodyContent
再添加一些内边距,应该在什么位置放置 padding
修饰符?
- 将修饰符应用于可组合项中唯一的直接子元素,以便所有对
BodyContent
的调用都会应用额外的内边距:
@Composable
fun BodyContent(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(8.dp)) {
Text(text = "Hi Scaffold!")
Text(text = "Here is BodyContent")
}
}
- 在调用可视需要添加额外内边距的可组合项时,应用修饰符:
@Composable
fun ScaffoldStudy(modifier: Modifier = Modifier) {
Scaffold(modifier) { paddingValue ->
BodyContent(Modifier.padding(paddingValue).padding(8.dp))
}
}
确定在何处放置修饰符完全取决于可组合项的类型和用例
如果修饰符是可组合项的固有特性,则将其放置在内部;如果不是,则放置在外部。即如果这个修改不对外开放的时候就在内部进行修改【方法1】
如果想要使用更多Material图标,需要在项目的
build.gradle
文件中添加依赖dependencies { ... implementation "androidx.compose.material:material-icons-extended:$compose_version" }
列表
使用列表
显示项列表是应用的常见模式。Jetpack Compose 可让您使用 Column
和 Row
可组合项轻松实现此模式,但它还提供了仅应用于编写和布局当前可见项的延迟列表【LazyColumn
和LazyRow
】
@Composable
fun SimpleList() {
Column {
repeat(100) {
Text("Item #$it")
}
}
}
默认情况下,Column
不会处理滚动操作,某些项是看不到的,因为它们在界面范围外。请添加 verticalScroll
修饰符,以在 Column
内启用滚动:
@Composable
fun SimpleList() {
val scrollState = rememberScrollState()
Column(Modifier.verticalScroll(scrollState)) {
repeat(100) {
Text("Item #$it")
}
}
}
但在数据量大的情况下使用这种方式创建列表会造成性能问题,可能会导致界面卡顿
延迟列表
Column
会渲染所有列表项,甚至包括界面上看不到的项,当列表变大时,这会造成性能问题。为避免此问题,请使用 LazyColumn
,它只会渲染界面上的可见项,因而有助于提升性能,而且无需使用 scroll
修饰符
Jetpack Compose中的
LazyColumn
等同于RecycleView
LazyColumn
具有一个 DSL
,用于描述其列表内容。您将使用 items
,它会接受一个数字作为列表大小。它还支持数组和列表(如需了解详情,请参阅列表文档部分)。
@Composable
fun LazyList() {
val scrollState = rememberLazyListState()
LazyColumn(state = scrollState) {
items(100) {
Text("Item #$it")
}
}
}
列表中显示图像
Image
是一个可组合项,用于显示位图或矢量图像。如果图像是远程获取的,则该过程涉及的步骤会更多,因为应用需要下载资源,将其解码为位图,最后在 Image
中进行渲染。
如需简化这些步骤,可以使用 Coil 库,它提供了能够高效运行这些任务的可组合项。
Coil:Android官推 kotlin-first的图片加载库
将 Coil 依赖项添加到项目的 build.gradle
文件中:
// build.gradle
dependencies {
implementation 'io.coil-kt:coil-compose:1.4.0'
...
}
由于我们要提取远程图像,请在清单文件中添加 INTERNET
权限:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.studyofjetpackcompose">
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.INTERNET" />
<application
...
android:networkSecurityConfig="@xml/network_security_config">
...
</application>
</manifest>
xml/network_security_config
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
代码使用
const val imgPath="https://upload.jianshu.io/users/upload_avatars/22683414/11b218ff-7c9a-43dd-a84a-b3b41de13318.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/240/h/240"
@Composable
fun LazyImageListItem(modifier: Modifier = Modifier, text: String) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(4.dp)
) {
Image(
painter = rememberImagePainter(data = imgPath),
contentDescription = null,
modifier = Modifier.size(50.dp)
)
Text(text = "LazyItem #$text")
}
}
@Composable
fun LazyImageList(modifier: Modifier = Modifier) {
LazyColumn(modifier = modifier) {
items(100) {
LazyImageListItem(text="$it")
}
}
}
列表滚动控制
有时候我们需要控制列表滚动位置,比如滚动到最顶部或最底部
为避免在滚动时阻止列表呈现,滚动 API 属于挂起函数。因此,我们需要在协程中调用它们。如需实现此目的,可使用 rememberCoroutineScope
函数创建 CoroutineScope
,从按钮事件处理脚本创建协程
@Composable
fun LazyImageList(modifier: Modifier = Modifier, itemSize: Int) {
val scrollState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
Column(modifier) {
Row {
Button(modifier = Modifier.weight(1f), onClick = {
coroutineScope.launch {
scrollState.animateScrollToItem(0)
}
}) {
Text(text = "Scroll to Top")
}
Button(modifier = Modifier.weight(1f), onClick = {
coroutineScope.launch {
scrollState.animateScrollToItem(itemSize - 1)
}
}) {
Text(text = "Scroll to Bottom")
}
}
LazyColumn(modifier = modifier, state = scrollState) {
items(itemSize) {
LazyImageListItem(text = "$it")
}
}
}
}
自定义布局
Compose 提高了这些像小数据块一样的可组合项的可重用性,您可以组合各种内置可组合项(例如 Column
、Row
或 Box
),充分满足某些自定义布局的需求
不过,您可能需要为应用构建一些独特内容,构建时需要手动测量和布置子元素。对此,可以使用 Layout
可组合项。实际上,Column
和 Row
等所有较高级别的布局都是基于此构建的
相当于View系统中重写
ViewGroup
编写onMeasure
和onLayout
函数,不过在Compose中只需要编写Layout
可组合函数即可其实是将测量和摆放在
Layout
中一起完成了而已,不过在测量上比View体系方便,不需要像View体系中手动计算margin值
Compose 中的布局原则
某些可组合函数在被调用后会发出一部分界面,这部分界面会添加到将呈现到界面上的界面树中。每次发送(或每个元素)都有一个父元素,还可能有多个子元素。此外,它在父元素中具有位置 (x, y) 和大小,即 width
和 height
即每个可组合函数创建一个控件都会记录其的宽和高、在父控件中的位置(x,y)
系统会要求元素使用其应满足的约束条件进行自我测量。约束条件可限制元素的最小和最大 width
和 height
。如果某个元素有子元素,它可能会测量每个元素,以帮助确定它自己的大小。一旦某个元素报告了自己的大小,就有机会相对于自身放置它的子元素。创建自定义布局时,我们将对此做进一步解释说明
即让最底层的控件先进行测量得出宽高,将数值传给父控件,然后其父控件根据子控件的宽高计算得出自己的宽高再交给父控件的父控件,一层一层套娃向上传递
Compose 界面不允许多遍测量。这意味着,布局元素不能为了尝试不同的测量配置而多次测量任何子元素。单遍测量对性能有利,使 Compose 能够高效地处理较深的界面树。如果某个布局元素测量了它的子元素两次,而该子元素又测量了它的一个子元素两次,依此类推,那么一次尝试布置整个界面就不得不做大量的工作,这使得很难让应用保持良好的性能。不过,有时除了子元素的单遍测量告知您的信息之外,您确实还需额外的信息 - 对于这些情况,我们稍后会介绍一些解决方法
布局修饰符
使用 layout
修饰符可手动控制如何测量和定位元素。通常,自定义 layout
修饰符的常见结构如下:
fun Modifier.customLayoutModifier(...) = Modifier.layout { measurable, constraints ->
...
})
使用 layout
修饰符时,您会获得两个 lambda 参数:
-
measurable
:要测量和放置的子元素 -
constraints
:子元素宽度和高度的最小值和最大值
Google Compose
教程中使用firstBaselineToTop
。对于没有了解过Text
绘制中的FontMetrics
的人而言可能理解起来会有些难度,所以这里的案例我选择较为简单的myPadding
实现
fun Modifier.myPadding(all: Dp,tag:String) =
this.then(layout { measurable, constraints ->
Log.i("Modifier", """
constraints:${constraints}
measurable:${measurable}
Tag=$tag
""".trimIndent())
val placeable = measurable.measure(constraints)
//将dp值转化为px值【四舍五入】
val padding = all.roundToPx()
layout(placeable.width + padding * 2, placeable.height + padding * 2) {
placeable.placeRelative(padding, padding)
}
})
@Composable
fun CustomSimpleLayout() {
Text(
text = "CustomSimpleLayout", modifier = Modifier
.myPadding(10.dp,"1")
.background(Purple200)
//效果图中没有该修饰符
.defaultMinSize(120.dp,60.dp)
.myPadding(10.dp,"2")
)
}
修饰符以串联的方式进行描述,而
layout
返回一个Modifier
,要和之前的Modifier
串联使用需要使用then
修饰符
Logcat打印信息
I/Modifier: constraints:Constraints(minWidth = 0, maxWidth = 1080, minHeight = 0, maxHeight = 2069)
measurable:androidx.compose.ui.node.ModifiedDrawNode@32a3d5a
Tag=1
I/Modifier: constraints:Constraints(minWidth = 330, maxWidth = 1080, minHeight = 165, maxHeight = 2069)
measurable:androidx.compose.ui.node.ModifiedDrawNode@549bb8b
Tag=1
I/Modifier: constraints:Constraints(minWidth = 330, maxWidth = 1080, minHeight = 165, maxHeight = 2069)
measurable:androidx.compose.ui.semantics.SemanticsWrapper@b6468 id: 2 config: SemanticsConfiguration@de1a081{ Text : [CustomSimpleLayout] }
Tag=2
通过打印对象可能比较好理解measurable
和constraints
,首先再将两个参数描述抄过来
-
measurable:Measurable
:要测量和放置的子元素 -
constraints:Constraints
:子元素宽度和高度的最小值和最大值
constraints
也就是控件的限制,从打印对象的描述中可以看出,该参数就是控件size
的范围,可以通过defaultMinSize
对最小宽高进行设置
measurable
只有在最后一个修饰符才会将要测量和放置的子元素加入,因为在此案例中myPadding
修饰符修饰元素及其子元素只有Text
,由此计算得出实际上的控件大小
经过测试只有在最后一个修饰符
measurable
才会是SemanticsConfiguration
,具体的原因要查看布局流程
实际上的控件大小再经过控件size范围限制,最终得出显示的控件大小
val placeable = measurable.measure(constraints)
因为要实现padding
效果,我们需要将宽/高+外边距值*2
于是这样我们就完成了控件布局的第一步:测量大小,然后就是第二步:计算父控件中的相对摆放位置
很显然在padding
效果中x、y的偏差值就是外边距值
最后通过调用 layout(width, height)
方法来指定其大小,在lambda
中设置摆放位置
//指定控件大小
layout(placeable.width + padding * 2, placeable.height + padding * 2) {
//设置摆放位置
placeable.placeRelative(padding, padding)
}
创建自定义
Layout
或LayoutModifier
时,Android Studio 会发出警告,直至系统调用layout
函数如果不调用
placeRelative
,该可组合项将不可见。placeRelative
会根据当前的layoutDirection
自动调整可放置位置
布局可组合项
上面的布局修饰符是针对于单个可组合项的测量和布局方式,但有时我们可能需要的是针对一组可组合项实施操作。为此,您可以使用 Layout
可组合项手动控制如何测量和定位布局的子元素。通常,使用 Layout
的可组合项的常见结构如下所示:
@Composable
fun CustomLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// measure and position children given constraints logic here
}
}
CustomLayout
至少需要 modifier
和 content
参数;这些参数随后会传递到 Layout
在
Layout
(MeasurePolicy
类型)的尾随 lambda 参数
measurables:List<Measurable>
constraints:Constraints
很显然布局中可能不止存在一个子元素,所以这里通过List方式给出各个直接子元素与布局相关信息
而
constraints
是针对修饰的布局而言的,所以只有一个
为了展示 Layout
的实际运用,让我们使用 Layout
实现一个非常基本的 Column
,以便了解该 API。稍后会构建更复杂的内容,以展示 Layout
可组合项的灵活性。
实现MyColumn
布局
自定义 MyColumn
实现类似Column
会垂直布局各个项,首先创建名为 MyColumn
的新可组合项,然后添加 Layout
可组合项的常见结构:
@Composable
fun MyColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
//根据约束逻辑,测量和摆放子元素
}
}
和View系统中写ViewGroup
一样,第一步为测量子元素尺寸。与布局修饰符的工作原理类似,在 measurables
lambda 参数中,您可以通过调用 measurable.measure(constraints)
获取所有可测量的 content
在测量子元素时,还应根据每行的 width
和height
计算得出布局的大小:
@Composable
fun MyColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
var maxWidth = 0
var height = 0
val placeableList = measurables.map { measurable ->
//测量每个元素
measurable.measure(constraints = constraints).also { placeable ->
maxWidth = max(maxWidth, placeable.width)
height += placeable.height
}
}
...
}
}
在
MyColumn
这个案例中,没有必要进一步限制子元素,所以直接通过measurable.measure(constraints)
就可以了如果需要写Grid布局,以2列为例→则需要限制子元素的
maxWidth
为布局maxWidth
的一半
最后,我们通过调用 placeable.placeRelative(x, y)
将子元素放置到界面上。垂直放置子元素,需要跟踪放置子元素的 y
坐标。MyColumn
的最终代码如下所示:
@Composable
fun MyColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
var maxWidth = 0
var height = 0
var yPosition = 0
val placeableList = measurables.map { measurable ->
//测量每个元素
measurable.measure(constraints = constraints).also { placeable ->
maxWidth = max(maxWidth, placeable.width)
height += placeable.height
}
}
layout(maxWidth, height) {
placeableList.forEach { placeable ->
//摆放子元素至界面
placeable.placeRelative(x = 0, y = yPosition)
//记录下一个子元素摆放的y坐标
yPosition += placeable.height
}
}
}
}
使用
@Preview(
group = "2.6"
)
@Composable
fun MyColumnPreview(modifier: Modifier=Modifier) {
MyColumn(modifier = modifier.background(Teal200)) {
//可是我还没找到工作(இωஇ )
Text(text = " /⌒ヽ 不想上班")
Text(text = " く/・〝 ⌒ヽ ")
Text(text = " | 3 (∪ ̄]")
Text(text = " く、・〟 (∩ ̄]")
Text(text = " ̄ ̄ ̄ ̄  ̄ ̄ ̄ ̄")
Text(text = "∧_∧")
Text(text = "(il´‐ω‐)ヘ")
Text(text = "∩,,__⌒ つっ")
}
}
复杂的自定义布局
Layout
的基础知识已介绍完毕。我们来创建一个更复杂的示例,以展示 API 的灵活性。我们要构建自定义的 Material Study Owl
交错网格,如下图中间位置所示:
当然对于这种布局方式,我们可以采用Column
配合Row
的方式完成实现,不过在本案例中选择通过自定义布局的方式完成
如果要让布局可以以各种行数展示,可以使用参数作为想要在布局上展示的行数。由于该信息应该是在调用布局时出现的,因此将其作为参数传递:
@Composable
fun RowGrid(
modifier: Modifier = Modifier,
row: Int = 3,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
//根据约束逻辑,测量和摆放子元素
}
}
首先要做的是测量子元素,同样在RowGrid
中不需要对子元素进行限制。再根据子元素的宽高计算出布局的宽高
布局宽高逻辑:
RowGrid
的宽 = 最长行的宽
RowGrid
的高 = 每一行的高累加
行高 = 行中最高元素的高
fun Constraints.widthRange() = minWidth.rangeTo(maxWidth)
fun Constraints.heightRange() = minHeight.rangeTo(maxHeight)
@Composable
fun RowGrid(
modifier: Modifier = Modifier,
rowNum: Int = 3,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier
//默认支持横向滚动
.horizontalScroll(rememberScrollState())
.background(Purple200),
content = content
) { measurables, constraints ->
//记录每行的宽度
val widthList = MutableList(rowNum) { 0 }
//记录每行的高度
val heightList = MutableList(rowNum) { 0 }
val placeableList = measurables.mapIndexed { index, measurable ->
measurable.measure(constraints = constraints).also { placeable ->
//该元素应该在哪一行
val row = index % rowNum
heightList[row] = max(heightList[row], placeable.height)
widthList[row] = widthList[row] + placeable.width
}
}
//布局的宽 = 最长行的宽,再将值限制在constraints范围中
val width = widthList.maxOrNull()
?.coerceIn(constraints.widthRange())
?: constraints.minWidth
//布局的高 = 每一行的高累加,再将值限制在constraints范围中
val height = heightList
.sumOf { it }
.coerceIn(constraints.heightRange())
layout(width, height) {
...
}
}
}
计算出布局的宽高之后,我们就需要在layout
中通过计算子元素的(x,y),摆放子元素的位置
layout(width, height) {
val rowX = MutableList(rowNum) { 0 }
val rowY = MutableList(rowNum) { 0 }
for (i in 1 until rowNum) {
//计算每行元素的y坐标
rowY[i] = rowY[i - 1] + heightList[i - 1]
}
placeableList.mapIndexed { index, placeable ->
val row = index % rowNum
placeable.placeRelative(rowX[row], rowY[row])
//计算出该行下一个元素的x坐标
rowX[row] = rowX[row] + placeable.width
}
}
使用
val topics = listOf(
"Arts & Crafts", "Beauty", "Books", "Business", "Comics", "Culinary",
"Design", "Fashion", "Film", "History", "Maths", "Music", "People", "Philosophy",
"Religion", "Social sciences", "Technology", "TV", "Writing"
)
@Preview(group = "2.7")
@Composable
fun RowGridPreview(modifier: Modifier = Modifier) {
RowGrid {
topics.forEach {
RowGridItem(
Modifier
.padding(4.dp)
.clip(RoundedCornerShape(4.dp))
.background(Purple200.copy(alpha = .2f)), it
)
}
}
}
@Composable
fun RowGridItem(modifier: Modifier = Modifier, str: String) {
Row(modifier = modifier) {
Image(
painter = painterResource(id = R.drawable.user),
contentDescription = null,
modifier = Modifier.height(48.dp)
)
Text(
text = str, modifier = Modifier
.align(Alignment.CenterVertically)
.padding(horizontal = 2.dp)
)
}
}
如果使用的是互动式预览按钮因为添加了
horizontalScroll(rememberScrollState())
,可以进行横向滚动
或通过点按 Android Studio 运行按钮在设备上运行应用,您会看到如何水平滚动内容。
深入了解布局修饰符
修饰符允许您自定义可组合项的行为。您可以将多个修饰符串联在一起,以组合修饰符。修饰符有多种类型,但在此部分中重点介绍的是 LayoutModifier
,因为它们可以改变界面组件的测量和布局方式。
可组合项对其自己的内容负责,并且父元素不能检查或操纵该内容,除非该可组合项的发布者公开了明确的 API 来执行此操作。
同样,可组合项的修饰符对其他修饰符而言是不透明,修饰符看不到其他修饰符具体操作,只能在其他修饰符修饰的基础上执行自己的操作
分析修饰符
由于 Modifier
和 LayoutModifier
是公共接口,因此您可以创建自己的修饰符。
padding
是一个由实现 LayoutModifier
接口的类提供支持的函数,而且该函数将替换 measure
方法。PaddingModifier
是一个实现 equals()
的标准类,因此可以在重组后比较该修饰符。
下面显示的是有关 padding
如何修改元素的大小和约束条件的源代码:
@Stable
fun Modifier.padding(all: Dp) =
this.then(
PaddingModifier(start = all, top = all, end = all, bottom = all, rtlAware = true)
)
private class PaddingModifier(
val start: Dp = 0.dp,
val top: Dp = 0.dp,
val end: Dp = 0.dp,
val bottom: Dp = 0.dp,
val rtlAware: Boolean,
) : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val horizontal = start.roundToPx() + end.roundToPx()
val vertical = top.roundToPx() + bottom.roundToPx()
val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))
val width = constraints.constrainWidth(placeable.width + horizontal)
val height = constraints.constrainHeight(placeable.height + vertical)
return layout(width, height) {
if (rtlAware) {
placeable.placeRelative(start.roundToPx(), top.roundToPx())
} else {
placeable.place(start.roundToPx(), top.roundToPx())
}
}
}
}
元素的新 width
将是子元素的 width
+ 元素宽度约束条件的起始和结束内边距值
元素的新height
将是子元素的 height
+ 元素高度约束条件的上下内边距值
修饰符顺序
串联修饰符时顺序非常重要,因为这些修饰符会从前到后应用于所修改的可组合项,这意味着左侧修饰符的测量值和布局会影响右侧的修饰符。可组合项的最终大小取决于作为参数传递的所有修饰符
首先,修饰符会从左到右更新约束条件,然后从右到左返回大小。下面我们通过一个示例来更好地了解这一点:
@Preview(
group = "2.8",
showBackground = true,
backgroundColor = 0xFFFFFF,
widthDp = 232,
heightDp = 232
)
@Composable
fun TestModifierPreview() {
Row {
Row(
modifier = Modifier
.background(Teal200)
.size(200.dp)
.padding(16.dp)
) {
Box(modifier = Modifier
.fillMaxSize()
.background(Purple200)) {}
}
}
}
预览中对于在外层的布局通过
size
设置尺寸是无效的,会默认填充整个界面
首先,我们更改背景,了解修饰符对界面有何影响;接下来,将大小限制为 200.dp
width
和 height
,最后应用内边距
由于约束条件在链中是从左到右传播的,因此对要测量的 Row
的内容采用的约束条件为:width
和 height
的最大值和最小值都为 (200-16-16)=168dp
。即Box
的大小为 168x168 dp
因此,在 modifySize
链从右向左运行后, Row
的最终大小为 200x200 dp
如果我们更改修饰符的顺序,先应用内边距,然后再应用大小,则会得到不同的界面:
@Preview(
group = "2.8",
showBackground = true,
backgroundColor = 0xFFFFFF,
widthDp = 232,
heightDp = 232
)
@Composable
fun TestModifierPreview() {
Row {
Row(
modifier = Modifier
.padding(16.dp)
.size(200.dp)
) {
Box(modifier = Modifier
.fillMaxSize()
.background(Purple200)) {}
}
}
}
在这种情况下,Row
和 padding
最初具有的约束条件将被强制转换为 size
约束条件,在该约束条件下进行子元素d 测量。因此,对于最小和最大 width
以及 height
,Box
将被约束为 200dp
。
随着修饰符从右向左修改大小,padding
修饰符会将大小增大为 (200+16+16)x(200+16+16)=232x232
,这也就是 Row
的最终大小
布局方向
可以通过更改 LocalLayoutDirection
CompositionLocal 来更改可组合项的布局方向
@Composable
fun RowGrid(
modifier: Modifier = Modifier,
rowNum: Int = 3,
content: @Composable () -> Unit
) {
//将布局方向修改为从左向右
@Composable
fun RowGrid(
modifier: Modifier = Modifier,
rowNum: Int = 3,
content: @Composable () -> Unit
) {
//将布局方向修改为从左向右
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr){
...
}
}
layoutDirection
是 layout
修饰符或 Layout
可组合项的 LayoutScope
的一部分,可以在layout
和Layout
中获取layoutDirection
不过在使用 layoutDirection
时,应使用 place
放置可组合项而不是placeRelative
val LocalLayoutDirection = staticCompositionLocalOf<LayoutDirection> { noLocalProvidedFor("LocalLayoutDirection") }
enum class LayoutDirection { /** * Horizontal layout direction is from Left to Right. */ Ltr, /** * Horizontal layout direction is from Right to Left. */ Rtl }
约束布局
ConstraintLayout
有助于依据可组合项的相对位置将它们放置到界面上,是使用多个 Row
、Column
和 Box
元素的替代方案。在实现对齐要求比较复杂的较大布局时,ConstraintLayout
很有用。
需要在项目的 build.gradle
文件中添加 Compose ConstraintLayout
依赖项:
dependencies {
implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-rc02"
}
Compose 中的 ConstraintLayout
支持 DSL
:
- 使用
createRefs()
(或createRef()
)创建的引用,ConstraintLayout
中的每个可组合项都需要有与之关联的引用。 - 约束条件是使用
constrainAs
修饰符提供的,该修饰符将引用作为参数,可让您在主体 lambda 中指定其约束条件。 - 约束条件是使用
linkTo
或其他有用的方法指定的。 -
parent
是一个现有的引用,可用于指定对ConstraintLayout
可组合项本身的约束条件。
从一个简单的例子开始:
@Composable
fun ConstraintLayoutStudy(modifier: Modifier = Modifier) {
ConstraintLayout(modifier = modifier) {
val (button, text) = createRefs()
Button(onClick = { /*TODO*/ }, modifier = Modifier.constrainAs(button) {
top.linkTo(parent.top,margin = 12.dp)
}) {
Text(text = "Button")
}
Text("Text", Modifier.constrainAs(text) {
top.linkTo(button.bottom,margin = 12.dp)
})
}
}
此代码使用 16.dp
的外边距来约束 Button
顶部到父项的距离,同样使用 16.dp
的外边距来约束 Text
到 Button
底部的距离
如果希望文本水平居中,可以使用 centerHorizontallyTo
函数将 Text
的 start
和 end
均设置为 parent
的边缘:
@Composable
fun ConstraintLayoutStudy(modifier: Modifier = Modifier) {
ConstraintLayout(modifier = modifier) {
...
Text("Text", Modifier.constrainAs(text) {
top.linkTo(button.bottom,margin = 12.dp)
centerHorizontallyTo(parent)
})
}
}
fun centerHorizontallyTo(other: ConstrainedLayoutReference) { linkTo(other.start, other.end) } fun linkTo( start: ConstraintLayoutBaseScope.VerticalAnchor, end: ConstraintLayoutBaseScope.VerticalAnchor, startMargin: Dp = 0.dp, endMargin: Dp = 0.dp, @FloatRange(from = 0.0, to = 1.0) bias: Float = 0.5f ) { this@ConstrainScope.start.linkTo(start, startMargin) this@ConstrainScope.end.linkTo(end, endMargin) tasks.add { state -> state.constraints(id).horizontalBias(bias) } }
可以看到
centerHorizontallyTo
内部调用了linkTo
函数实现居中
DSL
还支持创建guideline
、barrier
和chain
辅助类用于帮助布局
屏障[Barrier]
有时候我们需要以多个组件做为摆放的基准,可以理解为将多个组件组合看作为一个组件,基于这个组件位置的基础上进行摆放
//限制显示的界面大小
@Preview(group = "2.9",widthDp = 200,heightDp = 120)
@Composable
fun ConstraintLayoutPreview() {
ConstraintLayoutStudy_Barrier()
}
@Composable
fun ConstraintLayoutStudy_Barrier(modifier: Modifier = Modifier) {
ConstraintLayout(modifier = modifier) {
val (button, text, button2,box,box2) = createRefs()
//创建屏障
val startBarrier = createStartBarrier(button, text)
val endBarrier = createEndBarrier(button, text)
val topBarrier = createTopBarrier(button, text)
val bottomBarrier = createBottomBarrier(button, text)
//两个Box用于显示屏障的具体位置与大小
Box(modifier = Modifier.constrainAs(box){
start.linkTo(startBarrier)
top.linkTo(topBarrier)
}.background(Teal200.copy(.5f)).fillMaxSize()){}
Box(modifier = Modifier.constrainAs(box2){
end.linkTo(endBarrier)
bottom.linkTo(bottomBarrier)
}.background(Purple700.copy(.5f)).fillMaxSize()){}
Button(onClick = {}, modifier = Modifier.constrainAs(button) {
top.linkTo(parent.top, margin = 12.dp)
}) {
Text(text = "Button")
}
Text("Text", Modifier.constrainAs(text) {
top.linkTo(button.bottom, margin = 12.dp)
//相对于button水平居中摆放
centerHorizontallyTo(button)
})
Button(onClick = {}, modifier = Modifier.constrainAs(button2) {
start.linkTo(endBarrier)
linkTo(topBarrier, bottomBarrier)
}) {
Text(text = "Button2")
}
}
}
为了布局逻辑简单,将text
改为相对于button
水平居中,然后新建一个button2
使得它相对于button
和text
组合成的组件垂直居中,在其右边摆放
通过Box
背景可以很显然的看出对于text
和button
组成的屏障的大小与位置【背景颜色重叠部分】
由
centerHorizontallyTo
源码可以知道想要实现水平居中效果,只需要调用linkTo
函数传入start
与end
就可以了,同理垂直居中只需要调用linkTo
函数传入top
与bottom
创建
Barrier
还有createAbsoluteLeftBarrier
和createAbsoluteRightBarrier
是用于国际化适配,因为有些语言是从右到左排列的,如阿拉伯语
- 屏障(以及所有其他辅助类)可以在
ConstraintLayout
的正文中创建,但不能在constrainAs
内部创建linkTo
可用Guideline和Barrier进行约束,就像运用引用进行约束一样
准则[Guideline]
相当于为布局中设置了一条看不见的基准线,控件可以根据该基准线位置进行位置摆放
@Composable
fun ConstraintLayoutStudy_Guideline(modifier: Modifier = Modifier) {
ConstraintLayout(modifier = modifier.background(Color.White)) {
val text = createRef()
val guidelineStart = createGuidelineFromStart(.5f)
val guidelineTop = createGuidelineFromTop(.1f)
Text(text = "Text", modifier = Modifier.constrainAs(text) {
start.linkTo(guidelineStart)
top.linkTo(guidelineTop)
})
}
}
可以看到text
摆放到了布局水平位置一半的右边,垂直位置0.1的下方
此案例中只使用了guideline
根据百分比来设置它的位置,其实也可以根据偏移量来设置
//根据左侧距离父布局偏移量来设置 guideline 位置
createGuidelineFromStart(offset: Dp)
//根据左侧距离父布局的百分比来设置 guideline 位置
createGuidelineFromStart(fraction: Float)
//同Barrier一样,在国际化才使用
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
的子元素选择封装其内容所需的大小。例如,这意味着当文本过长时,可以超出界面边界:
@Preview(group = "2.9", widthDp = 400, heightDp = 120)
@Composable
fun ConstraintLayoutPreview() {
ConstraintLayoutStudy_Dimension()
}
@Composable
fun ConstraintLayoutStudy_Dimension(modifier: Modifier = Modifier) {
ConstraintLayout(modifier = modifier.background(Color.White)) {
val text = createRef()
val guidelineStart = createGuidelineFromStart(.5f)
Text(text = "不逼就不努力的,即使逼了也不会努力多久的超级贱人", modifier = Modifier.constrainAs(text) {
linkTo(guidelineStart,parent.end)
})
}
}
选择将text
居中显示在guidelineStart
与父控件的end
,但是text
的长度可以超出这个范围。如果希望让text
限制这个范围中换行显示,就需要更改text
的width
行为
@Composable
fun ConstraintLayoutStudy_Dimension(modifier: Modifier = Modifier) {
ConstraintLayout(modifier = modifier.background(Color.White)) {
val text = createRef()
val guidelineStart = createGuidelineFromStart(.5f)
Text(text = "不逼就不努力的,即使逼了也不会努力多久的超级贱人", modifier = Modifier.constrainAs(text) {
linkTo(guidelineStart,parent.end)
width = Dimension.preferredWrapContent
})
}
}
可用的Dimension
:
preferredWrapContent
:布局大小根据内容设置,同时受到布局约束的限制。比如该例子中text
内容文本长度超过了200dp
,但是因为受到linkTo(guidelineStart,parent.end)
的限制,所以text
的end
最多只能等于parent.end
wrapContent
:Dimension
的默认值,布局大小根据内容设置,不受布局约束限制fillToConstraints
:布局大小就是布局约束限制的空间preferredValue
:布局大小是固定值,同时受到布局约束的限制value
:布局大小是固定的值,不受布局约束的限制
此外,
Dimension
还可组合设置布局大小例如:
width = Dimension.preferredWrapContent.atLeast(100.dp)
可设置最小布局大小,同样还有atMost()
可设置最大布局大小等。
链[Chain]
Chain
作用为将一系列子元素按照顺序打包成一行或一列,创建Chain
的api只有两个:
-
createHorizontalChain
:创建横向的链 -
createVerticalChain
:创建纵向的链
不过官方将这个 api 标记为可以改进的状态,可能后续会发生变化
// TODO(popam, b/157783937): this API should be improved fun createHorizontalChain( vararg elements: ConstrainedLayoutReference, chainStyle: ChainStyle = ChainStyle.Spread ) // TODO(popam, b/157783937): this API should be improved fun createVerticalChain( vararg elements: ConstrainedLayoutReference, chainStyle: ChainStyle = ChainStyle.Spread )
参数:
-
elements
:需要打包在一起的所有子元素引用 -
chainStyle
:链的类型,目前有三种类型-
Spread
:所有子元素平均分布在父布局空间中,默认类型 -
SpreadInside
:第一个和最后一个分布在链条的两端,其余子元素平均分布剩下的空 -
Packed
:所有子元素打包在一起,并放在链条的中间
-
@Preview(group = "2.9", widthDp = 240, heightDp = 120)
@Composable
fun ConstraintLayoutPreview() {
ConstraintLayoutStudy_Chain()
}
@Composable
fun ConstraintLayoutStudy_Chain(modifier: Modifier = Modifier) {
ConstraintLayout(modifier = modifier.background(Color.White)) {
val (box1, box2, box3) = createRefs()
val boxSize = 60.dp
createHorizontalChain(box1, box2, box3, chainStyle = ChainStyle.Spread)
Box(modifier = modifier
.size(boxSize)
.background(Color.Red)
.constrainAs(box1) {}) {}
Box(modifier = modifier
.size(boxSize)
.background(Color.Green)
.constrainAs(box2) {}) {}
Box(modifier = modifier
.size(boxSize)
.background(Color.Blue)
.constrainAs(box3) {}) {}
}
}
解耦[ConstraintSet]
以上的示例都是通过内嵌的方式指定约定条件,不过在某些情况下我们可能需要更改约定条件,此时就需要将约束条件和布局进行分离解耦。例如:横竖屏切换情况下约束条件的变化
对于这些情况,可以通过使用 ConstraintSet
进行解耦分离,具体步骤如下:
- 将
ConstraintSet
作为参数传递给ConstraintLayout
- 使用
layoutId
修饰符将在ConstraintSet
中创建的引用分配给可组合项
@Composable
fun ConstraintLayoutStudy_Decoupled(modifier: Modifier = Modifier) {
BoxWithConstraints {
val constraints = if (maxWidth < maxHeight) {
decoupledConstraints(margin = 16.dp) // Portrait constraints
} else {
decoupledConstraints(margin = 32.dp) // Landscape constraints
}
ConstraintLayout(modifier = modifier, constraintSet = constraints) {
Button(onClick = { /*TODO*/ }, modifier = Modifier.layoutId("button")) {
Text(text = "Button")
}
Text("Text", Modifier.layoutId("text"))
}
}
}
fun decoupledConstraints(margin: Dp) = ConstraintSet {
val button = createRefFor("button")
val text = createRefFor("text")
constrain(button) {
top.linkTo(parent.top, margin = margin)
}
constrain(text) {
top.linkTo(button.bottom, margin)
}
}
其中layoutId
与createRefFor
中的只需要一一对应,createRefFor
参数为Any
,可以不用拘泥于String
BoxWithConstraints
中记录测量数据,可以通过宽高比确定横竖屏
使用
ConstraintSet
方式进行解耦时,ConstraintLayout
布局内部就不能通过createRefs
或createRef
方式创建引用@Composable inline fun ConstraintLayout( constraintSet: ConstraintSet, modifier: Modifier = Modifier, optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD, animateChanges: Boolean = false, animationSpec: AnimationSpec<Float> = tween<Float>(), noinline finishedAnimationListener: (() -> Unit)? = null, noinline content: @Composable () -> Unit ) @Composable inline fun ConstraintLayout( modifier: Modifier = Modifier, optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD, crossinline content: @Composable ConstraintLayoutScope.() -> Unit )
从源码中我们可以找到答案,两个方法中
lambda
所在环境不同
约束布局参考文献
https://blog.csdn.net/lbs458499563/article/details/120386772
固有特性
Compose 有一项规则,即,子元素只能测量一次,测量两次就会引发运行时异常。但是,有时需要先收集一些关于子元素的信息,然后再对其进行测量。
可以借助固有特性,您可以先查询子元素,然后再进行实际测量
对于可组合项,可以查询intrinsicWidth
或intrinsicHeight
:
-
(min|max)IntrinsicWidth
:给定高度,可以正确绘制内容的最小/最大宽度 -
(min|max)IntrinsicHeight
:给定宽度,可以正确绘制内容的最小/最大高度
实际运用
假设我们需要创建一个可组合项,该可组合项在屏幕上显示两个用分隔线隔开的文本
我们可以将两个 Text
放在同一 Row
中,并在其中最大程度地扩展,另外在中间放置一个 Divider
。我们需要将分隔线的高度设置为与最高的 Text
相同,粗细设置为 width = 1.dp
@Preview(group = "2.10", widthDp = 240, heightDp = 120)
@Composable
fun IntrinsicWidthPreview() {
Scaffold {
IntrinsicStudy()
}
}
@Composable
fun IntrinsicStudy(modifier: Modifier = Modifier) {
Row(modifier = modifier
.background(Purple200)
.height(IntrinsicSize.Min)) {
Text(text = "Left", modifier = Modifier.weight(1f), textAlign = TextAlign.Start)
Divider(
modifier = Modifier
.width(1.dp)
.fillMaxHeight(),
color = Color.Black
)
Text(text = "Right", modifier = Modifier.weight(1f), textAlign = TextAlign.End)
}
}
可以看到分隔线扩展到整个界面,这并不是我们想要的效果,分割线应该和Text
的高度一致才对
之所以出现这种情况,是因为 Row
会逐个测量每个子元素,并且 Text
的高度不能用于限制 Divider
。我们希望 Divider
以一个给定的高度来填充可用空间。为此,我们可以使用 height(IntrinsicSize.Min)
修饰符
height(IntrinsicSize.Min)
可将其子元素的高度强行调整为最小固有高度。由于该修饰符具有递归性,因此它将查询 Row
及其子元素的 minIntrinsicHeight
@Composable
fun IntrinsicStudy(modifier: Modifier = Modifier) {
Row(modifier = modifier
.background(Purple200)
.height(IntrinsicSize.Min)) {
Text(text = "Left", modifier = Modifier.weight(1f), textAlign = TextAlign.Start)
Divider(
modifier = Modifier
.width(1.dp)
.fillMaxHeight(),
color = Color.Black
)
Text(text = "Right", modifier = Modifier.weight(1f), textAlign = TextAlign.End)
}
}
行的 minIntrinsicHeight
将作为其子元素的最大 minIntrinsicHeight
。分隔线的 minIntrinsicHeight
为 0,因为如果没有给出约束条件,它不会占用任何空间。因此,Row 的 height
约束条件将为 Text
的最大 minIntrinsicHeight
,而 Divider
会将其 height
扩展为 Row 给定的 height
约束条件
然而这里使用
height(IntrinsicSize.Max)
效果一致,这里官网教程看的比较迷,之后在琢磨琢磨(@_@;)