Android Compose学习 -- 布局学习

1,849 阅读11分钟

这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战

这篇学习笔记住主要是根据官方文档学习Android Compose中的布局,通过学习这边笔记,可以大概了解Compose中Row,ColumnBox三种布局,同时还会学习到如何设置布局的背景颜色,大小等属性。(Compose 布局基础知识  |  Jetpack Compose  |  Android Developers)点击此链接可以直接查看官方文档,下面进入正文:

概述

这篇学习笔记主要是学习Compose中布局相关的只是,主要内容都是来自官方文档,可以直接点击上面的链接访问官方文档。

目标

Compose中的布局系统主要为了实现两个目标:

  1. 实现高性能。(在普通的Android的View体系中,我们都是应该尽量避免布局的多层嵌套,因为多层嵌套会导致布局的多次绘制,从而影响性能,但是Compose中布局的测量方式称为固有特性测量,通过避免多次测量布局子级实现高性能。)这里是基本的概念,具体怎么做还需要后面进行学习。

  2. 让开发者能够轻松地编写自定义布局。(在普通的Android的View体系中,自定义布局是比较繁琐的,一方面是要考虑measure的过程,另一方面是要考虑layout的过程。)同样是基本概念,需要后面学习之后进行对比。

可组合函数

可组合函数是Compose的基本构建块,返回Unit的函数,这种函数用于描述界面中的某一部分。这种函数可以接受一些输入,然后根据这些输入的内容生成显示在屏幕上的信息。

一个可组合函数可能会发出多个界面元素,如果我们没有对这些元素指定一个恰当的布局,那么最终呈现的效果可能不是我们想要的那样。下面的代码演示了在一个可组合函数中发出两个文本,最终呈现的效果可能不是我们想要的那样:

    @Composable
    private fun noLayout(){
        Text(text = "the first text")
        Text(text = "the second test")
    }

上面的可组合函数中由于没有提供如何排列这两个元素的布局,因此最终的结果是这两个Text将会堆叠在一起,类似于FrameLayout的效果:

不指定布局的元素排列方式

垂直排列元素(Column)

使用Column可以将多个元素垂直地放置在屏幕上,类似于LinearLayoutoritation:vertical的效果。

    @Composable
    private fun columnLayout(){
        Column() {
            Text(text = "the first text",fontSize = 20.sp,color = Color.Black)
            Text(text = "the second test",fontSize = 16.sp,color = Color.Red)
        }
    }

上面的代码中在两个Text的外部使用了Column进行包裹,下面是使用Column的效果:

使用Column的效果

水平排列元素(Row)

使用Row可以将其中的子项水平地放置在屏幕上,类似于LinearLayout中的oritation:horitation的效果:

    @Composable
    private fun rowLayout(){
        Row() {
            Text(text = "the first text",fontSize = 20.sp,color = Color.Black)
            Text(text = "the second test",fontSize = 16.sp,color = Color.Red)
        }
    }

效果如下:

使用Row的效果

层叠排列元素(Box)

使用Box可以将一个元素放置在另一个元素之上,如下所示:

    @Composable
    private fun boxLayout(){
        Box() {
            Text(text = "the first text",fontSize =30.sp,color = Color.Black)
            Text(text = "the second test",fontSize = 16.sp,color = Color.Red)
        }
    }

效果如下:

使用Box的效果

虽然上面的效果看起来和不使用任何的效果很相似,但是通过使用Box可以指定一些额外的参数,这是不使用任何布局达不到的效果,如下面的代码指定了子项的对齐方式:

    @Composable
    private fun boxLayout(){
        Box(contentAlignment = Alignment.Center) {
            Text(text = "the first text",fontSize = 30.sp,color = Color.Black)
            Text(text = "the second test",fontSize = 12.sp,color = Color.Red)
        }
    }

效果如下:

指定Box的子项的对齐方式

通常情况下使用上面的这三个构建块基本就可以满足需求,通过可组合函数,配合这些布局便可以实现相应的需求,同时由于不需要考虑多重绘制的问题,我们可以任意组合这些布局而基本不需要考虑性能问题。

完善参数

