Jeptpack Compose 官网教程学习笔记(二)布局

布局

主要学习内容

  • 如何使用 Material 组件可组合项
  • 什么是修饰符以及如何在布局中使用它们
  • 如何创建自定义布局
  • 何时可能需要固有特性

修饰符

借助Modifier,可以修饰可组合项。您可以更改其行为、外观,添加无障碍功能标签等信息,处理用户输入,甚至添加高级互动(例如使某些元素可点击、可滚动、可拖动或可缩放)。Modifier是标准的 Kotlin 对象。您可以将它们分配给变量并重复使用,也可以将多个修饰符逐一串联起来,以组合这些修饰符。

下面我们通过实现下方的个人介绍布局进行学习


d2c39f3c2416c321.png

首先实现最基础的布局

@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)
            )
        }
    }
}
image-20220509111209823.png

通过包含文本的 Column 上使用 Modifier.padding,从而在可组合项的 start 上添加一些空间,用以分隔图像和文本

某些布局提供了仅适用于它们本身及其布局特性的修饰符。例如,Row 中的可组合项可以访问适用的修饰符(来自 Row 内容的 RowScope 接收者),例如 weightalign。这种作用域限制具备类型安全性,因此您不可能会意外使用在其他布局中不适用的修饰符(例如,weightBox 中就不适用),系统会将其视为编译时错误加以阻止

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)) {
    ...
    }
}
image-20220509112817914.png

可以看到实现的效果与想象的有些不一样,padding不应该是内边距吗?怎么这里padding设置的是外边距,因为 padding 是在 background 修饰符前面应用的

如果我们在 background 后面应用 padding 修饰符实现效果才是内边距

image-20220509113116014.png

串联修饰符相当于是在前一个修饰符的基础上进行操作,所以在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)) {
    ...
}
image-20220509114407857.png

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
)
Button 槽位

命名为 content 的 lambda 是最后一个参数。这样,我们就能使用尾随 lambda 语法,以结构化方式将内容插入到 Button 中。

在更复杂的组件(如顶部应用栏)中,Compose 会大量使用槽位

在构建自己的可组合项时,您可以使用槽位 API 模式提高它们的可重用性

Material 组件

Compose 附带内置的 Material 组件可组合项,我们可以用它们创建应用。最高级别的可组合项是 Scaffold

Scaffold

可以使用 Scaffold 实现具有基本 Material Design 布局结构的界面。Scaffold 可以为最常见的顶层 Material 组件(例如 TopAppBarBottomAppBarFloatingActionButtonDrawer)提供槽位。使用 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中存在许多槽位topBarbottomBarsnackbarHost

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 模式,我们希望 TopAppBartitle 槽位包含一个带有界面标题的 Text

@Composable
fun TopBarContent() {
    TopAppBar(
        title = {
            Text(text = "MaterialDesign控件")
        }
    )
}
image-20220509140812920.png

顶部应用栏通常包含一些操作项。在本示例中,我们将添加一个收藏夹按钮。当您觉得自己已学会一些内容时,可以点按该按钮。Compose 还带有一些您可以使用的预定义 Material 图标,例如关闭、收藏夹和菜单图标。

顶部应用栏中的操作项槽位为 actions 参数,该参数在内部使用 Row,因此系统会水平放置多个操作。如需使用某个预定义图标,可结合使用 IconButton 可组合项和其中的 Icon

@Composable
fun TopBarContent() {
    TopAppBar(
        title = {
            Text(text = "MaterialDesign控件")
        },
        actions = {
            IconButton(onClick = { /* doSomething() */ }) {
                Icon(Icons.Filled.Favorite, contentDescription = null)
            }
        }
    )
}
image-20220509141128138.png

放置修饰符

每当创建新的可组合项时,提高可组合项可重用性的一种最佳做法是使用默认为 Modifiermodifier 参数

BodyContent 可组合项已经接受一个修饰符作为参数。如果要为 BodyContent 再添加一些内边距,应该在什么位置放置 padding 修饰符?

  1. 将修饰符应用于可组合项中唯一的直接子元素,以便所有对 BodyContent 的调用都会应用额外的内边距:
@Composable
fun BodyContent(modifier: Modifier = Modifier) {
    Column(modifier = modifier.padding(8.dp)) {
        Text(text = "Hi Scaffold!")
        Text(text = "Here is BodyContent")
    }
}
  1. 在调用可视需要添加额外内边距的可组合项时,应用修饰符:
@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 可让您使用 ColumnRow 可组合项轻松实现此模式,但它还提供了仅应用于编写和布局当前可见项的延迟列表【LazyColumnLazyRow

@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 提高了这些像小数据块一样的可组合项的可重用性,您可以组合各种内置可组合项(例如 ColumnRowBox),充分满足某些自定义布局的需求

