3. Wear OS界面设计基础

Wear OS 的界面设计与手机端最大的差异,来自屏幕形态:绝大多数设备是圆形表盘,可用区域是一个内切于方形的圆。如果直接把手机上的布局搬到手表上,最常见的后果是文字被裁掉、按钮贴边、内容被系统表冠或底部手势区遮挡。Wear Compose 提供了一套面向手表的组件与布局约定,本节围绕 Scaffold、Text、Button 三个最基础的构件,讲清楚在圆形屏幕上如何把界面搭起来并保证可读、可点。

一、先理解圆形屏幕的“安全区”

Wear Compose 的布局坐标系与常规 Compose 一致:原点在左上角,x 向右为正,y 向下为正,单位是 dp。区别在于,圆形屏幕的四个角是无效区域。系统通过 Scaffold 暴露的 contentPadding(内边距)告诉我们哪些区域是安全的。这个内边距不是固定值,它会随设备形状(圆形/方形)、是否有系统指示器而变化。

因此有一条基本纪律:所有内容都应放在 Scaffold 提供的内边距之内,不要用硬编码的 padding 去“猜”圆形边界。 一个常见的错误是给根布局写死 padding(16.dp),在圆形表盘上,靠近顶部和底部的文字仍可能被裁切,因为圆形在垂直方向的有效宽度远小于水平方向。

判断内容是否越界,可以在预览中切换到圆形设备(如 Wear OS Small Round),并打开“显示布局边界”,观察内容是否落在圆形内切区域内。

二、Scaffold:手表界面的骨架

Scaffold 是 Wear Compose 中承载页面结构的容器,它负责处理屏幕形状带来的内边距,并预留时间、位置等系统指示器的位置。一个最小可用的手表页面结构如下:

@Composable
fun SimpleWatchScreen() {
    Scaffold(
        timeText = { TimeText() },          // 顶部时间,系统自动避让
        vignette = { Vignette(vignettePosition = VignettePosition.Top) }
    ) { contentPadding ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(contentPadding),   // 关键:使用系统给的安全内边距
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center
        ) {
            Text(text = "今日步数")
            Text(text = "8,420", style = MaterialTheme.typography.displayMedium)
        }
    }
}

这里有两个要点。第一,timeText 让系统在顶部绘制时间,Scaffold 会自动把内容往下推,避免与时间重叠。第二,contentPadding 必须真正应用到内容容器上,否则安全区形同虚设。如果页面需要滚动,应把 contentPadding 传给 ScalingLazyColumn 的 contentPadding 参数,而不是套在外层 padding,否则滚动时内容会被裁切。

三、Text:圆形屏幕上的排版约束

圆形屏幕的可用宽度随垂直位置变化:越靠近顶部和底部,可用宽度越窄。因此文字排版要遵循“短、居中、少行”的原则。Wear Compose 的 Text 与标准 Compose 用法一致,但需要主动约束宽度与行数:

Text(
    text = "今日步数",
    style = MaterialTheme.typography.titleMedium,
    textAlign = TextAlign.Center,
    maxLines = 1,
    overflow = TextOverflow.Ellipsis,
    modifier = Modifier.fillMaxWidth()
)

建议正文使用 bodyMedium 及以上字号,手表上小于 12sp 的文字在强光下几乎不可读。数值类信息可用 displayMedium 突出,但同样要限制 maxLines。若一段说明必须超过两行,应改用滚动列表或拆分为多个页面,而不是压缩字号。

四、Button:可点击区域与圆形适配

手表上的点击目标比手机更苛刻,因为手指遮挡面积大。Wear Compose 推荐使用 Button 或 CompactButton,并保证最小点击尺寸不小于 48dp。在圆形屏幕上,按钮通常放在屏幕中央或底部中央,避免放在左右两侧的圆弧边缘。

Button(
    onClick = { /* 处理点击 */ },
    modifier = Modifier
        .fillMaxWidth(0.8f)   // 留出两侧圆弧余量
        .height(48.dp)
) {
    Text(text = "开始记录")
}

把按钮宽度设为屏幕宽度的 80% 左右,是一个实用的经验值:既保证点击面积,又不会顶到圆形边缘。若页面同时有多个按钮,应使用 Column 垂直排列并保持足够间距,不要横向并排,因为圆形屏幕横向空间在上下两端急剧收窄。

圆形屏幕安全区与内容布局 圆形有效区 contentPadding 安全边界 10:24 今日步数 8,420 开始记录 ✕ 越界文字会被裁切 布局要点 内容放入 contentPadding 内 文字居中、maxLines 限制行数 按钮宽度约屏宽 80% 点击目标不小于 48dp 避免左右圆弧边缘放控件 不要硬编码 padding 猜边界 Scaffold 负责安全区,组件负责内容

五、把三者组合成一个可运行页面

下面给出一个完整的示例:顶部显示时间,中间显示步数,底部一个按钮。示例参数(步数 8420、按钮文案“开始记录”)为教学假设值,实际运行时由数据源提供。

@Composable
fun StepScreen(onStart: () -> Unit) {
    Scaffold(timeText = { TimeText() }) { padding ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(padding)
                .padding(horizontal = 12.dp),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center
        ) {
            Text(
                text = "今日步数",
                style = MaterialTheme.typography.titleMedium,
                textAlign = TextAlign.Center,
                maxLines = 1
            )
            Spacer(Modifier.height(4.dp))
            Text(
                text = "8,420",
                style = MaterialTheme.typography.displayMedium,
                textAlign = TextAlign.Center,
                maxLines = 1
            )
            Spacer(Modifier.height(16.dp))
            Button(
                onClick = onStart,
                modifier = Modifier.fillMaxWidth(0.8f).height(48.dp)
            ) {
                Text("开始记录", maxLines = 1)
            }
        }
    }
}

运行后应观察三点:时间是否与内容重叠、步数数字是否完整显示、按钮是否可稳定点击。若在圆形预览中数字被裁切,优先减小字号或缩短文案,而不是继续缩小 padding。若按钮点击不灵敏,检查高度是否达到 48dp、是否被其他元素覆盖。

六、常见问题与排查

现象可能原因处理方式
顶部文字被时间遮挡未使用 Scaffold 的 timeText 或未应用 contentPadding启用 timeText,并把 contentPadding 应用到内容容器
文字在圆形边缘被裁切内容超出安全区,或硬编码 padding改用 contentPadding,缩短文案,限制 maxLines
按钮点不中高度不足 48dp 或被遮挡设置 height(48.dp),检查层级顺序
滚动列表内容被裁contentPadding 套在外层而非传给列表把 contentPadding 传给 ScalingLazyColumn 的 contentPadding 参数

掌握 Scaffold 的安全区、Text 的排版约束和 Button 的点击尺寸,就具备了在圆形表盘上搭建可用界面的基础能力。后续更复杂的列表、卡片与导航,都建立在这三个构件的正确使用之上。