上面我们演示了在Box中设置参数来改变子项的位置信息,在ColumnRow中我们也可以指定相应的参数来达到不同的效果。通过指定horizontalArrangementverticalAlignment参数(对于Row来说)或者指定verticalArrangementhorizontalAlignment参数(对于Column)来说,可以指定其子项的位置。

  1. 下面的代码指定了Column中子项在垂直方向上处于底部,在水平方向上居中的效果:
    @Composable
    private fun columnLayoutChild(){
        Column(
            verticalArrangement = Arrangement.Bottom,
            horizontalAlignment = Alignment.CenterHorizontally,
            modifier = Modifier.fillMaxSize()
        ) {
            Text(text = "the first text",fontSize = 30.sp,color = Color.Black)
            Text(text = "the second test",fontSize = 12.sp,color = Color.Red)
        }
    }

由于Column默认是按照子项的大小来调整自身的大小,因此这里通过modifer参数来指定Column的大小为充满其父容器,否则verticalArrangement = Arrangement.Bottom则没有效果。

指定Column子项的位置

  1. 下面的代码指定了Row中子项的位置:
    @Composable
    private fun rowLayoutChild(){
        Row(
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.End,
            modifier = Modifier.fillMaxSize()
        ) {
            Text(text = "the first text",fontSize = 30.sp,color = Color.Black)
            Text(text = "the second test",fontSize = 12.sp,color = Color.Red)
        }
    }

在上面的代码中,首先指定了Row的大小为布满其父容器,然后指定了Row中子项在水平方向上处于最右边,在垂直方向上居中,效果如下:

指定Row子项的位置

  1. 下面是我们熟悉的在RowColumn指定weight属性,如下所示:
    @Composable
    private fun columnWeightChild() {
        Column(modifier = Modifier.fillMaxSize()) {
            Text(
                text = "the first text",
                fontSize = 30.sp,
                color = Color.Black,
                modifier = Modifier.weight(1f)
                    .background(color = Color.LightGray)
            )
            Text(
                text = "the second test",
                fontSize = 12.sp,
                color = Color.Red,
                modifier = Modifier.weight(1f)
                    .background(color = Color.DarkGray)
            )
        }
    }

效果如下:

指定Column中的weight

修饰符

在上面完善参数的部分,我们已经认识了一些属性,其实就和我们在xml布局中定义属性是一样的,我们需要添加更多的效果,则需要更多的属性,下面将会学习系统为我们提供的更多的属性,也称作修饰符。

修饰符是标准的Kotlin对象,可以通过调用某个Modifier类函数来创建修饰符,也可以将多个修饰符函数连接起来形成组合效果。

size指定大小

size修饰符我们已经在上面使用到了,通过这个修饰符我们可以指定某一项的大小,如下所示:

    @Composable
    private fun sizeColumn(){
        Column(
            modifier = Modifier
                .size(width = 200.dp, height = 200.dp)
                .background(color = Color.Red),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text(text = "宽度和高度均为200dp",
                color = Color.White
                )
        }
    }

在上面的代码中,我们就给Column指定了大小,宽度和高度均为200dp,另外我们还指定了颜色,为了方便查看效果。可以看到sizebackground属性均为Modifier类函数,这两个函数可以组合使用来达到我们想要的效果。下面是运行结果:

指定Column的大小

上面演示了指定Column大小的情况。默认情况下,子项应该遵从父项的约束,也就是子项的大小不能超过父项的大小。默认情况下,子项如果超过父项的大小,则不会生效,如下所示:

    @Composable
    private fun bigParentSize() {
        Box(
            modifier = Modifier
                .size(300.dp, 600.dp)
                .background(color = Color.LightGray),
            contentAlignment = Alignment.Center
        ) {
            Column(
                modifier = Modifier
                    .size(200.dp)
                    .background(color = Color.DarkGray),
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Text(
                    text = "子项比父项大",
                    color = Color.Red,
                    modifier = Modifier
                        .size(width = 50.dp, height = 300.dp)
                        .background(color = Color.Green)
                )
            }
        }
    }

在上面的代码中,我们指定了Column的宽度和高度均为200dp,其中的子项Text的高度为300dp,比父项要大。理论上我们应该看不到Text里面的内容,或者只能看到一部分,因为有一部分内容在父项的上面,但是其实我们仍然能够完整看到Text中的内容,因为此时子项的大小受到父项的约束,子项本身的高度不能超过父项的高度。效果如下:

