Compose 学习界面架构 - 基础控件和布局

终于把概念上的东西过完了,其实官方文档里面还有架构和架构分层方面上的东西,不过我觉得布局都还不会就学架构有些太早了点,所以就打算延后再看了。

一、常用可组合项的基本用法

1. Text 

如果想要显示一行文字,最基本方法就是给他指定一个 String 类型的参数即可:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeTestTheme {
                Text(text = "Hello Android")
            }
        }
    }
}

但这样运行后很明显功能太单调了肯定不符合产品需求的,可以用 Text 提供的 API 对 Text 的内容进行定制,例如:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeTestTheme {
                Text(
                    text = "Hello Android",
                    color = Color.Red,              // 设置文本颜色
                    fontSize = 30.sp,               // 设置文字大小
                    fontStyle = FontStyle.Italic,   // 设置文字样式(正常、斜体)
                    fontWeight = FontWeight.Bold,   // 设置文字权重(正常、加粗等)
                )
            }
        }
    }
}

运行后的文本:

通过 style 参数可以设置一个类型为 TextStyle 的对象来配置多个参数,也可以通过 Shadow 来配置阴影,Shadow 会有三个参数,分别为:

  • 接收阴影颜色、
  • 相对于 Text 所在的位置的偏移量
  • 模糊半径(用来控制模糊效果)
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeTestTheme {
                val offset = Offset(5.0f, 10.0f)
                Text(
                    text = "Hello Android",
                    style = TextStyle(
                        color = Color.Blue,
                        fontSize = 25.sp,
                        shadow = Shadow(
                            color = Color.Gray, offset = offset, blurRadius = 3f
                        )
                    )
                )
            }
        }
    }
}

    试想一下,如果产品想一行显示:标题颜色黑色,后面跟随蓝色的邮箱文本,在 xml 中最简单的就是写两个 TextView 设置不同的颜色吧。但是在 Compose 中只需要使用  AnnotatedString 数据类即可在同一 Text 可组合项中设置不同的样式,AnnotatedString 其中包含:

    • 一个 Text 值
    • 一个 SpanStyleRange 的 List,等同于位置范围在文字值内的内嵌样式
    • 一个 ParagraphStyleRange 的 List,用于指定文字对齐、文字方向、行高和文字缩进样式
    class MainActivity : ComponentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                ComposeTestTheme {
                    Text(
                        text =  buildAnnotatedString {
                            withStyle(style = SpanStyle(color = Color.Black)) {
                                append("标题: ")
                            }
                            withStyle(style = SpanStyle(color = Color.Blue)) {
                                append("xxxx.email")
                            }
                        }
                    )
                }
            }
        }
    }

    TextStyle 用于普通 String 的 Text 可组合项, SpanStyle 和 ParagraphStyle 用于 AnnotatedString 的 Text 可组合项。运行结果:

    如果产品需求更变态一点,同一行文字他需要五颜六色的文字样式呢?在 Compose 中可以将 Brush API 与 TextStyle 和 SpanStyle 搭配使用,就能实现这样的效果,请看代码:

    class MainActivity : ComponentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                ComposeTestTheme {
                    // 设置 Brush 笔刷颜色
                    val gradientColors = listOf(Color.Red, Color.Cyan, Color.LightGray, Color.Yellow)
                    Text(
                        text = "这是一段超级超级超级超级超级超级超级超级超级超级超级超级长的文字",
                        // 使用 TextStyle 中的内置画笔配置文字
                        style = TextStyle(
                            brush = Brush.linearGradient(
                                colors = gradientColors
                            )
                        )
                    )
                }
            }
        }
    }

    实现的效果:

    在任何位置只要使用到了 TextStyle 或 SpanStyle 都可以使用 Brush,所以分段设置文字颜色也非常简单:

    class MainActivity : ComponentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                ComposeTestTheme {
                    val gradientColors = listOf(Color.Red, Color.Cyan, Color.LightGray, Color.Yellow, Color.Red)
                    Text(
                        text = buildAnnotatedString {
                            withStyle(
                                SpanStyle(
                                    brush = Brush.linearGradient(
                                        colors = gradientColors
                                    ), alpha = .5f) // 设置透明度
                            ){
                                append("这是一段\n")
                            }
                            withStyle(
                                SpanStyle(
                                    brush = Brush.linearGradient(
                                        colors = gradientColors
                                    ), alpha = 1f)
                            ) {
                                append("很长很长很长很长很长很长很长很长很长很长很长很长")
                            }
                            append("\n的文字.")
                        }
                    )
                }
            }
        }
    }

    除此之外,Text 还有丰富的 API 可以通过它的源码参数列表参考:

    @Composable
    fun Text(
        text: AnnotatedString,
        modifier: Modifier = Modifier,
        color: Color = Color.Unspecified,
        fontSize: TextUnit = TextUnit.Unspecified,
        fontStyle: FontStyle? = null,
        fontWeight: FontWeight? = null,
        fontFamily: FontFamily? = null,
        letterSpacing: TextUnit = TextUnit.Unspecified,
        textDecoration: TextDecoration? = null,
        textAlign: TextAlign? = null,
        lineHeight: TextUnit = TextUnit.Unspecified,
        overflow: TextOverflow = TextOverflow.Clip,
        softWrap: Boolean = true,
        maxLines: Int = Int.MAX_VALUE,
        minLines: Int = 1,
        inlineContent: Map<String, InlineTextContent> = mapOf(),
        onTextLayout: (TextLayoutResult) -> Unit = {},
        style: TextStyle = LocalTextStyle.current
    ) {
        ... 
    }

    2. Button

    Button 也是比较常用的组件,在 Compose 中定义了 5 种按钮类型:

    类型外观用途
    Filled纯色背景,文字对比鲜明。高强调度按钮。这些操作适用于应用中的主要操作,例如“提交”和“保存”。阴影效果突出了按钮的重要性。
    填充色调背景颜色会根据表面而变化。也适用于主要操作或重要操作。填充色调按钮具有更强的视觉效果,适合“添加到购物车”和“登录”等功能。
    过高通过添加阴影来突出显示。用途与色调按钮类似。增加高程,使按钮看起来更加突出。
    Outlined具有无填充的边框。中强调度按钮,包含重要但并非主要的操作。这类按钮可与其他按钮搭配使用,用于指示替代性次要操作,例如“取消”或“返回”。
    文本显示没有背景或边框的文字。低强调按钮,非常适合不太重要的操作,例如导航链接或“了解详情”或“查看详情”等辅助功能。

    具体的就不详细描述了,这里只记录最基本的 Filled 填充按钮的用法,更多详细的用法和按钮就放在组件篇再记录吧。使用 Button 可组合项设置一个按钮:

    class MainActivity : ComponentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                ComposeTestTheme {
                    val context = LocalContext.current
                    Button(
                        onClick = {
                            Toast.makeText(context, "Hello!", Toast.LENGTH_SHORT).show()
                        }
                    ) {
    
                    }
                }
            }
        }
    }
    

    这样设置以后界面上就显示了一个按钮,点击弹出 Toast 提示:

    但是此时按钮是没有任何文字的,给 Button 设置一个 text 指定文字内容,运行就会直接报错了,因为 Button 并没有 text 属性。那么如何才能给Button指定文字内容呢?答案是:配合 Text 一起使用:

    class MainActivity : ComponentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                ComposeTestTheme {
                    val context = LocalContext.current
                    Button(
                        onClick = {
                            Toast.makeText(context, "Hello!", Toast.LENGTH_SHORT).show()
                        }
                    ) {
                        Text(
                            text = "点我",
                            color = Color.White,
                            fontSize = 26.sp
                        )
                    }
                }
            }
        }
    }

    3. TextField

    TextField 想必已经很熟悉了,在上一章的 State 中多次使用输入框组件举例了,这里就大致介绍一下吧。TextField 有两种实现方式,第一种就是上一章中基于值的文本字段;第二种就是最新的也是官方主要推荐的基于状态的文本字段。

    如果是新项目的话就直接用第二种就好了,以下展示一下两种写法的区别:

    class MainActivity : ComponentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                ComposeTestTheme {
                    Column {
                        // 基于值的文本写法
                        var valueState by remember { mutableStateOf("") }
                        TextField(
                            value = valueState,
                            label = { Text("基于值的输入框") },
                            onValueChange = { valueState = it },
                            placeholder = { Text(text = "请输入~") }
                        )
                        
                        // 基于状态的 TextField
                        // 创建 TextFieldState 参数: initialText = "默认值"
                        val nameState = rememberTextFieldState(initialText = "")
                        TextField(
                            state = nameState, // 核心:绑定状态容器,与 value 相同
                            label = { Text("基于状态的输入框") }, // 输入框标签
                            placeholder = { Text("请输入~") }, // 占位提示
                        )
    
                    }
                }
            }
        }
    }

    由此可见两种输入框其实是一样的,新项目优先选择使用 基于状态的 TextField 就好了。两种写法具体的差异如下所示:

    当然 TextField 也是支持 Brush 的,而且这个颜色也太丑了:

    val colors = remember {
        Brush.linearGradient(
            colors = listOf(
                Color.Red, Color.Yellow, Color.Green,
                Color.Blue, Color.Magenta
            )
        )
    }
    val nameState = rememberTextFieldState(initialText = "")
    TextField(
        state = nameState,
        label = { Text("基于状态的输入框") },
        placeholder = { Text("请输入~") },
        textStyle = TextStyle(
            brush = colors,
            fontSize = 18.sp,
            fontWeight = FontWeight.Bold
        ),
        colors = TextFieldDefaults.colors(
            // 未聚焦时的背景色
            unfocusedContainerColor = Color.LightGray.copy(alpha = 0.2f),
            // 聚焦时的背景色
            focusedContainerColor = Color.Green,
        )
    )

    虽然也丑但是只是做个示例吧,更多的 API 就后面再研究吧。

    4. Image

    Image 就是用于展示图片的,对应的是 ImageView,这个也是比较常用的控件了。Image 用法很简单,只不过需要区分是 drawable资源还是bitmap对象:

    class MainActivity : ComponentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                ComposeTestTheme {
                    Row {
                        Image(
                            painter = painterResource(id = R.mipmap.image01),
                            contentDescription = "a Image"
                        )
    
                        // 将 drawable 资源转换成了一个 ImageBitmap 对象
                        val bitmap: ImageBitmap = ImageBitmap.imageResource(id = R.mipmap.image02)
                        Image(
                            bitmap = bitmap,
                            contentDescription = null
                        )
                    }
                }
            }
        }
    }

    用法很简单,但是有几个细节还是需要注意:

    1、Image 必须要有 contentDescription 参数,这个参数是用于指定对于这个资源的文字描述,主要用于无障碍模式提供发音辅助的,可以直接传入 null,但是必须得有。

    2、Image 接收的是 Compose 中专有的 ImageBitmap 对象,而不是传统的 Bitmap 对象。如果想要传入传统的 Bitmap 对象,需要使用 asImageBitmap 函数转换:

    Image(
        bitmap = bitmap.asImageBitmap(),
        contentDescription = null
    )

    运行结果:

    注意:Image 只能展示本地图片,如果要展示网络加载的图片需要依赖第三方库。目前Google比较推荐的第三方 Compose 图片加载库就是 Coil 和 Glide,如果是用 Kotlin 的话,直接用 Coil 就好了,因为 Coil 是基于协程开发的新兴图片加载库,用法更加贴合Kotlin也更加简单,使用前需要添加依赖:

    dependencies {
        implementation("io.coil-kt:coil-compose:2.4.0")
    }

    然后使用 Coil 提供的 AsyncImage 控件加载网络图片资源:

    // 记得申请网络权限
    AsyncImage(
        model = "https://pic.nximg.cn/file/20230520/31372278_171245946104_2.jpg",
        contentDescription = "First line of code"
    )

    二、三大布局

    Compose 中的三个基本标准布局元素是 ColumnRow 和 Box 可组合项,如图所示。

    在学习基本布局之前,需要先了解一个术语: Modifier (修饰符),借助修饰符可以修饰或扩充可组合项。具体可以使用修饰符来执行以下操作:

    • 更改可组合项的大小、布局、行为和外观
    • 添加信息,如无障碍标签
    • 处理用户输入
    • 添加高级互动,如使元素可点击、可滚动、可拖动或可缩放

    1. Column

    Column就是让控件纵向排列,相当于 LinearLayout(orientation=vertical),例如上面输入框的例子。它关键的参数其实就几个,更多的是对 modifier 的定制:

    参数名作用常用取值示例
    verticalArrangement控制子组件在 垂直方向 的排列方式

    Arrangement.Top(默认,顶部对齐)

    Arrangement.Center(垂直居中)

    Arrangement.SpaceBetween(均匀分布)

    horizontalAlignment控制子组件在 水平方向 的对齐方式

    Alignment.Start(默认,左对齐)、Alignment.CenterHorizontally(水平居中)

    Alignment.End(右对齐)

    modifier控制 Column 自身的大小、边距、背景等

    fillMaxWidth()(占满父容器宽度)

    padding(xx.dp)(内边距)

    Column中的所有控件都是默认左对齐的,如果想让它居中就可以这样:

    @Composable
    fun ColumnStudyTest() {
        Column(
            modifier = Modifier.fillMaxSize(),
            horizontalAlignment = Alignment.CenterHorizontally,
        ) {
            。。。
        }
    }

    如果需要 Text1 又保持默认的左对齐,只需要在 Text1 中额外指定对齐方式即可:

    @Composable
    fun ColumnStudyTest() {
        Column(
            modifier = Modifier.fillMaxSize(),
            horizontalAlignment = Alignment.CenterHorizontally,
        ) {
            Text(
                modifier = Modifier.align(Alignment.Start),
                text = "This is Text 1", color = Color.Blue, fontSize = 26.sp
            )
            。。。
        }
    }

    这样 Text1 就是左对齐,其它保持居中了:

    这就是 horizontalAlignment 的用法控制的是水平方向上的对齐方式,与之对应的就是 verticalArrangement,垂直方向上的分布方式。这里展示一下 SpaceEvenly 均分属性,这个其实就相当于 LinearLayout 的 layout_weight = 1:

    除此以外,verticalArrangement 还有很多可指定分布方式的参数,具体可以看看Google官方的动图示例就能快速了解每种分布方式的效果了:

    2. Row

    再来看Row,它跟 Column 基本是就是完全一样的东西,只是方向上有所区别,相当于 LinearLayout(orientation=horizontal),它的参数逻辑和 Column 是对称的:

    参数名作用常用取值示例
    horizontalArrangement控制子组件在 水平方向 的排列方式

    Arrangement.Start(默认,左对齐)

    Arrangement.Center(水平居中)

    Arrangement.SpaceAround(均匀分布)

    verticalAlignment控制子组件在 垂直方向 的对齐方式

    Alignment.CenterVertically(默认,垂直居中)

    Alignment.Top(顶部对齐)

    Alignment.Bottom(底部对齐)

    modifier控制 Row 自身的大小、边距等同 Column

    直接放代码展示 horizontalArrangement 和 verticalAlignment 的效果吧:

    @Composable
    fun RowStudyTest() {
        Row(
            modifier = Modifier.fillMaxSize(),
            horizontalArrangement = Arrangement.Center,
            verticalAlignment = Alignment.CenterVertically,
        ) {
            。。。
        }
    }

    运行结果就是居中横向排列的几个 Text 

    这里有一个 Column 和 Row 中可指定的对齐方式和分布方式的参数有一个细节:

    • Column 因为是纵向布局只能指定水平方向上的对齐方式,所以只有 horizontalAlignment 参数,并没有 verticalAlignment 的参数。
    • 而 Row 因为是横向布局只能指定垂直方向上的对齐方式,所以只有 verticalAlignment 参数,没有 horizontalAlignment 参数。
    • 同理,Column 因为是纵向布局只能指定垂直方向上的分布方式,所以只有 verticalArrangement  参数。
    • Row 因为是横向布局只能指定水平方向上的分布方式,所以同样只有 horizontalArrangement 参数。

    因为屏幕大小的问题,这里看不出 horizontalArrangement 参数指定的分布方式的效果是怎么样的,所以直接看官方给的动图吧:

    补充:

    在 View 中,如果一屏显示不下通常会采用 HorizontalScrollView 或 NestedScrollView 布局嵌套滚动显示,而 Compose 实现滚动只需要借助 modifier 参数即可,代码如下所示:

    @Composable
    fun RowStudyTest() {
        Row(
            modifier = Modifier
                .fillMaxSize()
                .horizontalScroll(rememberScrollState()),
            horizontalArrangement = Arrangement.Center,
            verticalAlignment = Alignment.CenterVertically,
        ) {
            ... 
        }
    }

    其实就是在 modifier 后面串接 horizontalScroll 函数,horizontalScroll 有一个必填的 ScrollState参数,它是用于保证在手机横竖屏旋转的情况下滚动位置不会丢失的,通常可以调用rememberScrollState 函数获得。

    同理,在 Column 中想要实现滑动,那么就应该在 modifier 后面串接 verticalScroll 函数,使用方法与 Row 同理。

    3. Box

    Box 相当于 FrameLayout,所有的元素都默认在布局的左上角,后添加的元素覆盖前面的元素。

    Box 和 Column、Row 相同,同样可以设置内部元素的对齐方式,只不过就没有分布方式设置的参数了。直接看代码吧:

    @Composable
    fun BoxStudyTest() {
        Box(
            modifier = Modifier.fillMaxSize(),
            contentAlignment = Alignment.BottomStart
        ) {
            Text(modifier = Modifier.padding(bottom = 50.dp), text = "未指定位置, 离底部50dp")
            Text(modifier = Modifier.align(Alignment.TopStart), text = "左上角")
            Text(modifier = Modifier.align(Alignment.TopCenter), text = "顶部居中")
            Text(modifier = Modifier.align(Alignment.TopEnd), text = "右上角")
            Text(modifier = Modifier.align(Alignment.CenterStart), text = "左边居中")
            Text(modifier = Modifier.align(Alignment.Center), text = "屏幕居中")
            Text(modifier = Modifier.align(Alignment.CenterEnd), text = "右边居中")
            Text(modifier = Modifier.align(Alignment.BottomStart), text = "左下角")
            Text(modifier = Modifier.align(Alignment.BottomCenter), text = "底部居中")
            Text(modifier = Modifier.align(Alignment.BottomEnd), text = "右下角")
        }
    }
    

    Box 可以通过 contentAlignment 来设置内部的元素默认的对齐方式,如果不设置,默认就是左上角对齐;内部的元素可以通过 Modifier.align 分别指定了它们各自的对齐方式,这样就不会重叠到一起了。运行结果如下:

    4. 小结

    这里列了个表格,大概对比一下三种布局的差异:

    组件排列方式关键参数(分布 / 对齐)
    Column垂直verticalArrangement(垂直分布)、horizontalAlignment(水平对齐)
    Row水平horizontalArrangement(水平分布)、verticalAlignment(垂直对齐)
    Box堆叠contentAlignment(整体对齐)

    三、补充:Compose 中的 ConstraintLayout

    当使用 Column/Row/Box 组合复杂界面时,很容易出现 “多层嵌套”(比如 “Column 里套 Row,Row 里再套 Column”)。这时候就需要 ConstraintLayout—— 通过 “约束关系” 定位组件,实现 “扁平布局结构”。

    在 View 系统中,建议使用 ConstraintLayout 来创建复杂的大型布局,是因为扁平视图层次结构比嵌套视图的效果更好。不过在 Compose 中不是什么问题,因为 Compose 能够高效地处理较深的布局层次结构,所以也可以选择层层嵌套 Column/Row/Box 。

    1. 准备步骤

    如需使用 ConstraintLayout,需要在 build.gradle 中添加以下依赖项:

    dependencies {
      implementation("androidx.constraintlayout:constraintlayout-compose:1.0.1")
    }

    constraintLayout-compose 的版本控制请在 ConstraintLayout 版本页面中查看最新版本:ConstraintLayout 版本

    2. 使用方法

    ConstraintLayout 的核心逻辑是 “给组件加‘引用’,再通过引用设置约束”,具体分三步:

    步骤 1:创建组件引用(Ref)
    用 createRefs() 或 createRefFor() 为每个子组件创建 “唯一标识”(类似 View 的 id)。
    步骤 2:设置约束(constrainAs)
    用 Modifier.constrainAs(引用) 为组件绑定约束,通过 linkTo() 指定 “相对于谁、在哪个方向对齐”。
    步骤 3:使用父容器引用(parent)
    parent 是 ConstraintLayout 内置的引用,代表布局本身,可用于 “组件相对于父容器定位”。

    基本用法和 View 中差不多,直接上示例吧

    @Composable
    fun ConstraintStudyTest() {
        ConstraintLayout(
            modifier = Modifier.fillMaxSize()
        ) {
            // 为子组件创建引用(按钮id → buttonRef,文本id → textRef)
            val (buttonRef, textRef) = createRefs()
            // 按钮:父容器顶部,水平居中
            Button(
                modifier = Modifier.constrainAs(buttonRef){
                    // 顶部与父容器顶部对齐,边距16dp
                    top.linkTo(parent.top, margin = 16.dp)
                    // 开始/结束与父容器开始/结束对齐(即水平居中)
                    start.linkTo(parent.start)
                    end.linkTo(parent.end)
                },
                onClick = { },
            ) {
                Text("点我")
            }
            // 文本:按钮下方,水平居中
            Text(
                "Text",
                Modifier.constrainAs(textRef) {
                    // 顶部与按钮底部对齐,边距16dp
                    top.linkTo(buttonRef.bottom, margin = 16.dp)
                    // 复用按钮的水平居中约束
                    start.linkTo(buttonRef.start)
                    end.linkTo(buttonRef.end)
                }
            )
        }
    }

    运行后的样式为:

    3. ConstraintLayout  引导线与屏障线

    引导线是 “虚拟的参考线”,可按 固定 dp 或 父容器百分比 创建,组件可相对于引导线定位(适合自适应不同屏幕尺寸):

    @Composable
    fun GuidelineConstraintLayout() {
        ConstraintLayout(modifier = Modifier.fillMaxSize()) {
            // 垂直引导线:从父容器左侧开始,占父容器宽度的20%)
            val startGuideline = createGuidelineFromStart(0.2f)
            // 水平引导线:从父容器顶部开始,固定32dp
            val topGuideline = createGuidelineFromTop(32.dp)
            // 创建组件引用
            val (text1, text2) = createRefs()
            
            Text(
                text = "AAAAAAAAAAAAAAA",
                modifier = Modifier.constrainAs(text1) {
                    top.linkTo(topGuideline) // 顶部对齐水平引导线
                    start.linkTo(startGuideline) // 左侧对齐垂直引导线
                    end.linkTo(parent.end, margin = 16.dp) // 右侧距父容器16dp
                    width = Dimension.fillToConstraints // 宽度 = 左引导线到右约束的距离
                }
                    .background(Color.Yellow)
            )
            Text(
                text = "BBBBBBBBBBBBBB",
                modifier = Modifier.constrainAs(text2) {
                    top.linkTo(text1.bottom, margin = 16.dp) // 顶部对齐 text1 底部
                    start.linkTo(startGuideline) // 左侧与 text1 对齐
                    end.linkTo(text1.end) // 右侧与用户名对齐
                    width = Dimension.fillToConstraints
                }
                    .background(Color.Red)
            )
        }
    }
    

    运行后结果:

    屏障线是 “动态参考线”,会根据 多个组件的最边缘位置 自动调整(比如 “组件 A 和组件 B 中最高的那个的顶部” 就是屏障线位置),适合多组件高度 / 宽度不固定的场景。

    @Composable
    fun BarrierConstraintLayout() {
        ConstraintLayout(modifier = Modifier.padding(20.dp)) {
            // 创建组件引用
            val (text1, text2, text3) = createRefs()
            // 创建屏障线:取 text1 和 text2 的“右侧”最边缘作为屏障线
            val rightBarrier = createEndBarrier(text1, text2)
    
            Text(
                text = "长长长文本",
                modifier = Modifier.constrainAs(text1) {
                    top.linkTo(parent.top)
                    start.linkTo(parent.start)
                }
                    .background(Color.Red)
            )
            Text(
                text = "短文本",
                modifier = Modifier.constrainAs(text2) {
                    top.linkTo(text1.bottom, margin = 16.dp)
                    start.linkTo(parent.start)
                }
                    .background(Color.Red)
            )
    
            // 在屏障线右侧(即两个文本中最宽的那个的右侧)
            Text(
                text = "我在两个文本的右边",
                modifier = Modifier.constrainAs(text3) {
                    top.linkTo(parent.top)
                    start.linkTo(rightBarrier, margin = 16.dp) // 左侧对齐屏障线
                }
                    .background(Color.Yellow)
            )
        }
    }

    运行结果:

    官方还有一种“链” 的用法,但是看了一下好像和 FlowRow 和 FlowColumn 类似,后面需要在补充吧。

    4. decoupled API:分离约束与布局

    当需要 “动态切换约束”(如横屏 / 竖屏显示不同布局)时,可将约束逻辑抽离为 ConstraintSet,与布局本身分离:

    fun DecoupledConstraintLayout() {
        // BoxWithConstraints:获取父容器尺寸(用于判断横竖屏)
        BoxWithConstraints {
            // 根据屏幕宽度判断:<600dp 为竖屏,≥600dp 为横屏
            val constraints = if (maxWidth < 600.dp) {
                // 竖屏约束:按钮在上,文本在下
                verticalConstraints(margin = 16.dp)
            } else {
                // 横屏约束:按钮在左,文本在右
                horizontalConstraints(margin = 24.dp)
            }
    
            // 传入约束集,使用 layoutId 绑定组件
            ConstraintLayout(constraintSet = constraints) {
                Button(onClick = {}, modifier = Modifier.layoutId("button")) { Text("按钮") }
                Text(text = "我是文本", modifier = Modifier.layoutId("text"))
            }
        }
    }
    // 竖屏约束集:按钮在上,文本在下
    private fun verticalConstraints(margin: Dp): ConstraintSet {
        return ConstraintSet {
            val buttonRef = createRefFor("button")
            val textRef = createRefFor("text")
    
            constrain(buttonRef) {
                top.linkTo(parent.top, margin)
                start.linkTo(parent.start)
                end.linkTo(parent.end)
            }
    
            constrain(textRef) {
                top.linkTo(buttonRef.bottom, margin)
                start.linkTo(buttonRef.start)
                end.linkTo(buttonRef.end)
            }
        }
    }
    // 横屏约束集:按钮在左,文本在右
    private fun horizontalConstraints(margin: Dp): ConstraintSet {
        return ConstraintSet {
            val buttonRef = createRefFor("button")
            val textRef = createRefFor("text")
    
            constrain(buttonRef) {
                top.linkTo(parent.top, margin)
                start.linkTo(parent.start, margin)
            }
    
            constrain(textRef) {
                top.linkTo(buttonRef.top)
                bottom.linkTo(buttonRef.bottom)
                start.linkTo(buttonRef.end, margin)
                end.linkTo(parent.end, margin)
            }
        }
    }

    运行结果 - 竖屏:

    运行结果 - 横屏:

    四、总结

    1、四个基础可组合项

    • Text:用于显示文字,可通过 API 或 TextStyle 定制样式,用 AnnotatedString 实现同一文本内不同样式,搭配 Brush 可做渐变效果。
    • Button:含 5 种类型、基础 Filled 通过 onClick 设置点击事件,且需嵌套 Text 实现文字显示,无单独 text 属性。
    • TextField:分基于值(用 mutableStateOf)和基于状态(用 rememberTextFieldState)两种实现。
    • Image:显示本地图片需用 painterResource 或 ImageBitmap(传统 Bitmap 需转 asImageBitmap),网络图片依赖 Coil 的 AsyncImage;必须设置 contentDescription(可传 null),用于无障碍辅助。

    2、基础核心三布局:

    • Column:垂直排列,用 verticalArrangement 控制垂直间距,horizontalAlignment 控制水平对齐。
    • Row:水平排列,参数与 Column 对称,用 horizontalArrangement 控制水平间距。
    • Box:层叠排列,后声明的组件覆盖前面的,用 zIndex 控制层叠顺序。

    3、ConstraintLayout:

    • 基础用法:createRefs() 创引用 → constrainAs() 设约束 → linkTo() 绑定关系。
    • 高级特性:引导线(按比例定位)、屏障线(多组件联动)
    • Decoupled API:抽离 ConstraintSet,实现动态约束切换。

    总算是写完了,拖更比较久,主要是公司事情比较多,还有本章想写的内容也多加上官方顺序也挺杂的...其实到这里主要布局啥的都学完了,已经可以进行简单页面的开发了,想要什么样式或者不懂的api网上也一大堆具体的讲解了,下部分就填一下坑、记录一下 Modifier 修饰符的作用、还有一些 View 中比较常用的组件比如 NavigationView 啥的吧。

    评论
    成就一亿技术人!
    拼手气红包6.0元
    还能输入1000个字符
     
    红包 添加红包
    表情包 插入表情
     条评论被折叠 查看
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值