4. Wear OS表盘与屏幕适配
Wear OS 设备形态差异极大:同样是 1.4 英寸左右的屏幕,可能是 450×450 的圆形屏(如 Galaxy Watch 系列),也可能是 480×480 的方形屏(部分儿童手表或方形表款),还有 390×390 的圆形屏、454×454 的圆形屏等。如果布局写死尺寸,圆形屏上四角内容会被裁掉,方形屏上又会留下大片空白。本节的核心任务,就是让同一套 Compose 代码在两种屏幕形态下都"看起来是专门为它设计的"。
一、先理解两种屏幕的几何差异
圆形屏的可用区域是一个内切于屏幕矩形的圆,直径等于屏幕短边;方形屏的可用区域是整个矩形。这意味着:
- 圆形屏上,屏幕矩形的四个角是"物理不可见"的,任何放在角落的内容都会被裁切。
- 圆形屏的顶部和底部中央区域虽然可见,但系统可能叠加状态栏、时间或手势提示,需要额外避让。
- 方形屏的四个角可见,但边缘同样需要留出安全边距,避免内容贴边。
因此,适配的本质不是"为圆形和方形各写一套布局",而是让布局感知当前屏幕形状,并据此调整内容的内边距和排布方式。
二、用 BoxWithConstraints 获取可用空间
Compose 中判断屏幕形态最直接的工具是 BoxWithConstraints。它在测量阶段就能拿到父容器给出的最大宽高约束,从而在组合阶段决定子内容如何排布,而不需要等到绘制后再回调。
@Composable
fun AdaptiveWatchScreen() {
BoxWithConstraints(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
val w = maxWidth
val h = maxHeight
// 圆形屏:宽高相等;方形屏:宽高通常也相等,
// 因此用"是否近似正方形"无法区分,需结合设备形状判断
val isRound = w == h && isRoundDevice()
val safeRadius = minOf(w, h) / 2f
val contentPadding = if (isRound) safeRadius * 0.18f else 12.dp
Column(
modifier = Modifier.padding(contentPadding),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("12:30", fontSize = 40.sp)
Text("周三 10月8日", fontSize = 14.sp)
}
}
}
注意:maxWidth == maxHeight 只能说明屏幕是正方形,不能区分圆形与方形。真正的形状判断需要借助 LocalConfiguration 或 android.content.res.Configuration 中的 UI_MODE_TYPE_WATCH 与 isScreenRound 属性。在 Compose 中常用做法是:
val configuration = LocalConfiguration.current
val isRound = configuration.isScreenRound // API 23+ 提供
拿到 isRound 后,再结合 BoxWithConstraints 的 maxWidth/maxHeight,就能同时知道"形状"和"尺寸",这是响应式布局的两个关键输入。
三、圆形屏的安全区域计算
圆形屏上,距离圆心越远,可用高度越小。设屏幕半径为 R,某内容块距离圆心水平偏移为 x,则该处可用的垂直半高为 √(R² − x²)。如果内容块宽度为 w,其左右边缘到圆心距离为 w/2,那么该内容块能安全放置的最大高度为 2√(R² − (w/2)²)。
实际开发中不必每次手算,但需要理解这个约束:越靠近圆形边缘的内容,越要收窄或缩短。一个实用的近似做法是:把圆形屏内容限制在一个内切正方形内,该正方形边长约为直径的 0.707 倍(即 R√2)。例如 450×450 的圆形屏,安全内容区约为 318×318。再在此基础上留 8~12dp 的视觉边距,就能保证绝大多数内容不被裁切。
四、响应式布局的三种策略
根据内容复杂度,可以选择不同粒度的响应式策略:
- 统一内边距策略:所有内容放在一个带
padding的容器里,圆形屏用较大 padding(如 24dp),方形屏用较小 padding(如 12dp)。实现最简单,适合信息量少的表盘。 - 形状分支策略:用
if (isRound) { ... } else { ... }分别组合两套布局。适合圆形屏需要环形排布、方形屏需要网格排布的场景。 - 约束驱动策略:不判断形状,只根据
maxWidth和maxHeight动态计算字号、间距和列数。例如字号 =maxWidth.value * 0.09,间距 =maxWidth * 0.04。这种写法在圆形和方形上都能自动收敛,是推荐的首选方式。
下面是一个约束驱动的表盘时间区域示例:
BoxWithConstraints(Modifier.fillMaxSize()) {
val unit = minOf(maxWidth, maxHeight)
val timeSize = (unit.value * 0.22f).sp
val dateSize = (unit.value * 0.07f).sp
val gap = (unit.value * 0.03f).dp
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize()
) {
Text("12:30", fontSize = timeSize, fontWeight = FontWeight.Bold)
Spacer(Modifier.height(gap))
Text("周三 10月8日", fontSize = dateSize)
}
}
这段代码在 450×450 圆形屏上,时间字号约为 99sp;在 480×480 方形屏上约为 105sp。两者都保持与屏幕尺寸成比例,不会出现"圆形屏上字太大被裁、方形屏上字太小显空"的问题。
五、边距与安全区域的工程约定
Wear OS 官方设计指南建议:圆形屏内容距离屏幕边缘至少保留 12dp 的视觉边距,方形屏至少 8dp。对于表盘这类需要显示在息屏状态下的界面,还要考虑 AmbientMode 下系统可能叠加的提示区域,通常顶部和底部各预留约 20dp。
在 Compose 中,可以用 WindowInsets 获取系统栏和手势区域,但 Wear OS 上更常用的是直接使用固定安全边距,因为手表屏幕小、系统栏通常不占布局空间。推荐做法是定义一个 WatchDimens 对象统一管理:
object WatchDimens {
val roundPadding = 24.dp
val squarePadding = 12.dp
val ambientTopInset = 20.dp
val ambientBottomInset = 20.dp
}
然后在根布局中根据 isRound 选择对应值。这样所有页面共享同一套边距约定,避免每个页面各写各的魔法数字。
六、验证适配效果
开发阶段可以用 Wear OS 模拟器快速切换圆形和方形皮肤:在 AVD Manager 中创建 Wear OS 设备时,选择 "Wear OS Round" 和 "Wear OS Square" 两种硬件配置。运行后重点检查三件事:
- 圆形屏四角是否有内容被裁切(尤其是长文本和图片)。
- 方形屏上内容是否过度居中、四周留白过大。
- 两种屏幕下字号、间距是否与屏幕尺寸成比例,而不是简单缩放。
如果使用约束驱动策略,通常一次编写即可在两种屏幕上获得可接受的效果;如果使用形状分支策略,则需要分别验证两套布局的边界情况。无论哪种方式,都建议在真机或模拟器上实际截图对比,而不是仅凭代码推断。
Modifier.fillMaxSize() 后直接放内容而不加任何 padding。在圆形屏上,这几乎必然导致边缘内容被裁切;在方形屏上,内容会紧贴边框,视觉上非常局促。
总结来说,Wear OS 屏幕适配的关键是:用 BoxWithConstraints 拿到可用尺寸,用 isScreenRound 判断形状,用比例而非固定值计算字号和间距,用统一的安全边距对象管理内边距。掌握这四点后,同一套表盘代码就能在圆形和方形屏幕上都有得体的表现。