子项大小受到父项的约束

requiredSize突破父项的大小限制

如果我们就是不想要父项的约束,保持子项的大小,那么我们在设置大小的时候应该使用requiredSize(),使用此方法指定的大小不会受到父项的约束:

    @Composable
    private fun ignoreParentSize() {
        Box(
            modifier = Modifier
                .size(300.dp, 600.dp)
                .background(color = Color.LightGray),
            contentAlignment = Alignment.Center
        ) {
            Column(
                modifier = Modifier
                    .size(200.dp)
                    .background(color = Color.Blue),
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.Bottom
            ) {
                Text(
                    text = "子项比父项还要大很多哦",
                    color = Color.White,
                    fontSize = 40.sp,
                    modifier = Modifier
                        .requiredSize(width = 80.dp, height = 500.dp)
                        .background(color = Color.Green)
                )
            }
        }
    }

在上面的代码中,我们仍然设置了子项的高度超过了父项的高度,并且为了查看最终的效果,我们设置了Text的文字大小,以及在外部设置了一个更大的父项Box,最终的效果如下:

使用requiredSize突破父项的大小限制

wrapContentSize设置子项对齐方式

在上面的代码中,我们将子项的大小设置地超过了父项的大小,最后发现由于文字默认从上到下显示,所以上半边的文字会看不到,只能看到下半边的文字,那如果我们希望能看到上半边的文字呢?则应该使用wrapContentSize属性,如下所示:

    @Composable
    private fun ignoreParentSize() {
        Box(
            modifier = Modifier
                .size(300.dp, 600.dp)
                .background(color = Color.LightGray),
            contentAlignment = Alignment.Center
        ) {
            Column(
                modifier = Modifier
                    .size(200.dp)
                    .background(color = Color.Blue),
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.Bottom
            ) {
                Text(
                    text = "子项比父项还要大很多哦",
                    color = Color.White,
                    fontSize = 40.sp,
                    modifier = Modifier
                        .requiredSize(width = 80.dp, height = 500.dp)
                        .background(color = Color.Green)
                        .wrapContentSize(align = Alignment.BottomCenter)
                )
            }
        }
    }

我们仍然使用之前的代码,只是在子项Text中添加了wrapContentSize属性,并指定了其中的align为底部居中,这样就尽可能地让上半边的文字显示出来,效果如下:

使用wrapContentSize属性指定子项对齐方式

需要注意的是:上面的wrapContentSize属性只能指定子项自身的对齐方式,上面是由于文字较少,指定底部居中对齐以后,似乎文字的上半部分能显示出来了,但是其实并不是,当文字多了以后还是看不到上半部分,如下所示:

wrapContentSize的局限性

可以看到,当文字变多了以后只能看到中间部分的文字,上下的文字都看不到了。

另外需要注意的是:当文字的数量较少时,可以使用wrapContentSizealign属性指定内部文字的对齐方式,但是当文字超过子项的高度的时候,这个参数也不会生效。如下所示,下面两个图片均是指定了align = Alignment.Center的效果:

文字较少时使文本居中

文字较多时使文本居中

另外,对于文字本身如何对齐的文字则应该使用textAlign属性去设置。

另外,如果只是想设置宽度或者高度,可以使用width(),height(),requiredWidth(),requiredHeight()等方法。

如果我们仅仅需要在宽度或者高度上填满父项,则可以使用fillMaxWidth(),fillMaxHeight()fillMaxSize()方法,如下所示:

    @Composable
    private fun fillParent() {
        Column() {

            Box(
                modifier = Modifier
                    .size(200.dp, 100.dp)
                    .background(color = Color.Red),
                contentAlignment = Alignment.Center
            ) {
                Text(
                    text = "宽度占满父项", color = Color.White, modifier = Modifier
                        .fillMaxWidth()
                        .background(color = Color.Gray)
                )
            }

            Spacer(modifier = Modifier.height(10.dp))

            Box(
                modifier = Modifier
                    .size(200.dp, 100.dp)
                    .background(color = Color.Yellow),
                contentAlignment = Alignment.Center
            ) {
                Text(
                    text = "宽度占满父项",
                    color = Color.White,
                    modifier = Modifier
                        .fillMaxHeight()
                        .background(color = Color.Gray)
                )
            }

            Spacer(modifier = Modifier.height(10.dp))

            Box(
                modifier = Modifier
                    .size(200.dp, 100.dp)
                    .background(color = Color.Blue),
                contentAlignment = Alignment.Center
            ) {
                Text(
                    text = "全部占满父项",
                    color = Color.White,
                    modifier = Modifier
                        .fillMaxSize()
                        .background(color = Color.Gray)
                )
            }
        }
    }

