鸿蒙实战:穿戴手表设备自适应开发框架

鸿蒙智能穿戴设备同时存在圆形屏和方形屏两种形态,本框架通过容器层分叉 + 内容层共用的架构,实现一套代码自适应多种屏幕形态。

核心能力:屏幕类型自动检测 → 容器组件分叉 → 共用业务组件 → 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 渲染      │
└──────────────────────────────────────┘

关键判断点

  1. 屏幕类型检测:通过 display.getDefaultDisplaySync().screenShape 获取,返回值 1 为圆形屏,0 为方形屏
  2. 尺寸分级:基于屏幕宽度(vp)分为 small、standard、large 三级
  3. 容器选择:圆形屏使用弧形容器(ArcList),方形屏使用标准容器(List)
  4. Token 应用:根据屏幕类型和尺寸,自动选择对应的 Token 值
1.3 容器层分叉

![[屏幕截图 2026-06-21 210557.png]]

对于两种屏幕布局差异较大的情况下,例如List构成的页面可以在圆形屏时使用ArcList构建页面,分别引入方形屏和圆形屏。对于风格一致的,则可以直接复用。如上图为应用首页导航,采用List和arcList两种组件分别实现。详情可看下一章教程以及项目源码。

屏幕类型 容器组件 特点
圆形屏 ArcList + ArcListItem 弧形裁切、表冠灵敏度、自动圆弧排列
方形屏 List + ListItem 标准容器、四角全利用、线性滚动

分叉点:在页面入口处根据屏幕类型选择不同的容器组件,内部业务组件保持一致。

二、核心文件

2.1 WearScreenUtil 工具类

源码地址:product/wearable/src/main/ets/utils/WearScreenUtil.ets

核心结构

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 根页面布局

源码地址:product/wearable/src/main/ets/pages/Index.ets

维度 圆形屏 方形屏
轮播容器 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 导航点显隐控制逻辑

源码地址:product/wearable/src/main/ets/pages/Index.ets

导航点自动隐藏

状态 行为
首次加载 显示 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 开发流程
  1. 确定屏幕类型:使用 WearScreenUtil.isRoundScreen() 判断
  2. 选择容器:圆形屏用 ArcList,方形屏用 List
  3. 使用 Token:优先使用百分比 Token,避免硬编码
  4. Canvas 缩放:使用 WearScreenUtil.scaledSize() 精确缩放
4.2 注意事项
  • 最小触摸区域统一为 48vp(手表平台强制要求)
  • 圆形屏需要更大的安全边距(避开弧形裁切区域)
  • Canvas 绘制需要考虑缩放比例(以 466vp 为基准)
  • 低版本 API 需要降级处理(_detectShape 中的 try-catch)

参考文档

随易 App 已上架官方应用市场,手表端也在审核中,欢迎大家体验!

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