鸿蒙实战:穿戴手表设备自适应开发框架
鸿蒙实战:穿戴手表设备自适应开发框架
鸿蒙智能穿戴设备同时存在圆形屏和方形屏两种形态,本框架通过容器层分叉 + 内容层共用的架构,实现一套代码自适应多种屏幕形态。
核心能力:屏幕类型自动检测 → 容器组件分叉 → 共用业务组件 → Token 统一配置
交互亮点:导航点自动隐藏(滑动/表冠时显示,停止后淡出)+ 首页右滑渐进式退出(距离+速度双重判断)
源码地址:参照鸿蒙原生应用随易进行开发实战:product/wearable/ 该文档相关源码均在 wearable 模块内
效果预览:

核心文件结构:
product/wearable/src/main/ets/
├── pages/
│ └── Index.ets ← 根页面(圆/方屏分发 + 右滑退出)
├── sub_pages/
│ ├── WearNavPanelRound.ets ← 圆形屏导航面板(ArcList)
│ ├── WearNavPanelSquare.ets ← 方形屏导航面板(List)
│ └── ... ← 业务组件(按需替换)
├── utils/
│ └── WearScreenUtil.ets ← 屏幕适配工具类(形状检测 + Token)
└── wearableability/
└── WearableAbility.ets ← 入口 Ability
快速开始: WearScreenUtil.ets 以及 Index.ets 是核心框架,可以开箱即用,把子组件放进 Index 的 Swiper 列表里即可。子组件最好使用百分比布局,Index 内已经做了样式判断。
目录:
正文
〇、效果对比
鸿蒙智能穿戴设备同时存在圆形屏和方形屏两种形态,传统方案需要维护多套代码。本框架实现了一套通过容器层分叉 + 内容层共用的架构,实现一套代码自适应多种屏幕。并实现了优雅的导航点显隐与右滑退出应用。
- 主要分支逻辑在Index首页,方形屏使用Swiper,圆形屏使用arcSwiper。使用时将子组件放进两种Swiper的子组件里即可。
- 内置方形、圆形两种隐私政策浮窗模版。使用时替换隐私政策内容即可。内置隐私政策持久化逻辑,
- 内置方形、圆形两种设置页模版
- 导航点样式优化+动态显隐
- 右滑退出应用动画
| 维度 | 传统方案 | 本框架方案 |
|---|---|---|
| 适配方式 | 多套代码 | 一套代码自适应 |
| 屏幕支持 | 单一 | 圆形+方形 |
| 组件复用 | 低 | 高(共用功能组件) |
| 扩展性 | 差 | 好(Token化设计) |
一、架构设计
1.1 三层递进适配模型