不过,您可能需要为应用构建一些独特内容,构建时需要手动测量和布置子元素。对此,可以使用 Layout可组合项。实际上,ColumnRow 等所有较高级别的布局都是基于此构建的

相当于View系统中重写ViewGroup编写onMeasureonLayout函数,不过在Compose中只需要编写Layout可组合函数即可

其实是将测量和摆放在Layout中一起完成了而已,不过在测量上比View体系方便,不需要像View体系中手动计算margin值

Compose 中的布局原则

某些可组合函数在被调用后会发出一部分界面,这部分界面会添加到将呈现到界面上的界面树中。每次发送(或每个元素)都有一个父元素,还可能有多个子元素。此外,它在父元素中具有位置 (x, y) 和大小,即 widthheight

即每个可组合函数创建一个控件都会记录其的宽和高、在父控件中的位置(x,y)

系统会要求元素使用其应满足的约束条件进行自我测量。约束条件可限制元素的最小和最大 widthheight。如果某个元素有子元素,它可能会测量每个元素,以帮助确定它自己的大小。一旦某个元素报告了自己的大小,就有机会相对于自身放置它的子元素。创建自定义布局时,我们将对此做进一步解释说明

即让最底层的控件先进行测量得出宽高,将数值传给父控件,然后其父控件根据子控件的宽高计算得出自己的宽高再交给父控件的父控件,一层一层套娃向上传递

Compose 界面不允许多遍测量。这意味着,布局元素不能为了尝试不同的测量配置而多次测量任何子元素。单遍测量对性能有利,使 Compose 能够高效地处理较深的界面树。如果某个布局元素测量了它的子元素两次,而该子元素又测量了它的一个子元素两次,依此类推,那么一次尝试布置整个界面就不得不做大量的工作,这使得很难让应用保持良好的性能。不过,有时除了子元素的单遍测量告知您的信息之外,您确实还需额外的信息 - 对于这些情况,我们稍后会介绍一些解决方法

布局修饰符

使用 layout 修饰符可手动控制如何测量和定位元素。通常,自定义 layout 修饰符的常见结构如下:

fun Modifier.customLayoutModifier(...) = Modifier.layout { measurable, constraints ->
  ...
})

使用 layout 修饰符时,您会获得两个 lambda 参数:

  • measurable:要测量和放置的子元素
  • constraints:子元素宽度和高度的最小值和最大值

Google Compose教程中使用firstBaselineToTop。对于没有了解过Text绘制中的FontMetrics的人而言可能理解起来会有些难度,所以这里的案例我选择较为简单的myPadding实现

image-20220509194935078.png

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

通过打印对象可能比较好理解measurableconstraints,首先再将两个参数描述抄过来

  • 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)
}

创建自定义 LayoutLayoutModifier 时,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 至少需要 modifiercontent 参数;这些参数随后会传递到 Layout

LayoutMeasurePolicy 类型)的尾随 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

在测量子元素时,还应根据每行的 widthheight计算得出布局的大小:

@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 = "∩,,__⌒  つっ")
    }
}
image-20220509220056501.png

复杂的自定义布局

Layout 的基础知识已介绍完毕。我们来创建一个更复杂的示例,以展示 API 的灵活性。我们要构建自定义的 Material Study Owl 交错网格,如下图中间位置所示:

image.png

当然对于这种布局方式,我们可以采用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)
        )
    }
}
image-20220510112954749.png

因为添加了horizontalScroll(rememberScrollState()),可以进行横向滚动

如果使用的是互动式预览按钮
image.png

或通过点按 Android Studio 运行按钮在设备上运行应用,您会看到如何水平滚动内容。

深入了解布局修饰符

修饰符允许您自定义可组合项的行为。您可以将多个修饰符串联在一起,以组合修饰符。修饰符有多种类型,但在此部分中重点介绍的是 LayoutModifier,因为它们可以改变界面组件的测量和布局方式。

可组合项对其自己的内容负责,并且父元素不能检查或操纵该内容,除非该可组合项的发布者公开了明确的 API 来执行此操作。

同样,可组合项的修饰符对其他修饰符而言是不透明,修饰符看不到其他修饰符具体操作,只能在其他修饰符修饰的基础上执行自己的操作

分析修饰符

由于 ModifierLayoutModifier 是公共接口,因此您可以创建自己的修饰符。

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 widthheight,最后应用内边距

由于约束条件在链中是从左到右传播的,因此对要测量的 Row 的内容采用的约束条件为:widthheight 的最大值和最小值都为 (200-16-16)=168dp 。即Box 的大小为 168x168 dp

因此,在 modifySize 链从右向左运行后, Row 的最终大小为 200x200 dp

image-20220510143216022.png

如果我们更改修饰符的顺序,先应用内边距,然后再应用大小,则会得到不同的界面:

@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)) {}
        }
    }
}

在这种情况下,Rowpadding 最初具有的约束条件将被强制转换为 size 约束条件,在该约束条件下进行子元素d 测量。因此,对于最小和最大 width 以及 heightBox 将被约束为 200dp

