云计算百科
云计算领域专业知识百科平台

【Jetpack Compose声明式UI界面开发实用技术学与练】第 3 课:Modifier 与样式——从能看看到好看

本课定位: 第 1 课建立了状态驱动的心智模型,第 2 课掌握了空间组织能力。第 3 课进入 Compose 的"外观系统"——Modifier。Modifier 是 Compose 中使用频率最高的 API,它决定组件的尺寸、内边距、背景、边框、圆角、阴影、点击、滚动、变换等几乎所有视觉和交互属性。理解 Modifier 顺序,是区分"会用 Compose"和"用得好 Compose"的分水岭。 后面主题、动画、手势、性能优化,都建立在本课之上。


一、本课目标

学完本课,你应该能够:

  • 理解 Modifier 的本质:链式、不可变、顺序敏感。
  • 理解 Modifier 为什么会影响测量、绘制、交互三个阶段。
  • 掌握常用尺寸修饰符:size、width、height、fillMaxWidth、requiredSize、sizeIn。
  • 掌握内边距:padding,并理解 Compose 为什么没有 margin。
  • 掌握背景与边框:background、border。
  • 掌握圆角与裁剪:clip。
  • 掌握阴影与层级:shadow、zIndex。
  • 掌握点击与交互:clickable、combinedClickable。
  • 深刻理解 Modifier 顺序的语义,能解释常见"效果不符预期"问题。
  • 能定制 Card、Surface、Button 的样式。
  • 能独立完成一套卡片样式组件。
  • 初步具备"按效果推顺序"的逆向思维能力。

  • 二、Modifier 的本质

    2.1 什么是 Modifier?

    Modifier 是 Compose 中用来"修饰组件"的对象。它可以设置尺寸、内边距、背景、边框、圆角、阴影、点击、滚动等属性。

    Text(
    text = "你好",
    modifier = Modifier
    .padding(16.dp)
    .background(Color.Yellow)
    .clickable { }
    )

    这里 Modifier 链按顺序作用在 Text 上。注意:Modifier 不是 XML 属性,它是一段可组合的行为描述。 每个修饰符都会改变最终组件的测量、布局、绘制或交互行为。

    2.2 Modifier 的三个核心特性

    特性一:链式调用

    Modifier
    .padding(16.dp)
    .background(Color.Yellow)
    .clickable { }

    每个方法返回一个新的 Modifier,可以继续链式调用。

    特性二:不可变

    Modifier 本身是不可变的。Modifier.padding(16.dp) 不会修改原来的 Modifier,而是返回一个新的 Modifier。这意味着你可以安全地复用和共享 Modifier。

    val baseModifier = Modifier.padding(16.dp)
    val cardModifier = baseModifier.background(Color.White) // baseModifier 不变
    val chipModifier = baseModifier.background(Color.Yellow) // 各自独立

    这在 Compose 中非常重要。如果 Modifier 可变,共享会导致难以排查的样式污染。

    特性三:顺序敏感

    Modifier.padding(16.dp).background(Color.Red)

    和

    Modifier.background(Color.Red).padding(16.dp)

    效果完全不同。顺序是 Modifier 最重要的语义。

    2.3 Modifier 到底做了什么?

    可以把 Modifier 想象成"一层一层包裹组件的包装纸":

    Modifier
    .padding(16.dp) // 第 1 层:外面加 16dp
    .background(Red) // 第 2 层:加红色背景
    .size(80.dp) // 第 3 层:固定 80dp

    每一层都会影响下一层的可用空间和行为。

    更本质地说,Modifier 会参与三个阶段:

    • 测量阶段:影响约束,比如 size、padding、fillMaxWidth。
    • 绘制阶段:影响外观,比如 background、border、shadow。
    • 交互阶段:影响事件,比如 clickable、pointerInput。

    理解这一点,就能理解为什么 Modifier 顺序会影响测量和绘制。

    2.4 从"包装纸"到"洋葱模型"

    把 Modifier 链想象成一个洋葱:

    ┌─────────────────────────────┐
    │ padding(16.dp) │ ← 最外层
    │ ┌───────────────────────┐ │
    │ │ background(Red) │ │
    │ │ ┌─────────────────┐ │ │
    │ │ │ size(80.dp) │ │ │
    │ │ │ ┌───────────┐ │ │ │
    │ │ │ │ 内容 │ │ │ │
    │ │ │ └───────────┘ │ │ │
    │ │ └─────────────────┘ │ │
    │ └───────────────────────┘ │
    └─────────────────────────────┘

    • 越靠外的修饰符,作用范围越大。
    • 越靠内的修饰符,作用范围越小。
    • 每一层都缩小或改变下一层的可用空间。

    这个模型能解释几乎所有 Modifier 顺序问题。

    2.5 Modifier 与 Modifier.companion

    Modifier.padding(16.dp)

    Modifier 本身是一个伴生对象,相当于"空的 Modifier 起点"。

    val base = Modifier
    val padded = base.padding(16.dp)

    base 是空修饰符,padded 是新的修饰符。不要修改已有的 Modifier,永远返回新的。

    2.6 为什么 Composable 推荐接收 modifier: Modifier = Modifier

    @Composable
    fun MyCard(
    modifier: Modifier = Modifier
    ) {
    Column(modifier = modifier) { ... }
    }

    这样做的好处:

  • 调用方可控:外部可以传入 Modifier 控制间距、尺寸、点击等。
  • 组合灵活:可以叠加在调用处,而不是写死在组件内部。
  • 符合 Compose 官方规范:几乎所有官方组件都这样设计。
  • 注意:组件内部应该把 modifier 应用在最外层,而不是中间层。否则外部传入的修饰符可能被内部修饰符覆盖。

    // 正确:外部 Modifier 应用在最外层
    @Composable
    fun MyCard(modifier: Modifier = Modifier) {
    Column(modifier = modifier.padding(16.dp)) { ... }
    }

    // 错误:外部 Modifier 被内部 padding 限制
    @Composable
    fun MyCard(modifier: Modifier = Modifier) {
    Column(modifier = Modifier.padding(16.dp).then(modifier)) { ... }
    }

    2.7 then:组合两个 Modifier

    Modifier
    .padding(16.dp)
    .then(otherModifier)

    then 用于把两个 Modifier 组合起来。常见场景:

    @Composable
    fun MyCard(modifier: Modifier = Modifier) {
    val baseModifier = Modifier
    .fillMaxWidth()
    .clip(RoundedCornerShape(12.dp))
    .background(Color.White)

    Column(modifier = modifier.then(baseModifier)) { ... }
    }

    then 的顺序同样重要:先写的靠外,后写的靠内。


    三、尺寸修饰符

    3.1 size:固定宽高

    Modifier.size(80.dp)
    Modifier.size(width = 80.dp, height = 40.dp)

    size 会同时设置宽度和高度。

    3.2 width / height:单独设置

    Modifier.width(120.dp)
    Modifier.height(48.dp)

    3.3 fillMaxWidth / fillMaxHeight / fillMaxSize

    Modifier.fillMaxWidth() // 宽度占满父容器允许的最大宽度
    Modifier.fillMaxHeight() // 高度占满父容器允许的最大高度
    Modifier.fillMaxSize() // 宽高都占满
    Modifier.fillMaxWidth(0.5f) // 占父容器宽度的 50%

    深入理解:fillMaxWidth(fraction) 允许按比例填充。fraction = 0.5f 表示占一半。fraction 的范围是 0f 到 1f。

    3.4 wrapContentSize:包裹内容

    Modifier.wrapContentSize()
    Modifier.wrapContentWidth()
    Modifier.wrapContentHeight()

    这是默认行为。除非特别需要,通常不需要显式写。

    3.5 requiredSize:无视父约束

    Modifier.requiredSize(120.dp)

    requiredSize 会忽略父容器的约束,强制使用指定尺寸。慎用,因为它会破坏父容器的布局。常见场景:图片必须显示某个固定尺寸时。

    // 父容器只给了 100dp
    Row(modifier = Modifier.width(100.dp)) {
    // 子元素强制 200dp,会溢出
    Box(modifier = Modifier.requiredSize(200.dp))
    }

    3.6 sizeIn:约束范围

    Modifier.sizeIn(minWidth = 100.dp, maxWidth = 200.dp)
    Modifier.sizeIn(minHeight = 40.dp)
    Modifier.sizeIn(minWidth = 100.dp, minHeight = 40.dp)

    sizeIn 不是固定尺寸,而是给尺寸设置上下限。

    3.7 defaultMinSize:最小尺寸

    Modifier.defaultMinSize(minWidth = 48.dp, minHeight = 48.dp)

    defaultMinSize 设置默认最小尺寸,但如果内容更大,会使用内容尺寸。和 sizeIn 的区别:

    • sizeIn:约束最终尺寸。
    • defaultMinSize:只在内容小于最小值时生效。

    3.8 aspectRatio:宽高比

    Modifier.aspectRatio(16f / 9f)
    Modifier.aspectRatio(1f) // 正方形

    aspectRatio 让组件保持固定宽高比。常用于图片、视频封面、方形头像。

    Box(
    modifier = Modifier
    .fillMaxWidth()
    .aspectRatio(16f / 9f)
    .background(Color.Gray)
    )

    3.9 尺寸修饰符对比

    修饰符作用是否受父约束
    size 固定尺寸 受
    width / height 单独固定 受
    fillMaxWidth 占满允许宽度 受
    fillMaxWidth(0.5f) 按比例占满 受
    wrapContentSize 包裹内容 受
    requiredSize 强制尺寸 不受
    sizeIn 尺寸范围 受
    defaultMinSize 默认最小尺寸 受
    aspectRatio 固定宽高比 受

    四、内外边距:padding

    4.1 padding 的用法

    Modifier.padding(16.dp) // 四周 16dp
    Modifier.padding(horizontal = 16.dp) // 左右 16dp
    Modifier.padding(vertical = 8.dp) // 上下 8dp
    Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 8.dp)

    4.2 padding 的本质是"内边距"

    padding 不是"外边距"(margin)。在 Compose 中,padding 是通过缩小内容的可用空间来实现的。

    Text(
    text = "你好",
    modifier = Modifier
    .background(Color.Yellow)
    .padding(16.dp)
    )

    效果:背景覆盖整个区域,包括 padding 部分。因为 background 在外层,padding 在内层。

    Text(
    text = "你好",
    modifier = Modifier
    .padding(16.dp)
    .background(Color.Yellow)
    )

    效果:先留出 16dp 外边距,背景只画在内容区域。

    4.3 Compose 没有 margin,怎么实现外边距?

    在 Compose 中,"外边距"通常由父容器控制子元素间距或子元素自己加 padding 实现:

    • 父容器用 Arrangement.spacedBy(12.dp)。
    • 父容器用 Spacer。
    • 子元素用 Modifier.padding(…)。

    理解这一点,就能从"margin/padding"的传统思维切换到"Compose 的修饰符链"思维。

    4.4 padding 与 size 的顺序

    Modifier
    .size(80.dp)
    .padding(16.dp)
    .background(Color.Red)

    效果:整体 80dp,内部 padding 16dp,背景画在 padding 内层。实际红色区域是 80 – 32 = 48dp。

    Modifier
    .padding(16.dp)
    .size(80.dp)
    .background(Color.Red)

    效果:外层 padding 16dp,再固定 80dp,背景画在 80dp 区域内。整体占用 80 + 32 = 112dp。

    尺寸修饰符和内边距的顺序会影响最终占用空间。

    4.5 一个常见需求:内边距不影响背景

    如果希望背景覆盖整体,但内容有内边距:

    Modifier
    .background(Color.Red)
    .padding(16.dp)

    如果希望背景只画在内容区域:

    Modifier
    .padding(16.dp)
    .background(Color.Red)

    选择哪种,取决于你想要的视觉效果。


    五、背景与边框

    5.1 background

    Modifier.background(Color.Red)
    Modifier.background(Color.Red, shape = RoundedCornerShape(8.dp))
    Modifier.background(color = Color.Red, alpha = 0.5f)
    Modifier.background(Brush.horizontalGradient(listOf(Color.Red, Color.Blue)))

    background 支持颜色、形状、透明度,还支持 Brush 渐变:

    Modifier.background(
    brush = Brush.linearGradient(
    colors = listOf(Color(0xFF6200EE), Color(0xFF03DAC5))
    ),
    shape = RoundedCornerShape(12.dp)
    )

    常用写法:

    Modifier.background(
    color = MaterialTheme.colorScheme.primary,
    shape = MaterialTheme.shapes.medium
    )

    5.2 border

    Modifier.border(1.dp, Color.Gray)
    Modifier.border(2.dp, Color.Red, shape = RoundedCornerShape(8.dp))
    Modifier.border(BorderStroke(2.dp, Color.Red), shape = CircleShape)
    Modifier.border(
    border = BorderStroke(1.dp, Brush.horizontalGradient(...)),
    shape = RoundedCornerShape(8.dp)
    )

    border 需要指定宽度、颜色、形状。注意:

    • border 是描边,不是背景。
    • border 必须配合 shape 才能画出圆角边框。
    • border 不会裁剪内容,只画边框。
    • border 支持 Brush,可以做渐变边框。

    5.3 background 与 border 的顺序

    Modifier
    .background(Color.Red)
    .border(2.dp, Color.Blue, RoundedCornerShape(8.dp))

    效果:背景先画,边框画在背景上面。边框不会被背景遮住。

    Modifier
    .border(2.dp, Color.Blue, RoundedCornerShape(8.dp))
    .background(Color.Red)

    效果:边框先画,背景再画在边框下面。如果背景是实心的,边框可能被覆盖。

    推荐顺序:

    Modifier
    .clip(shape) // 先裁剪
    .background(color) // 再画背景
    .border(width, color, shape) // 最后画边框

    5.4 border 与 clip 的配合

    如果想让内容也被圆角裁剪,需要 clip:

    Modifier
    .clip(RoundedCornerShape(12.dp))
    .background(Color.White)
    .border(1.dp, Color.Gray, RoundedCornerShape(12.dp))

    clip 负责裁剪内容和背景,border 负责描边。两者必须使用相同的 shape,才能视觉统一。

    5.5 border 的进阶:BorderStroke

    BorderStroke(1.dp, Color.Gray)
    BorderStroke(1.dp, Brush.horizontalGradient(listOf(Color.Red, Color.Blue)))

    BorderStroke 是边框的完整描述,支持渐变。使用场景:

    • 选中态边框。
    • 渐变边框。
    • 虚线边框(需要自定义 PathEffect)。

    六、圆角与裁剪:clip

    6.1 clip 的作用

    clip 把组件内容裁剪成指定形状。

    Modifier.clip(CircleShape)
    Modifier.clip(RoundedCornerShape(12.dp))
    Modifier.clip(RoundedCornerShape(topStart = 12.dp, bottomEnd = 12.dp))

    6.2 常用形状

    形状效果
    CircleShape 圆形
    RoundedCornerShape(8.dp) 8dp 圆角矩形
    RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp) 只有上方圆角
    CutCornerShape(8.dp) 切角
    MaterialTheme.shapes.small/medium/large 主题形状

    6.3 clip 的位置非常关键

    Modifier
    .size(80.dp)
    .clip(CircleShape)
    .background(Color.Blue)

    效果:先确定 80dp 尺寸,再裁剪成圆形,再画背景,得到圆形头像。

    Modifier
    .size(80.dp)
    .background(Color.Blue)
    .clip(CircleShape)

    效果:先确定尺寸,再画背景,最后裁剪。如果背景色已经铺满,裁剪后仍是圆形,但某些场景下视觉效果可能不同。

    推荐顺序:

    size -> clip -> background -> border

    6.4 clip 与 border 的常见坑

    Modifier
    .clip(RoundedCornerShape(8.dp))
    .background(Color.White)
    .border(1.dp, Color.Gray, RoundedCornerShape(8.dp))

    注意:border 需要自己指定 shape,它不会自动使用 clip 的形状。如果想让边框和裁剪一致,要传入相同的形状。

    6.5 clip 与 clickable 的配合

    如果希望点击区域是圆角形状:

    Modifier
    .clip(RoundedCornerShape(12.dp))
    .clickable { }
    .background(Color.White)

    clip 在前,clickable 在后,水波纹会沿圆角范围扩散。

    6.6 clipToBounds:裁剪越界内容

    Modifier.clipToBounds()

    clipToBounds 把内容裁剪到组件边界。常见场景:

    • 子元素超出父容器时,裁剪掉越界部分。
    • 滚动列表中的图片。

    七、阴影与层级

    7.1 shadow

    Modifier.shadow(elevation = 4.dp)
    Modifier.shadow(elevation = 4.dp, shape = RoundedCornerShape(12.dp))
    Modifier.shadow(elevation = 8.dp, shape = CircleShape, clip = false)

    参数:

    • elevation:阴影大小。
    • shape:阴影形状。
    • clip:是否裁剪内容。默认 false。
    • ambientColor、spotColor:阴影颜色。

    7.2 shadow 的顺序

    shadow 必须在 background 之前,才能看到阴影:

    Modifier
    .shadow(4.dp, RoundedCornerShape(12.dp))
    .background(Color.White)

    如果顺序错误,阴影可能被背景遮住。

    7.3 zIndex

    zIndex 控制层叠顺序:

    Box {
    Box(modifier = Modifier.zIndex(1f)) { ... }
    Box(modifier = Modifier.zIndex(2f)) { ... }
    }

    zIndex 数值越大越靠上。常用于自定义层叠效果。

    7.4 Card 与 Surface 的阴影

    Card 自带阴影:

    Card(
    elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
    ) { ... }

    Surface 也支持 shadowElevation 和 tonalElevation:

    Surface(
    tonalElevation = 2.dp,
    shadowElevation = 4.dp
    ) { ... }

    在真实项目中,优先用 Card 或 Surface 做卡片,而不是手写 Modifier.shadow。

    7.5 tonalElevation 和 shadowElevation 的区别

    参数作用
    tonalElevation 通过色调变化表达高度,不改阴影
    shadowElevation 通过阴影表达高度

    Material 3 中,tonalElevation 用于浅色表面,shadowElevation 用于需要明显阴影的场景。


    八、点击与交互

    8.1 clickable

    Modifier.clickable { /* 点击 */ }
    Modifier.clickable(enabled = true) { /* 点击 */ }
    Modifier.clickable(
    enabled = true,
    onClickLabel = "点击卡片"
    ) { /* 点击 */ }

    clickable 会自动添加水波纹(Ripple)效果,前提是外层有 MaterialTheme。

    onClickLabel 用于无障碍服务,描述点击行为。

    8.2 combinedClickable

    @OptIn(ExperimentalFoundationApi::class)
    Modifier.combinedClickable(
    onClick = { /* 单击 */ },
    onLongClick = { /* 长按 */ },
    onDoubleClick = { /* 双击 */ }
    )

    combinedClickable 是实验性 API,需要 @OptIn(ExperimentalFoundationApi::class)。

    8.3 clickable 与 padding 的顺序

    Modifier
    .clickable { }
    .padding(16.dp)

    点击范围包括 padding 部分。

    Modifier
    .padding(16.dp)
    .clickable { }

    点击范围只有内容区域,padding 部分不响应点击。

    推荐: 如果希望整块卡片可点击,把 clickable 放在 padding 之前。

    8.4 clip 与 clickable 的配合

    如果希望点击区域是圆角形状:

    Modifier
    .clip(RoundedCornerShape(12.dp))
    .clickable { }
    .background(Color.White)

    clip 在前,clickable 在后,水波纹会沿圆角范围扩散。

    8.5 用 Surface 代替 clickable + background

    Surface(
    onClick = { /* 点击 */ },
    shape = RoundedCornerShape(12.dp),
    color = Color.White
    ) { ... }

    Surface 自带了点击、背景、形状、阴影,代码更简洁,推荐在卡片场景使用。

    8.6 indication 和 interactionSource

    val interactionSource = remember { MutableInteractionSource() }

    Modifier.clickable(
    interactionSource = interactionSource,
    indication = null // 去掉水波纹
    ) { }

    indication = null 可以去掉水波纹。常见场景:自定义点击反馈。


    九、Card、Surface、Button 样式定制

    9.1 Card

    Card(
    modifier = Modifier.fillMaxWidth(),
    shape = RoundedCornerShape(12.dp),
    colors = CardDefaults.cardColors(
    containerColor = MaterialTheme.colorScheme.surface
    ),
    elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
    ) {
    // 内容
    }

    Card 的常用参数:

    • shape:形状。
    • colors:背景色和内容色。
    • elevation:阴影。
    • border:边框。

    9.2 Surface

    Surface(
    modifier = Modifier.fillMaxWidth(),
    shape = RoundedCornerShape(12.dp),
    color = MaterialTheme.colorScheme.surfaceVariant,
    contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
    tonalElevation = 2.dp,
    shadowElevation = 4.dp,
    border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline)
    ) {
    // 内容
    }

    Surface 的常用参数:

    • shape:形状。
    • color:背景色。
    • contentColor:内容色。
    • tonalElevation:色调高度。
    • shadowElevation:阴影高度。
    • border:边框。
    • onClick:点击。

    9.3 Button

    Button(
    onClick = { },
    shape = RoundedCornerShape(24.dp),
    colors = ButtonDefaults.buttonColors(
    containerColor = MaterialTheme.colorScheme.primary,
    contentColor = MaterialTheme.colorScheme.onPrimary
    ),
    border = BorderStroke(1.dp, MaterialTheme.colorScheme.primary)
    ) {
    Text("按钮")
    }

    9.4 选择建议

    需求选择
    普通卡片 Card
    带点击的卡片 Surface(onClick = …)
    自定义背景 + 边框 Surface
    主按钮 Button
    次按钮 OutlinedButton
    文字按钮 TextButton
    图标按钮 IconButton
    浮动操作按钮 FloatingActionButton

    9.5 按钮尺寸定制

    Button(
    onClick = { },
    modifier = Modifier.height(48.dp),
    contentPadding = PaddingValues(horizontal = 24.dp, vertical = 8.dp)
    ) {
    Text("按钮")
    }

    contentPadding 控制按钮内容的内边距,比 Modifier.padding 更推荐。


    十、完整示例:卡片样式组件库

    下面做一个"卡片样式组件库",包含普通卡片、可点击卡片、标签、头像、按钮。

    package com.example.composemodifier

    import android.os.Bundle
    import androidx.activity.ComponentActivity
    import androidx.activity.compose.setContent
    import androidx.compose.foundation.BorderStroke
    import androidx.compose.foundation.background
    import androidx.compose.foundation.clickable
    import androidx.compose.foundation.layout.*
    import androidx.compose.foundation.shape.CircleShape
    import androidx.compose.foundation.shape.RoundedCornerShape
    import androidx.compose.material3.*
    import androidx.compose.runtime.*
    import androidx.compose.ui.Alignment
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.draw.clip
    import androidx.compose.ui.draw.shadow
    import androidx.compose.ui.graphics.Color
    import androidx.compose.ui.text.font.FontWeight
    import androidx.compose.ui.tooling.preview.Preview
    import androidx.compose.ui.unit.dp

    class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
    MaterialTheme {
    Surface(modifier = Modifier.fillMaxSize()) {
    StyleShowcaseScreen()
    }
    }
    }
    }
    }

    @Composable
    fun StyleShowcaseScreen(modifier: Modifier = Modifier) {
    Column(
    modifier = modifier
    .fillMaxSize()
    .padding(16.dp),
    verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
    SimpleCard()
    ClickableCard()
    TagRow()
    ButtonRow()
    }
    }

    @Composable
    fun SimpleCard(modifier: Modifier = Modifier) {
    Card(
    modifier = modifier.fillMaxWidth(),
    shape = RoundedCornerShape(12.dp),
    elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
    ) {
    Column(
    modifier = Modifier
    .fillMaxWidth()
    .padding(16.dp),
    verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
    Text("普通卡片", style = MaterialTheme.typography.titleMedium)
    Text(
    "使用 Card + padding + elevation 实现。",
    style = MaterialTheme.typography.bodyMedium
    )
    }
    }
    }

    @Composable
    fun ClickableCard(modifier: Modifier = Modifier) {
    Surface(
    onClick = { },
    modifier = modifier.fillMaxWidth(),
    shape = RoundedCornerShape(12.dp),
    color = MaterialTheme.colorScheme.secondaryContainer,
    shadowElevation = 2.dp
    ) {
    Row(
    modifier = Modifier
    .fillMaxWidth()
    .padding(16.dp),
    verticalAlignment = Alignment.CenterVertically,
    horizontalArrangement = Arrangement.spacedBy(12.dp)
    ) {
    Avatar("李")
    Column(modifier = Modifier.weight(1f)) {
    Text("可点击卡片", style = MaterialTheme.typography.titleMedium)
    Text(
    "点击整块区域触发 onClick。",
    style = MaterialTheme.typography.bodySmall
    )
    }
    Text(">", style = MaterialTheme.typography.titleMedium)
    }
    }
    }

    @Composable
    fun TagRow(modifier: Modifier = Modifier) {
    Row(
    modifier = modifier,
    horizontalArrangement = Arrangement.spacedBy(8.dp)
    ) {
    Tag("Kotlin")
    Tag("Compose")
    Tag("Modifier")
    }
    }

    @Composable
    private fun Tag(text: String, modifier: Modifier = Modifier) {
    Surface(
    modifier = modifier,
    shape = RoundedCornerShape(50),
    color = MaterialTheme.colorScheme.primaryContainer
    ) {
    Text(
    text = text,
    modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
    style = MaterialTheme.typography.labelMedium,
    color = MaterialTheme.colorScheme.onPrimaryContainer
    )
    }
    }

    @Composable
    fun ButtonRow(modifier: Modifier = Modifier) {
    Row(
    modifier = modifier.fillMaxWidth(),
    horizontalArrangement = Arrangement.spacedBy(12.dp)
    ) {
    Button(
    onClick = { },
    modifier = Modifier.weight(1f),
    shape = RoundedCornerShape(24.dp)
    ) {
    Text("主按钮")
    }
    OutlinedButton(
    onClick = { },
    modifier = Modifier.weight(1f),
    shape = RoundedCornerShape(24.dp)
    ) {
    Text("次按钮")
    }
    }
    }

    @Composable
    fun Avatar(name: String, modifier: Modifier = Modifier) {
    Box(
    modifier = modifier
    .size(48.dp)
    .clip(CircleShape)
    .background(MaterialTheme.colorScheme.primary),
    contentAlignment = Alignment.Center
    ) {
    Text(
    text = name.take(1),
    color = Color.White,
    style = MaterialTheme.typography.titleMedium,
    fontWeight = FontWeight.Bold
    )
    }
    }

    @Preview(showBackground = true, name = "样式展示")
    @Composable
    fun StyleShowcasePreview() {
    MaterialTheme {
    StyleShowcaseScreen()
    }
    }

    10.1 用到的修饰符

    场景修饰符
    卡片背景与圆角 Card 自带 shape、elevation
    可点击卡片 Surface(onClick = …)
    圆角标签 Surface(shape = RoundedCornerShape(50))
    圆形头像 size -> clip(CircleShape) -> background
    按钮圆角 Button(shape = RoundedCornerShape(24.dp))
    按钮等宽 Modifier.weight(1f)

    10.2 关键设计点

    • 圆角统一用 RoundedCornerShape,卡片 12dp,标签全圆角,按钮 24dp。
    • 卡片优先用 Card 或 Surface,不手写 shadow。
    • 可点击卡片优先用 Surface(onClick = …),自带水波纹和点击处理。
    • 圆形头像用 size -> clip -> background 顺序。

    10.3 代码复用技巧

    • Avatar、Tag、ButtonRow 都是独立组件,可在其他页面复用。
    • 每个组件都接收 modifier: Modifier = Modifier,外部可控。
    • 颜色统一用 MaterialTheme.colorScheme,便于后续主题切换。

    十一、随堂练

  • 把普通卡片改为圆角 16dp,加 1dp 边框。
  • 把 ClickableCard 的点击范围改为不包含 padding,观察效果。
  • 给 Avatar 加 2dp 白色边框,模拟"头像描边"效果。
  • 用 Modifier.shadow 手写一个卡片,替代 Card,对比代码量。
  • 给 Tag 加 clickable,让标签可点击,观察水波纹范围。
  • 用 Brush.horizontalGradient 给标签做一个渐变背景。

  • 十二、15 道习题(含参考答案与解读)

    习题 1:Modifier 的三个核心特性是什么?

    参考答案: 链式调用、不可变、顺序敏感。

    解读: 链式调用让修饰符可以连续叠加;不可变意味着每次调用返回新 Modifier,不会修改原对象;顺序敏感意味着不同顺序会产生不同效果。这三点是理解 Modifier 的基础。特别是顺序敏感,是 Compose 与 XML 属性最大的差别之一。XML 属性是无序的,而 Modifier 是有序的、有语义的。


    习题 2:Modifier.padding(16.dp) 和 Modifier.background(Color.Red) 顺序不同,效果有什么区别?

    参考答案:

    • padding 在前:先留出 16dp 外边距,背景只画在内容区域。
    • background 在前:背景覆盖整个区域,包括 padding 部分。

    解读: 这体现 Modifier 链的"包裹"语义。先写的修饰符更靠外,后写的更靠内。padding 缩小内容区域,background 画在它所在的层。理解这一点,就能控制背景、点击、边框的范围。可以用洋葱模型记忆:

    外层 = 先写
    内层 = 后写


    习题 3:Compose 有 margin 吗?如何实现外边距?

    参考答案: Compose 没有 margin。外边距通常通过父容器控制间距(Arrangement.spacedBy、Spacer)或子元素自己加 padding 实现。

    解读: 这与 Compose 的"修饰符链"思维一致:没有独立的 margin 属性,间距由父容器或自身修饰符承担。在 XML 中习惯用 margin 的开发者,需要适应这种思维。一个常见做法:

    // 父容器控制间距
    Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
    Item()
    Item()
    }

    这比给每个 Item 加 margin 更统一、更可维护。


    习题 4:fillMaxWidth() 和 requiredSize() 有什么区别?

    参考答案: fillMaxWidth() 占满父容器允许的最大宽度,仍受父约束限制;requiredSize() 强制使用指定尺寸,忽略父约束。

    解读: requiredSize 是一把"双刃剑"。它能强制尺寸,但会破坏父容器的布局。比如父容器只给了 100dp,子元素 requiredSize(200.dp) 会溢出,可能被裁剪。除非确实需要,否则优先用 size 或 fillMaxWidth。

    // 慎用
    Modifier.requiredSize(200.dp)

    // 推荐
    Modifier.fillMaxWidth()
    Modifier.sizeIn(minWidth = 100.dp, maxWidth = 200.dp)


    习题 5:Modifier.size(80.dp) 和 Modifier.sizeIn(minWidth = 80.dp, maxWidth = 120.dp) 有什么区别?

    参考答案: size 固定 80dp;sizeIn 把尺寸限制在 80~120dp 之间,具体尺寸仍由内容决定。

    解读: sizeIn 适合"尺寸可变但有范围"的场景,比如自适应标签、输入框。它不会强制尺寸,只约束范围,比 size 更灵活。

    // 固定尺寸
    Modifier.size(80.dp)

    // 范围约束
    Modifier.sizeIn(minWidth = 80.dp, maxWidth = 120.dp)


    习题 6:写出一个圆形头像的 Modifier 链。

    参考答案:

    Modifier
    .size(80.dp)
    .clip(CircleShape)
    .background(Color.Blue)

    解读: 顺序是 size -> clip -> background。先确定尺寸,再裁剪成圆形,再画背景,才能得到圆形。如果 background 在 clip 之前,背景不会被裁剪,可能显示为方形。

    为什么这个顺序最稳妥?因为:

  • size 给后续一个确定的空间。
  • clip 把空间裁剪成圆形。
  • background 在裁剪后绘制,自然就是圆形。

  • 习题 7:clip 和 border 配合时有什么坑?

    参考答案: border 不会自动使用 clip 的形状,需要自己传入 shape。

    解读: 常见写法:

    Modifier
    .clip(RoundedCornerShape(12.dp))
    .background(Color.White)
    .border(1.dp, Color.Gray, RoundedCornerShape(12.dp))

    border 的 shape 要和 clip 一致,才能视觉统一。如果 border 不传 shape,会画成矩形边框,与圆角内容不匹配。可以提取一个变量复用:

    val shape = RoundedCornerShape(12.dp)
    Modifier
    .clip(shape)
    .background(Color.White)
    .border(1.dp, Color.Gray, shape)


    习题 8:shadow 应该在 background 之前还是之后?为什么?

    参考答案: shadow 应该在 background 之前。

    解读: shadow 需要在背景下面画阴影。如果 background 在前面,会覆盖阴影,导致阴影不可见。推荐顺序:

    Modifier
    .shadow(4.dp, RoundedCornerShape(12.dp))
    .background(Color.White)

    在实际项目中,优先用 Card 或 Surface 的 elevation,而不是手写 shadow。原因是官方组件更稳定,且能随主题变化。


    习题 9:clickable 和 padding 顺序不同,点击范围有什么区别?

    参考答案:

    • clickable 在前:点击范围包括 padding 部分。
    • padding 在前:点击范围只有内容区域。

    解读: 这和第 2 题同理。clickable 是一个"响应点击的层",它所在的层有多大,点击范围就有多大。如果希望整块卡片可点击,把 clickable 放在 padding 之前。

    // 整块可点击
    Modifier
    .clip(shape)
    .clickable { }
    .background(color)
    .padding(16.dp)

    // 只有内容可点击
    Modifier
    .clip(shape)
    .background(color)
    .padding(16.dp)
    .clickable { }


    习题 10:Surface(onClick = …) 和 Modifier.clickable { } 有什么区别?

    参考答案: Surface(onClick = …) 是组件级别的点击,自带背景、形状、阴影、水波纹;Modifier.clickable { } 是修饰符级别的点击,只负责响应点击和添加水波纹,背景、形状要自己加。

    解读: 卡片场景推荐 Surface(onClick = …),代码更简洁、视觉更统一。自定义场景(比如只需要点击而不需要背景)用 clickable。Surface 还支持 tonalElevation、shadowElevation 等参数,样式更完整。

    // 推荐:Surface
    Surface(
    onClick = { },
    shape = RoundedCornerShape(12.dp),
    color = Color.White,
    shadowElevation = 4.dp
    ) { ... }

    // 自定义:clickable
    Box(
    modifier = Modifier
    .clip(RoundedCornerShape(12.dp))
    .background(Color.White)
    .clickable { }
    ) { ... }


    习题 11:Card 和 Surface 有什么区别?如何选择?

    参考答案: Card 是 Material 3 封装的卡片组件,自带 shape、elevation、colors;Surface 是更底层的通用容器,支持 onClick、border、tonalElevation、shadowElevation。

    解读:

    • 普通卡片:Card。
    • 带点击的卡片:Surface(onClick = …)。
    • 需要自定义边框:Surface(border = …)。
    • 需要色调高度:Surface(tonalElevation = …)。
    • 需要同时控制阴影和色调:Surface。

    Card 本质上也是基于 Surface 的封装,理解这一点就不会纠结。


    习题 12:Button、OutlinedButton、TextButton 有什么区别?

    参考答案:

    • Button:实心主按钮。
    • OutlinedButton:带边框的次按钮。
    • TextButton:纯文字按钮。

    解读: 三者都属于 Material 3 按钮体系,按重要性层级区分。主操作使用 Button,次要操作使用 OutlinedButton,辅助操作使用 TextButton。它们都支持 shape、colors、border 等参数。

    Button(onClick = {}) { Text("主操作") }
    OutlinedButton(onClick = {}) { Text("次操作") }
    TextButton(onClick = {}) { Text("辅助操作") }


    习题 13:如何让 Row 中两个按钮等宽?

    参考答案:

    Row(
    modifier = Modifier.fillMaxWidth(),
    horizontalArrangement = Arrangement.spacedBy(12.dp)
    ) {
    Button(
    onClick = { },
    modifier = Modifier.weight(1f)
    ) {
    Text("主按钮")
    }
    OutlinedButton(
    onClick = { },
    modifier = Modifier.weight(1f)
    ) {
    Text("次按钮")
    }
    }

    解读: 关键在 Modifier.weight(1f)。weight 只能在 RowScope 中使用。Arrangement.spacedBy(12.dp) 负责按钮之间的间距,weight(1f) 负责等宽。两者配合,得到等宽且间距统一的按钮行。


    习题 14:Card 的 shape、elevation、colors 分别控制什么?

    参考答案:

    • shape:卡片形状,控制圆角。
    • elevation:卡片阴影高度。
    • colors:卡片背景色和内容色。

    解读:

    Card(
    shape = RoundedCornerShape(12.dp),
    elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
    colors = CardDefaults.cardColors(
    containerColor = MaterialTheme.colorScheme.surface
    )
    ) { ... }

    这三个参数是卡片样式的主要定制点。颜色建议使用 MaterialTheme.colorScheme,让卡片随主题变化。


    习题 15:综合题:手写一个带圆角、阴影、边框、点击的卡片,不用 Card / Surface。

    参考答案:

    Box(
    modifier = Modifier
    .fillMaxWidth()
    .shadow(4.dp, RoundedCornerShape(12.dp))
    .clip(RoundedCornerShape(12.dp))
    .background(Color.White)
    .border(1.dp, Color.Gray, RoundedCornerShape(12.dp))
    .clickable { }
    .padding(16.dp)
    ) {
    Column {
    Text("手写卡片", style = MaterialTheme.typography.titleMedium)
    Text("圆角、阴影、边框、点击都手动实现。")
    }
    }

    解读: 顺序是:

    shadow -> clip -> background -> border -> clickable -> padding

    • shadow:在背景之前,阴影可见。
    • clip:裁剪内容。
    • background:画背景。
    • border:描边,形状与 clip 一致。
    • clickable:点击范围包括 padding。
    • padding:内边距。

    对比 Card 或 Surface,手写需要 6 个修饰符,代码量明显更多。实际项目中优先用 Card 或 Surface,但理解手写方式能帮你应对自定义场景。

    逆向思维训练: 从效果反推顺序:

  • 要阴影 → shadow 放最外。
  • 要圆角裁剪内容 → clip 紧跟。
  • 要背景 → background。
  • 要边框 → border。
  • 要整块可点击 → clickable。
  • 要内容留白 → padding 放最内。
  • 这就是"按效果推顺序"的能力。


    十三、作业(含参考答案与解读)

    作业 1(必做):完成"卡片样式组件库"

    题目要求:

    • 至少包含:普通卡片、可点击卡片、标签、圆形头像、按钮。
    • 使用 Card、Surface、Modifier 实现。
    • 至少一个 @Preview。

    参考答案:

    参考第十节完整示例。核心组件:

    SimpleCard() // 普通卡片
    ClickableCard() // 可点击卡片
    TagRow() // 标签行
    Avatar("李") // 圆形头像
    ButtonRow() // 按钮行

    解读:

    • 普通卡片用 Card + padding + elevation。
    • 可点击卡片用 Surface(onClick = …)。
    • 标签用 Surface + RoundedCornerShape(50)。
    • 头像用 size -> clip -> background。
    • 按钮用 Button / OutlinedButton + weight。

    作业 2(选做):手写一个带圆角、阴影、边框、点击的卡片

    题目要求:

    • 不使用 Card 或 Surface。
    • 只用 Box 和 Modifier 实现。

    参考答案:

    参考习题 15。

    解读:

    • 顺序:shadow -> clip -> background -> border -> clickable -> padding。
    • 与 Card 对比代码量,理解封装的价值。
    • 实际项目中推荐 Card / Surface,但手写训练能加深对 Modifier 顺序的理解。
    • 建议尝试调整顺序,观察效果变化,加深理解。

    作业 3(提交):代码文件或 Git 仓库链接

    题目要求:

    • 提交代码文件或 Git 仓库链接。

    参考答案:

    项目结构建议:

    ComposeModifier/
    ├── app/
    │ └── src/main/java/com/example/composemodifier/
    │ └── MainActivity.kt
    └── README.md

    解读:

    • 提交前确保项目能编译、能运行、有 @Preview。
    • 不要提交 build/、.gradle/ 等生成目录。
    • README.md 写清楚功能、运行环境、截图。

    作业 4(提交):运行截图

    题目要求:

    • 提交运行截图。

    参考答案:

    建议至少提交:

  • 普通卡片。
  • 可点击卡片(点击状态可见)。
  • 标签行。
  • 圆形头像。
  • 按钮行。
  • 解读:

    • 截图要能证明样式真实运行成功。
    • 如果有水波纹,最好截出点击瞬间的效果。
    • 建议对比"正确顺序"和"错误顺序"的截图,说明 Modifier 顺序的重要性。

    作业 5(提交):200 字学习总结

    题目要求:

    • 写 200 字学习总结:Modifier 顺序、padding 与 background、clip 与 border、clickable 的位置。

    参考答案:

    本课让我理解了 Modifier 的三个核心特性:链式、不可变、顺序敏感。顺序不同,效果完全不同。比如 size -> clip -> background 才能得到圆形头像;background 在 padding 之前会覆盖 padding 区域。Compose 没有 margin,外边距要靠父容器或 padding 实现。clip 负责裁剪,border 负责描边,两者需要传入相同的 shape 才能视觉统一。shadow 要在 background 之前才能可见。clickable 的位置决定点击范围:在 padding 之前点击包括 padding,在 padding 之后点击只有内容区域。通过卡片样式组件库,我学会了用 Card、Surface、Button 定制样式,也手写了一个卡片理解封装的价值。后续我会继续学习状态与重组,让样式随状态变化。

    解读:

    • 总结中应出现关键词:Modifier 顺序、padding、background、clip、border、shadow、clickable、Card、Surface。
    • 结合卡片样式组件库,说明样式定制的组织方式。
    • 如果能写出自己踩过的坑,例如"border 忘记传 shape",会更有深度。
    • 建议结合"洋葱模型"或"包裹语义"描述 Modifier 顺序。

    十四、常见坑与调试建议

  • Modifier 顺序错误
    背景范围、点击范围、圆角效果不符预期,先检查顺序。

  • border 忘记传 shape
    圆角内容 + 矩形边框,视觉不统一。border 的 shape 要和 clip 一致。

  • shadow 被 background 覆盖
    shadow 要在 background 之前。

  • padding 当成 margin 用
    Compose 没有 margin,padding 是内边距。外边距需要父容器控制。

  • clickable 和 padding 顺序搞反
    点击范围不符合预期时,检查 clickable 位置。

  • Card 和 Surface 混用
    通常只需要其中一个。Card 更简单,Surface 更灵活。

  • fillMaxWidth 和 requiredSize 冲突
    requiredSize 会忽略父约束,慎用。

  • clip 和 border 形状不一致
    两者需要相同的 shape 才能视觉统一。建议提取成变量复用。

  • 组件内部忘记应用 modifier
    组件接收 modifier 参数后,要把它应用到最外层,否则外部无法控制间距和尺寸。

  • 硬编码颜色
    优先用 MaterialTheme.colorScheme,让样式随主题变化。

  • shadow 不显示
    检查 elevation 是否大于 0,shape 是否匹配,是否被背景覆盖。

  • background 渐变不显示
    检查 Brush 是否正确传入,shape 是否与 clip 一致。

  • clickable 无水波纹
    检查外层是否有 MaterialTheme,或 indication 是否被设为 null。

  • Modifier 重复叠加
    同一个修饰符叠加多次可能产生意外效果,尽量保持链简洁。

  • 在 Composable 中创建 Modifier 导致频繁重建
    简单场景没问题,但复杂场景建议用 remember 缓存,或提取为常量。


  • 十五、本节课知识点总结

    15.1 Modifier 本质

  • Modifier 是链式、不可变、顺序敏感的修饰符。
  • 每个修饰符返回新的 Modifier。
  • 先写的更靠外,后写的更靠内。
  • 修饰符参与测量、绘制、交互三个阶段。
  • 用"洋葱模型"理解 Modifier 链。
  • then 用于组合两个 Modifier。
  • 15.2 尺寸修饰符

  • size、width、height:固定尺寸。
  • fillMaxWidth、fillMaxSize:占满允许空间。
  • fillMaxWidth(0.5f):按比例填充。
  • wrapContentSize:包裹内容。
  • requiredSize:强制尺寸,忽略父约束。
  • sizeIn:尺寸范围。
  • defaultMinSize:默认最小尺寸。
  • aspectRatio:固定宽高比。
  • 15.3 内外边距

  • padding 是内边距,Compose 没有 margin。
  • 外边距由父容器控制(spacedBy、Spacer)。
  • padding 与 background 顺序决定背景范围。
  • padding 与 size 顺序影响占用空间。
  • 15.4 背景、边框、圆角

  • background:画背景,支持颜色、形状、透明度、渐变。
  • border:描边,需要传入 shape,支持 BorderStroke 和渐变。
  • clip:裁剪内容成指定形状。
  • clip 和 border 使用相同 shape。
  • 推荐顺序:size -> clip -> background -> border。
  • clipToBounds:裁剪越界内容。
  • 15.5 阴影与层级

  • shadow:阴影,要在 background 之前。
  • zIndex:层叠顺序。
  • 优先用 Card / Surface 的 elevation,不手写 shadow。
  • tonalElevation 和 shadowElevation 的区别。
  • 15.6 点击与交互

  • clickable:点击,自带水波纹。
  • combinedClickable:单击、长按、双击。
  • clickable 与 padding 顺序决定点击范围。
  • Surface(onClick = …) 是卡片点击的推荐写法。
  • interactionSource 和 indication 可以自定义点击反馈。
  • 15.7 组件样式定制

  • Card:形状、阴影、颜色、边框。
  • Surface:形状、色调高度、阴影、边框、点击。
  • Button、OutlinedButton、TextButton:按钮体系。
  • contentPadding 控制按钮内容内边距。
  • 优先用 MaterialTheme 中的颜色和形状,让样式随主题变化。
  • 15.8 布局与样式思维

  • 顺序是 Modifier 的灵魂。
  • 先想效果,再决定顺序。
  • 组件接收 modifier: Modifier = Modifier,应用到最外层。
  • 能封装就封装,能用现成组件就用现成组件。
  • 按效果推顺序,是 Modifier 的逆向思维。

  • 十六、下节预告

    第 4 课:状态与重组——让界面真正活起来。

    你将掌握:

    • remember 与 mutableStateOf 的深入原理。
    • mutableIntStateOf、mutableStateListOf 等状态类型。
    • rememberSaveable:配置变更后保存状态。
    • State Hoisting:状态提升的完整模式。
    • 单向数据流:状态向下,事件向上。
    • derivedStateOf:派生状态。
    • snapshotFlow:状态与协程的桥接。
    • 实战:购物车数量、清单勾选、筛选状态。

    附:系列 20 课规划(已更新最后两课)

    课次主题核心内容实战产出
    1 开课与第一个 Compose 声明式 UI、环境搭建、@Composable、@Preview 计数器
    2 布局系统 Column、Row、Box、Spacer、Arrangement、Alignment 个人名片
    3 Modifier 与样式 padding、size、background、border、clip、clickable 卡片样式
    4 状态与重组 remember、mutableStateOf、State Hoisting 购物车数量
    5 列表与脚手架 LazyColumn、LazyRow、Scaffold、TopAppBar 待办列表
    6 输入与表单 TextField、Checkbox、Switch、RadioButton 登录表单
    7 主题与设计系统 Material 3、ColorScheme、Typography、Shape、暗黑模式 一键换肤
    8 导航 Navigation Compose、路由、参数、底部导航 三页导航
    9 架构与状态管理 ViewModel、UiState、单向数据流、Repository 架构版计数器
    10 副作用 LaunchedEffect、DisposableEffect、rememberCoroutineScope 模拟加载数据
    11 动画 animate*AsState、AnimatedVisibility、Crossfade、Transition 展开卡片
    12 手势与交互 clickable、pointerInput、draggable、swipe 滑动删除
    13 自定义布局与绘制 Layout、Canvas、DrawScope、SubcomposeLayout 环形进度条
    14 性能优化 重组范围、稳定性、derivedStateOf、key 优化长列表
    15 测试 Compose UI Test、语义、单元测试 测试计数器
    16 互操作 AndroidView、ComposeView、View/Fragment 混合 嵌入旧 View
    17 工程化 多模块、Hilt/Koin、资源、预览、调试工具 模块拆分
    18 响应式与自适应 WindowSizeClass、平板、折叠屏、多窗口 自适应列表
    19 经典示例深度解读一 选取经典 Compose 示例,深度解读布局、状态、列表、主题、动画、导航等核心实现 拆解并重写关键页面
    20 经典示例深度解读二 选取经典架构示例,深度解读 ViewModel、数据流、网络、分页、测试、性能优化 复刻并扩展一个经典示例
    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【Jetpack Compose声明式UI界面开发实用技术学与练】第 3 课:Modifier 与样式——从能看看到好看
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!