运行效果如下:

占满父项的情况

paddingFromBaseline基于文字基线设置距离

对于文字来说,如果想要基于文字基线来设置距离,则可以使用paddingFromBaseline

    @Composable
    private fun distanceBaseline() {
        Row() {
            Box (
                modifier = Modifier.background(color = Color.Gray)
            ){
                Text(
                    text = "基于文字基线设置距离",
                    modifier = Modifier
                        .paddingFromBaseline(
                            top = 100.dp,
                            bottom = 50.dp,
                        )
                        .background(color = Color.Red),
                    color = Color.White
                )

            }
            
            Spacer(modifier = Modifier.width(20.dp))

            Box (
                modifier = Modifier.background(color = Color.Gray)
            ){
                Text(
                    text = "设置Padding",
                    modifier = Modifier
                        .padding(
                            top = 100.dp,
                            bottom = 50.dp,
                        )
                        .background(color = Color.Red),
                    color = Color.White
                )

            }
        }

    }

基于文字基线设置距离

offset设置偏移量

通过设置offset可以设置相对于原始位置放置布局,偏移量可以是正数,也可以是负数,如下所示:

    @Composable
    private fun paddingAndOffset(){
        Column {
            Text(text = "这是一段文字",
                modifier = Modifier
                    .background(color = Color.Gray)
                    .offset(x = 10.dp)
                )
            Text(text = "这是一段文字",
                modifier = Modifier
                    .padding(start = 10.dp,top = 10.dp)
                    .background(color = Color.Red)
                )
        }
    }

padding和offset

从上面的图片可以看出来,使用offset并不会改变可组合项的测量结果。

Compose中的类型安全

Compose中,有些修饰符仅适用于某些可组合项,比如上面已经学习过的weight修饰符就仅适用于RowColumn.

Box中的matchParentSize

在使用Box布局的时候,如果我们希望子项能够填满父项而不对父项的大小做出影响,则应该使用matchParentSize修饰符,这个修饰符仅可作用于Box的作用域内,也就是Box的直接子项可以使用此修饰符,如下所示:

    @Composable
    private fun matchParentSizeInBox(){
        Box(modifier = Modifier.background(color = Color.Gray)) {
            Spacer(modifier = Modifier
                .background(color = Color.Cyan)
                .matchParentSize())
            Text(text = "这是一个很大的文本框",modifier = Modifier.size(200.dp)
                .wrapContentSize(align = Alignment.Center),
                textAlign = TextAlign.Center,
                )
        }
    }

上面的代码运行结果如下:

Box中的matchParentSize

从上面的运行结果可以看出:Box中包含两个子项,分别是SpacerText,其中Spacer的大小设置为和父项保持一致,Text则拥有自己的大小。最终Box的大小设置为其所有子项中最大的那个子项的大小,而Spacer和大小也和最大的子项的大小一致(从背景颜色可以看出这点)。

之前我们学习了fllMaxSize能够使子项的大小和父项保持一致,但是使用fillMaxSize的问题在于子项会反过来对父项的大小造成影响,如下所示:

    @Composable
    private fun fillMaxSizeInBox(){
        Box(modifier = Modifier.background(color = Color.Gray)) {
            Spacer(modifier = Modifier
                .fillMaxSize()
                .background(color = Color.Cyan)
            )
            Text(text = "这是一个很大的文本框",
                modifier = Modifier
                    .size(200.dp)
                    .background(color = Color.Green)
                    .wrapContentSize(align = Alignment.Center),
                textAlign = TextAlign.Center
                )
        }
    }

在上面的代码中,同样Text有自己的大小,Spacer则设置为占满父项的大小,但是其父项又没有具体的大小,因此这个属性会反向作用域其父项,让其父项也撑满它的父项的大小,最终的结果如下:

在Box中使用fillMaxSize