随着修饰符从右向左修改大小,padding 修饰符会将大小增大为 (200+16+16)x(200+16+16)=232x232,这也就是 Row 的最终大小

image-20220510143325954.png

布局方向

可以通过更改 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){
        ...
    }    
}

layoutDirectionlayout 修饰符或 Layout 可组合项的 LayoutScope 的一部分,可以在layoutLayout中获取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 有助于依据可组合项的相对位置将它们放置到界面上,是使用多个 RowColumnBox 元素的替代方案。在实现对齐要求比较复杂的较大布局时,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)
        })
    }
}
image-20220510152254347.png

此代码使用 16.dp 的外边距来约束 Button 顶部到父项的距离,同样使用 16.dp 的外边距来约束 TextButton 底部的距离

如果希望文本水平居中,可以使用 centerHorizontallyTo 函数将 Textstartend 均设置为 parent 的边缘:

@Composable
fun ConstraintLayoutStudy(modifier: Modifier = Modifier) {
    ConstraintLayout(modifier = modifier) {
        ...
        Text("Text", Modifier.constrainAs(text) {
            top.linkTo(button.bottom,margin = 12.dp)
            centerHorizontallyTo(parent)
        })
    }
}
image-20220510154818035.png
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还支持创建guidelinebarrierchain辅助类用于帮助布局

屏障[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")
        }
    }
}
image-20220510164256560.png

为了布局逻辑简单,将text改为相对于button水平居中,然后新建一个button2使得它相对于buttontext组合成的组件垂直居中,在其右边摆放

通过Box背景可以很显然的看出对于textbutton组成的屏障的大小与位置【背景颜色重叠部分】

centerHorizontallyTo源码可以知道想要实现水平居中效果,只需要调用linkTo函数传入startend就可以了,同理垂直居中只需要调用linkTo函数传入topbottom

创建Barrier还有createAbsoluteLeftBarriercreateAbsoluteRightBarrier是用于国际化适配,因为有些语言是从右到左排列的,如阿拉伯语

  • 屏障(以及所有其他辅助类)可以在 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)
        })
    }
}
image-20220510170722457.png

可以看到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)
        })
    }
}
image-20220512094801581.png

选择将text居中显示在guidelineStart与父控件的end,但是text的长度可以超出这个范围。如果希望让text限制这个范围中换行显示,就需要更改textwidth行为

@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
        })
    }
}
image-20220512095319566.png

可用的Dimension

  • preferredWrapContent:布局大小根据内容设置,同时受到布局约束的限制。比如该例子中text内容文本长度超过了200dp,但是因为受到linkTo(guidelineStart,parent.end)的限制,所以textend最多只能等于parent.end

  • wrapContentDimension的默认值,布局大小根据内容设置,不受布局约束限制

  • 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) {}) {}
    }
}
`chainStyle `为 `ChainStyle.Spread` 的效果
`chainStyle `为 `ChainStyle.SpreadInside` 的效果
`chainStyle `为 `ChainStyle.Packed` 的效果

解耦[ConstraintSet]

以上的示例都是通过内嵌的方式指定约定条件,不过在某些情况下我们可能需要更改约定条件,此时就需要将约束条件和布局进行分离解耦。例如:横竖屏切换情况下约束条件的变化

对于这些情况,可以通过使用 ConstraintSet进行解耦分离,具体步骤如下:

  1. ConstraintSet 作为参数传递给 ConstraintLayout
  2. 使用 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)
    }
}

其中layoutIdcreateRefFor中的只需要一一对应,createRefFor参数为Any,可以不用拘泥于String

BoxWithConstraints中记录测量数据,可以通过宽高比确定横竖屏

使用ConstraintSet方式进行解耦时,ConstraintLayout布局内部就不能通过createRefscreateRef方式创建引用

@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 有一项规则,即,子元素只能测量一次,测量两次就会引发运行时异常。但是,有时需要先收集一些关于子元素的信息,然后再对其进行测量。

可以借助固有特性,您可以先查询子元素,然后再进行实际测量

对于可组合项,可以查询intrinsicWidthintrinsicHeight

  • (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)
    }
}
image-20220512112204666.png

可以看到分隔线扩展到整个界面,这并不是我们想要的效果,分割线应该和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)
    }
}
image-20220512112302262.png

行的 minIntrinsicHeight 将作为其子元素的最大 minIntrinsicHeight。分隔线的 minIntrinsicHeight 为 0,因为如果没有给出约束条件,它不会占用任何空间。因此,Row 的 height 约束条件将为 Text 的最大 minIntrinsicHeight,而 Divider 会将其 height 扩展为 Row 给定的 height 约束条件

然而这里使用height(IntrinsicSize.Max)效果一致,这里官网教程看的比较迷,之后在琢磨琢磨(@_@;)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容