从 0 开始 #03

用 Compose 写出会响应的页面

页面怎么写?用户操作后,页面怎么更新?

共享 Compose 界面 · 支持 Android / iOS / 鸿蒙

学习 Compose,可以从三个问题开始:

· 页面怎么写?

· 用户操作后,页面怎么更新?

· 共享页面如何使用平台原生组件?

这一篇先回答前两个问题。我们会用 Compose 的基础组件搭出页面,再给输入框、选择组件和卡片加入状态与交互。

示例工程通过三个提交逐步完成:第一个提交搭建静态组件页面,第二个提交加入状态与交互,第三个提交接入平台原生底栏。本篇使用前两个提交。

在 AtomGit 获取示例工程

在 AtomGit 获取示例工程

示例使用 Material 2,共享界面支持 Android、iOS 和鸿蒙。

01 使用组件搭建界面

· 用函数描述界面

在 Compose 中,界面由带有 @Composable 注解的函数描述。文本、按钮、输入框和布局容器,都可以通过函数调用组合起来。

第一阶段先使用固定数据,完成组件的排列:

Column(
    modifier = Modifier.fillMaxWidth().padding(20.dp),
    verticalArrangement = Arrangement.spacedBy(16.dp),
) {
    OutlinedTextField(
        value = "",
        onValueChange = {},
        modifier = Modifier.fillMaxWidth(),
        placeholder = { Text("Hello Compose") },
        singleLine = true,
    )
    Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
        Button(onClick = {}, modifier = Modifier.weight(1f)) {
            Text("Button")
        }
        OutlinedButton(onClick = {}, modifier = Modifier.weight(1f)) {
            Text("Outlined Button")
        }
    }
    CircularProgressIndicator(progress = 0.65f)
}

这里有两个层次:

  • Column、Row 决定组件怎么排列。

  • OutlinedTextField 、Button 等组件决定显示什么。

Column 将内容上下排列,Row 将内容左右排列。两个按钮都使用 weight(1f) ,因此会平均分配这一行的剩余宽度。

工程还展示了开关、复选框、单选按钮、滑块和卡片。第一阶段给它们传入固定值,先关注页面结构和组件参数。

· 使用 Modifier 调整布局

Modifier 用来设置组件的尺寸、间距、背景和交互行为。例如:

Modifier.fillMaxWidth().padding(20.dp)

这段代码让组件占满可用宽度,并在内部留出间距。

对于组件较多的页面,可以给 Columnf 添加滚动:

Modifier.verticalScroll(rememberScrollState())

这里的数据量很小,普通 Column 就足够完成示例。

Column 中的 background、padding 代码与右侧 Preview

· 处理主题与安全区域

工程通过 Material 2 的 MaterialTheme 提供统一的颜色和文字样式:

MaterialTheme(
    colors = if (isSystemInDarkTheme()) {
        darkColors()
    } else {
        lightColors()
    }
) {
    AppContent()
}

这里的 AppContent() 代表主题中的页面内容。实际工程直接在主题中组织页面和底栏。

页面还需要避开状态栏、导航栏等系统区域。示例使用 WindowInsets.safeDrawing ,并通过 only 指定需要处理的方向:

Modifier.windowInsetsPadding(
    WindowInsets.safeDrawing.only(
        WindowInsetsSides.Top + WindowInsetsSides.Horizontal
    )
)

这样,顶部内容会避开顶部和左右安全区域,底部区域则由底部布局单独处理。

页面避开状态栏、挖孔与底部系统区域

02 让组件拥有状态与交互

第二个提交保留第一阶段的布局,给组件加入各自的状态。

· 从固定值变成状态

第一阶段,输入框的内容固定为空字符串:

  • ​​​​​​​value = ""onValueChange = {}

要让它能够输入,只需要保存文本,并在输入回调中更新:

var text by rememberSaveable { mutableStateOf("") }
OutlinedTextField(
    value = text,
    onValueChange = { text = it },
    modifier = Modifier.fillMaxWidth(),
    placeholder = { Text("Hello Compose") },
    singleLine = true,
)

整个过程是:

用户输入
  → onValueChange 收到新文本
  → 更新 text
  → Compose 更新读取 text 的界面

我们通过修改状态表达变化,Compose 负责更新相关界面。

TextField 状态更新前后对照:无状态更新 vs 状态正常更新

开关也遵循相同的方式:​​​​​​​

var checked by rememberSaveable { mutableStateOf(true) }
Switch(
    checked = checked,
    onCheckedChange = { checked = it },
)

滑块同样如此:

var value by rememberSaveable { mutableStateOf(0.5f) }Slider(    value = value,    onValueChange = { value = it },)

文本、开关、复选框和滑块各自管理状态,方便单独观察每个组件的行为。

remember 可以在重组之间保留对象或状态;rememberSaveable 还会参与宿主支持的状态保存与恢复。示例中的文本和选中值使用了 rememberSaveable 。

· 让加载指示器动起来

第一阶段给圆形进度条传入固定进度:

CircularProgressIndicator(progress = 0.65f)

第二阶段改为不确定进度模式:

CircularProgressIndicator()

组件会持续播放加载动画。对于这个示例,不需要额外创建定时器来修改进度。

· 用按压状态驱动卡片阴影

卡片的交互稍有不同:我们希望按下时阴影加深,松开后恢复。

关键代码如下:

val interactionSource = remember { MutableInteractionSource() }
val pressed by interactionSource.collectIsPressedAsState()
val elevation by animateDpAsState(if (pressed) 12.dp else 4.dp)
Card(
    modifier = Modifier.clickable(
        interactionSource = interactionSource,
        indication = null,
        onClick = {},
    ),
    elevation = elevation,
) {
    Text("Card")
}

clickable 和 collectIsPressedAsState 使用同一个 interactionSource ,因此可以得到当前是否正在按压。

随后,将按压状态映射为阴影高度:

按下时为 12.dp。

松开时为 4.dp。

animateDpAsState 负责两个值之间的动画。

indication = null 关闭点击水波纹,卡片只通过阴影变化反馈按压。

工程将这部分代码提取为 PressableCard 。后面的菜品卡片也直接复用它。

卡片按压对照:松开 4.dp vs 按下 12.dp

写在最后

到这里,一个可以显示内容、接收输入并响应操作的 Compose 页面已经完成。

第一个提交使用固定数据搭建页面,第二个提交加入输入、选择、滑动和按压状态。第三个提交会在这个基础上完成搜索、菜品列表和购物车,并把共享 Compose 页面与鸿蒙 ArkUI 原生底栏连接起来。

示例工程

https://atomgit.com/qiang6x/demohelloharmony

即刻体验

CPF-KMP-CMP 鸿蒙平台首个正式版本已正式发布。欢迎上手验证、在社区提交 Issue/Discussion 反馈问题与需求,共同打磨 KMP&CMP 在鸿蒙平台上的生态与产品能力。

快速开始:(可复制链接至网页打开)

https://atomgit.com/CPF-KMP-CMP/docs

发布公告:(可复制链接至网页打开)

https://atomgit.com/org/CPF-KMP-CMP/discussions/15

参与社区共建

加入 CPF-KMP-CMP 社区,参与 KMP&CMP 鸿蒙版本规划和技术共建。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