核心思想:容器层分叉 + 内容层共用
框架采用三层递进的适配策略,从基础单位到统一配置,逐层解决不同屏幕形态的适配问题:
┌─────────────────────────────────────────────────────────┐
│ 第1层:vp/fp 基线 │
│ - 使用 vp 作为长度单位,fp 作为字号单位 │
│ - 屏蔽不同设备的物理像素差异 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 第2层:百分比布局 │
│ - 主图、内容区等使用百分比宽度 │
│ - 根据屏幕类型(圆/方)自动调整比例 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 第3层:WearScreenUtil Token │
│ - 统一管理所有适配参数 │
│ - 提供安全边距、字号、缩放等 Token │
└─────────────────────────────────────────────────────────┘
1.2 分层判断逻辑
整个适配流程遵循以下判断逻辑:
启动应用
│
▼
┌──────────────────────────────────────┐
│ 检测屏幕类型 │
│ display.getDefaultDisplaySync() │
│ .screenShape │
└──────────────────────────────────────┘
│
├─ ScreenShape.ROUND (1) ──→ 圆形屏
│
└─ ScreenShape.RECTANGLE (0) ──→ 方形屏
│
▼
┌──────────────────────────────────────┐
│ 检测屏幕尺寸 │
│ screenWidth < 340vp → small │
│ 340vp ≤ screenWidth ≤ 460vp → std │
│ screenWidth > 460vp → large │
└──────────────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ 选择容器组件 │
│ 圆形屏 → ArcList / ArcSwiper │
│ 方形屏 → List / Swiper │
└──────────────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ 应用 Token 配置 │
│ - safePadding(安全边距) │
│ - mainImageWidth(主图宽度) │
│ - resultFontSize(结果字号) │
│ - scaledSize(Canvas缩放) │
└──────────────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ 渲染共用功能组件 │
│ 所有业务组件使用统一的 Token 渲染 │
└──────────────────────────────────────┘
关键判断点:
- 屏幕类型检测:通过
display.getDefaultDisplaySync().screenShape获取,返回值1为圆形屏,0为方形屏 - 尺寸分级:基于屏幕宽度(vp)分为 small、standard、large 三级
- 容器选择:圆形屏使用弧形容器(ArcList),方形屏使用标准容器(List)
- Token 应用:根据屏幕类型和尺寸,自动选择对应的 Token 值
1.3 容器层分叉
![[屏幕截图 2026-06-21 210557.png]]
对于两种屏幕布局差异较大的情况下,例如List构成的页面可以在圆形屏时使用ArcList构建页面,分别引入方形屏和圆形屏。对于风格一致的,则可以直接复用。如上图为应用首页导航,采用List和arcList两种组件分别实现。详情可看下一章教程以及项目源码。
| 屏幕类型 | 容器组件 | 特点 |
|---|---|---|
| 圆形屏 | ArcList + ArcListItem | 弧形裁切、表冠灵敏度、自动圆弧排列 |
| 方形屏 | List + ListItem | 标准容器、四角全利用、线性滚动 |
分叉点:在页面入口处根据屏幕类型选择不同的容器组件,内部业务组件保持一致。
二、核心文件
2.1 WearScreenUtil 工具类
核心结构:
export default class WearScreenUtil {
// ==================== 形状检测 ====================
static isRoundScreen(): boolean // 是否为圆形屏
static isSquareScreen(): boolean // 是否为方形屏
// ==================== 屏幕尺寸 ====================
static get screenWidth(): number // 屏幕宽度 (vp)
static get screenHeight(): number // 屏幕高度 (vp)
static get screenSize(): string // 尺寸分级:small/standard/large
// ==================== 设计 Token ====================
static get safePadding(): number // 安全边距 (vp)
static get contentWidthRatio(): string // 内容区宽度占比
static get mainImageWidth(): string // 主图宽度占比
static get resultFontSize(): number // 结果字号 (fp)
static get subFontSize(): number // 辅助字号 (fp)
static get buttonFontSize(): number // 按钮字号 (fp)
static get listItemSpace(): number // 列表项间距 (vp)
static get minTouchSize(): number // 最小触摸区域 (vp)
// ==================== 缩放工具 ====================
static scaledSize(standardValue: number): number // 按屏幕尺寸缩放
}
尺寸分级策略:智能穿戴当前只包括Watch5、Utimate2、超新星X1pro,具体数值可以自行调整,您也可以先跳过这一步。该文件是为了将来新尺寸设备做准备。
| 分级 | 宽度范围 | 典型设备 |
|---|---|---|
| small | < 340 vp | Watch D 320×320 |
| standard | 340-460 vp | Watch GT 4 466×466 |
| large | > 460 vp | 未来大屏手表(预留) |
设计 Token 对照表:
| Token | 圆形small | 圆形standard | 方形small | 方形standard |
|---|---|---|---|---|
| safePadding | 18 | 24 | 8 | 12 |
| contentWidthRatio | 65% | 70% | 88% | 90% |
| mainImageWidth | 48% | 55% | 62% | 70% |
| resultFontSize | 40fp | 48fp | 40fp | 48fp |
| listItemSpace | 12 | 16 | 8 | 12 |
核心逻辑:
// 形状检测:基于 display.ScreenShape 检测(API 18+)
static isRoundScreen(): boolean {
if (!WearScreenUtil._shapeDetected) {
WearScreenUtil._detectShape();
}
return WearScreenUtil._isRound === true;
}
private static _detectShape(): void {
WearScreenUtil._shapeDetected = true;
try {
const d = display.getDefaultDisplaySync();
// ScreenShape.ROUND = 1, ScreenShape.RECTANGLE = 0
const shape = d.screenShape;
WearScreenUtil._isRound = (shape === 1);
} catch (e) {
// 低版本 API 或获取失败 → 默认方形
WearScreenUtil._isRound = false;
}
}
// 尺寸分级:基于屏幕宽度自动判断
static get screenSize(): string {
if (WearScreenUtil._screenSize === null) {
const w = WearScreenUtil.screenWidth;
if (w < 340) {
WearScreenUtil._screenSize = 'small';
} else if (w > 460) {
WearScreenUtil._screenSize = 'large';
} else {
WearScreenUtil._screenSize = 'standard';
}
}
return WearScreenUtil._screenSize;
}
// 缩放工具:以 466vp(Watch GT 4 圆形屏)为基准
static scaledSize(standardValue: number): number {
const scale = WearScreenUtil.screenWidth / 466;
return Math.max(standardValue * scale, standardValue * 0.7); // 最小缩到 70%
}
2.2 Index 根页面布局
| 维度 | 圆形屏 | 方形屏 |
|---|---|---|
| 轮播容器 | ArcSwiper |
Swiper |
| 表冠 | digitalCrownSensitivity(MEDIUM) |
无 |
| 页面裁切 | 每页 borderRadius(screenWidth/2) 裁圆 |
无裁切 |
圆形屏(buildRoundLayout + buildRoundPages):
@Builder
buildRoundLayout() {
ArcSwiper(this.arcController) {
this.buildRoundPages()
}
.digitalCrownSensitivity(CrownSensitivity.MEDIUM)
// ...
}
@Builder
buildRoundPages() {
// 每页外包圆形裁切
Stack() { WearNavPanelRound({ onNavigate: this.onNavigate }) }
.width('100%').height('100%')
.borderRadius(WearScreenUtil.screenWidth / 2)
.clip(true)
}
方形屏(buildSquareLayout + buildSquarePages):
@Builder
buildSquareLayout() {
Swiper(this.swiperController) {
this.buildSquarePages()
}
.loop(false)
// 注意:无 digitalCrownSensitivity
}
@Builder
buildSquarePages() {
// 不裁切
Stack() { WearNavPanelSquare({ onNavigate: this.onNavigate }) }
.width('100%').height('100%')
}
2.3 NavPanel 导航面板
圆形屏源码:product/wearable/src/main/ets/sub_pages/WearNavPanelRound.ets
方形屏源码:product/wearable/src/main/ets/sub_pages/WearNavPanelSquare.ets
| 维度 | 圆形屏 | 方形屏 |
|---|---|---|
| 滚动容器 | ArcList + ArcListItem |
List + ListItem |
| 滚动效果 | 弧形滚动、fadingEdge |
线性滚动、edgeEffect(Spring) |
| 边距控制 | WearScreenUtil.safePadding |
CARD_PAD_H |
| 分组 | 无分组 | ListItemGroup |
圆形屏(WearNavPanelRound):
@Component
export struct WearNavPanelRound {
onNavigate: (index: number) => void = () => {}
private arcScroller: Scroller = new Scroller()
build() {
Stack() {
ArcList({ scroller: this.arcScroller }) {
ArcListItem() {
// 顶部间距
}.height(20)
ArcListItem() {
// 标题 / 公告 / 快捷跳转 / 设置 / 关于
}
.borderRadius(22)
.backgroundColor(CARD_BG)
.padding({
left: WearScreenUtil.safePadding,
right: WearScreenUtil.safePadding
})
}
.width(WearScreenUtil.screenWidth)
.height(WearScreenUtil.screenHeight)
.borderRadius(WearScreenUtil.screenWidth) // 圆形裁切
.space(LengthMetrics.px(6))
.fadingEdge(true)
.scrollBar(BarState.Off)
ArcScrollBar({ scroller: this.arcScroller, state: BarState.Auto })
}
.width('100%')
.height('100%')
}
}
方形屏(WearNavPanelSquare):
@Component
export struct WearNavPanelSquare {
onNavigate: (index: number) => void = () => {}
private listScroller: Scroller = new Scroller()
build() {
Stack() {
List({ scroller: this.listScroller }) {
ListItem() {
// 顶部间距
}.height(24)
ListItem() {
// 标题区域
}
.padding({ left: CARD_PAD_H, right: CARD_PAD_H })
ListItemGroup({ header: this.sectionHeader('公告') }) {
ListItem() {
// 内容卡片
}
.padding({
left: CARD_PAD_H,
right: CARD_PAD_H,
top: CARD_PAD_V,
bottom: CARD_PAD_V
})
.backgroundColor(CARD_BG)
.borderRadius(14)
}
}
.width('100%')
.height('100%')
.scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.Spring)
ScrollBar({
scroller: this.listScroller,
direction: ScrollBarDirection.Vertical,
state: BarState.Auto
})
}
.width('100%')
.height('100%')
}
}
2.4 导航点显隐控制逻辑
导航点自动隐藏:
| 状态 | 行为 |
|---|---|
| 首次加载 | 显示 1.5s 后隐藏 |
| 滑动/表冠 | 即时淡入,停止后 1.5s 淡出 |
方形屏Swiper和圆形屏arcSwiper均已实现,详情参考完整源码
private scheduleHide(delay: number = 2000): void {
if (this.hideTimer) {
clearTimeout(this.hideTimer)
}
this.showIndicator = true
this.hideTimer = setTimeout(() => {
this.showIndicator = false
}, delay) as number
}
2.5 优雅的右滑退出
右滑退出:
在首页(index === 0)右滑时,显示渐进式退出提示。整个交互分为三个阶段:
| 右滑距离 | 阶段 | 视觉反馈 |
|---|---|---|
| 0 ~ 10vp | 隐藏期 | 图标和文字不显示(opacity = 0) |
| 10 ~ 45vp | 提示期 | 旋转箭头图标 + “继续右划退出” 文字,背景渐变为橙色 |
| ≥ 45vp | 确认期 | 红色圆形图标,背景渐变为红色,松手即退出 |
交互流程图:
首页右滑
│
▼
┌──────────────────────────┐
│ 距离 < 10vp │
│ opacity = 0,无视觉反馈 │
└──────────────────────────┘
│
▼
┌──────────────────────────┐
│ 10vp ≤ 距离 < 45vp │
│ 显示旋转箭头 + 文字提示 │
│ 背景色:橙色 │
│ 页面缩放:1 → 0.85 │
└──────────────────────────┘
│
▼
┌──────────────────────────┐
│ 距离 ≥ 45vp │
│ 显示红色圆形图标 │
│ 背景色:红色 │
└──────────────────────────┘
│
▼
┌──────────────────────────┐
│ 松手判断 │
│ 距离 > 45vp 或 速度 > 800 │
│ → terminateSelf() 退出 │
│ 否则 → 弹回原位 │
└──────────────────────────┘
核心代码:
// ═══ 右滑退出 UI ═══
if (this.swipeRightOffset > 0) {
Row() {
// 图标:距离 < 45vp 显示旋转箭头,≥ 45vp 显示红色圆形
if (this.swipeRightOffset >= 45) {
SymbolGlyph($r('sys.symbol.record_circle'))
.fontSize(18 + this.swipeRightOffset * 0.2)
.fontColor([Color.White])
} else {
SymbolGlyph($r('sys.symbol.order_play'))
.fontSize(18 + this.swipeRightOffset * 0.2)
.rotate({ angle: Math.min(this.swipeRightOffset / 45 * 180, 180) })
}
// 文字提示:距离 < 45vp 时显示
if (this.swipeRightOffset < 45) {
Column() {
Text('继').fontSize(10).fontColor(Color.White)
Text('续').fontSize(10).fontColor(Color.White)
Text('右').fontSize(10).fontColor(Color.White)
Text('划').fontSize(10).fontColor(Color.White)
Text('退').fontSize(10).fontColor(Color.White)
Text('出').fontSize(10).fontColor(Color.White)
}
}
}
.width(this.swipeRightOffset + 'vp')
.opacity(Math.min(Math.max((this.swipeRightOffset - 10) / 30, 0), 1))
}
// ═══ 退出判断 ═══
.onAnimationStart((index, targetIndex, extraInfo) => {
if (index === targetIndex &&
(extraInfo.currentOffset > 45 || extraInfo.velocity > 800)
&& index === 0) {
const context = getContext(this) as common.UIAbilityContext
context.terminateSelf() // 退出应用
}
})
// ═══ 背景色渐变 ═══
.backgroundColor(
this.swipeRightOffset >= 45 ? '#55FF3B30' : // 红色
this.swipeRightOffset > 0 ? '#55FF9450' : // 橙色
0x00000000 // 透明
)
表冠退出(圆形屏):
圆形屏还支持通过表冠旋转触发退出,在 onBackPress 中处理:
onBackPress(): boolean {
if (this.currentIndex > 0) {
// 非首页:返回首页
this.arcController.showPrevious()
this.currentIndex = 0
promptAction.showToast({
message: '已返回首页,右滑可退出应用',
duration: 1500
})
return true
}
// 首页:退出应用
const context = getContext(this) as common.UIAbilityContext
context.terminateSelf()
return true
}
三、组件复用
所有组件均可使用 WearScreenUtil 提供的 Token 进行适配,无需关心屏幕类型差异。
这样差异小的页面经过Index的处理,子组件内部不需要太多处理。直接复用即可。
差异较大的页面,例如List构成的页面,则最好解耦,圆形屏时使用arcList构建不同的页面。参考2.3我的Nav页面,在Index首页框架的不同区域
所有组件均采用统一的适配模式:使用百分比 Token 控制布局,使用 scaledSize() 处理 Canvas 绘制。
四、最佳实践
4.1 开发流程
- 确定屏幕类型:使用
WearScreenUtil.isRoundScreen()判断 - 选择容器:圆形屏用 ArcList,方形屏用 List
- 使用 Token:优先使用百分比 Token,避免硬编码
- Canvas 缩放:使用
WearScreenUtil.scaledSize()精确缩放
4.2 注意事项
- 最小触摸区域统一为 48vp(手表平台强制要求)
- 圆形屏需要更大的安全边距(避开弧形裁切区域)
- Canvas 绘制需要考虑缩放比例(以 466vp 为基准)
- 低版本 API 需要降级处理(
_detectShape中的 try-catch)
参考文档
- HarmonyOS 穿戴开发文档
- ArkUI 弧形组件 - 在该官方文档左上角搜索
arc可展示更多弧形组件(ArcList、ArcButton 等) - 随易项目源码
随易 App 已上架官方应用市场,手表端也在审核中,欢迎大家体验!
更多推荐




所有评论(0)