本文涉及 HarmonyOS 6.1.0(23) 与官方穿戴开发指南。文中代码是为说明问题编写的完整示例,不是官方示例的搬运;API 名称与版本号等事实性信息均标注官方出处;涉及真机表现的部分已明确标注,未做任何实测数据编造。


穿戴独立 UX 头图

引子:一套 UI 通吃五端的幻想

跟着官方 MultiWeather Codelab 实操时,最容易产生的幻想是"一套代码、五端通吃"。Codelab 工程的确在 products 层建了三个入口:default(手机/平板)、pc(电脑)、wearable(穿戴)。前两个入口还好说,断点一切、侧边栏一收一嵌,版式自动换。

到了穿戴入口,幻想就破了。

同一个"看天气"业务,手表端的窗体、交互、授权时机全都不一样。V哥当初的直觉是"把手机页缩一缩塞进去",动手之后才明白:穿戴不是手机的小号,它是另一个产品。 这篇文章把V哥踩的三道坎、两个降级和一份自检清单拆给你。


一、第一道坎:窗口不一样,“缩小"不是"适配”

手表屏幕窗口和手机差得比想象中大。官方穿戴开发指南里点得很直接——智能穿戴设备表盘屏幕尺寸较小,有圆形屏和方形屏两种屏幕形态(智能穿戴应用开发)。以 WATCH 5 为例,分辨率 466×466 px、约 233×233 vp,竖屏且不支持旋转。

所以"缩小"第一步就会出事:手机页四角的信息,在圆形屏上直接被裁掉。

V哥把手机上的常见元素映射到手表该怎么做,整理成一张表,加了一列"V哥的处理":

手机上的元素手表上该怎么做V哥的处理
底部 TabBar(多 Tab 切换)改成竖向滑动翻页,一次看一屏Swiper().vertical(true) 承载信息页,不保留横向 Tab
多列网格(天气指标铺开)单列竖向信息流,一屏 2-4 项每个指标独立成卡,随翻页切换
侧边栏 / 弹窗(管理城市)整页跳转,不在小窗里塞进独立页,手表上不弹侧边栏
角落角标 / 状态信息移到圆心安全区,四角不放关键信息所有核心文案居中,避开边缘 16vp
长列表(城市搜索)缩短,默认展示热门 + 搜索入口列表项高度放大到可点,避免误触

手机页 vs 手表页对比

V哥的判断口诀只有九个字:缩小不是适配,重排才是。 你做的不是等比缩放,是把一屏信息重新分配成"一屏一件事"。哪个元素该留、该合并、该砍,得按手表重新想一遍,不能靠缩放器。


二、第二道坎:页面序列要重排,用竖向滑动翻页

手机端的天气是"首页 + 管理城市 + 搜索城市"三个页面靠导航跳转。手表上这么走太累——屏幕小、手指粗,每多一次跳转就多一次流失。

Codelab 的穿戴设计是:授权后进首页看当前城市基本天气,上下滑动进入预报、紫外线、湿度等其它信息页(MultiWeather Codelab · wearable 入口)。也就是把"多页面跳转"压成"单页竖向翻页"。

V哥写了一个最小可用的竖向翻页承载:

// products/wearable/src/main/ets/pages/WeatherEntry.ets
@Entry
@Component
struct WeatherEntry {
  @State authorized: boolean = false; // 实际由权限检查得出,此处省略查询
  @State cityName: string = '当前城市';

  build() {
    if (!this.authorized) {
      // 未授权时给降级提示页(见第五节),绝不空白或退出
      LocationFallbackPage()
      return;
    }
    // 已授权:竖向滑动翻页承载多张信息页
    Swiper() {
      WeatherNowCard({ city: this.cityName })   // 当前天气
      WeekForecastCard()                        // 多日预报
      UltravioletCard()                         // 紫外线
      HumidityCard()                            // 湿度
    }
    .vertical(true)     // 手表竖向翻页,符合上下滑手势
    .indicator(true)    // 小屏保留圆点指示器,提示还有下一页
    .loop(false)        // 天气页不循环,首尾即边界
  }
}

竖向滑动翻页示意

这里有两处V哥自己加的东西值得说:第一,indicator(true) V哥特意开着。手机上V哥习惯关掉指示器省空间,但手表一屏一信息,用户不知道"下面还有"会以为功能只有一页;第二,.loop(false)。天气信息有顺序感,循环滑动反而让人找不到头。圆形屏上还可以换官方提供的 ArcSwiper,这里用通用 Swiper 是为了把逻辑讲清楚。


三、入口与独立 hap:WearableAbility 到底是不是官方类

很多人看到 Codelab 里的 WearableAbility 会以为这是个新 Ability 类型。V哥查了官方文档确认:它不是官方类。

WearableAbility 是 MultiWeather Codelab 在 products/wearable 模块里,给 Stage 模型 UIAbility 起的类名(本质 extends UIAbility)。官方穿戴指南里那个屏幕常亮的示例文件也叫 WearableAbility.ets,但代码里用的还是 onWindowStageCreate + window.getLastWindow——典型的 UIAbility 写法。

真正让穿戴成为一个"独立 hap"的,是工程配置:在 products 层单独建 wearable 模块,并在它的 module.json5 里把 deviceType 配成穿戴:

// products/wearable/src/main/module.json5(节选)
{
  "module": {
    "name": "wearable",
    "type": "entry",
    "deviceType": ["wearable"],   // 只装到穿戴设备
    "requestPermissions": [
      {
        "name": "ohos.permission.APPROXIMATELY_LOCATION",
        "reason": "$string:location_reason",
        "usedScene": { "abilities": ["WearableAbility"], "when": "inuse" }
      },
      {
        "name": "ohos.permission.LOCATION",
        "reason": "$string:location_reason",
        "usedScene": { "abilities": ["WearableAbility"], "when": "inuse" }
      }
    ]
  }
}

入口 Ability V哥建议就用 Codelab 的命名习惯,但心里清楚它就是 UIAbility

// products/wearable/src/main/ets/wearableability/WearableAbility.ets
import { UIAbility, window } from '@kit.AbilityKit';
import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

// 本质仍是 Stage 模型 UIAbility;WearableAbility 只是本工程的类名习惯
export default class WearableAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage): void {
    // 首屏内容先加载,权限申请必须在其后调用,否则会失败
    windowStage.loadContent('pages/WeatherEntry', (err: BusinessError) => {
      if (err) { return; }
    });
    this.requestLocationAtLaunch();
  }

  // 穿戴应用启动即需要定位(看当前城市天气),故在窗口就绪后申请
  private requestLocationAtLaunch(): void {
    const atManager = abilityAccessCtrl.createAtManager();
    const permissions: Permissions[] = [
      'ohos.permission.APPROXIMATELY_LOCATION',
      'ohos.permission.LOCATION',
    ];
    atManager.requestPermissionsFromUser(this.context, permissions,
      (err: BusinessError, _data) => {
        // 申请结果仅记录,首屏由页面读取授权状态自行分支
        if (err) { /* 交给页面降级处理 */ }
      });
  }
}

V哥的处理原则:入口 Ability 放 products/wearable,业务视图(天气卡片)放 features/weather 复用,差异只在穿戴端这一层。 这正是 Codelab 分层架构的用意——核心业务多端一致,界面交互各自定制。


四、定位授权时机:手表和手机不一样

同一个定位权限,手机和手表的触发时机不同,这是V哥实操时最容易忽略的点。

课件里的事实写得很清楚:手机/平板在"搜索城市页点立即添加"时才申请定位;穿戴设备是启动应用即弹窗提示授权,因为手表的首屏就是"当前城市天气",没定位就没内容(MultiWeather Codelab · 定位权限)。

权限本身有个硬约束必须记住:精确位置权限 LOCATION 需要和模糊位置权限 APPROXIMATELY_LOCATION 一起申请,缺一不可开放权限(用户授权))。LOCATION 起始版本 7,APPROXIMATELY_LOCATION 起始版本 9。

V哥把"检查 + 申请 + 被拒引导去设置"封装成一个文件,避免每个页面各写一遍:

// common/utils/LocationPerm.ets
import { abilityAccessCtrl, Permissions, common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { Want } from '@kit.AbilityKit';

const LOCATION_PERMISSIONS: Permissions[] = [
  'ohos.permission.APPROXIMATELY_LOCATION',
  'ohos.permission.LOCATION',
];

// 两个权限都拿到才算授权(返回 true 表示已授权)
export function hasLocationPermission(ctx: common.UIAbilityContext): boolean {
  const atManager = abilityAccessCtrl.createAtManager();
  const tokenId = ctx.applicationInfo.accessTokenId;
  const exact = atManager.checkAccessToken(tokenId, 'ohos.permission.LOCATION');
  const approx = atManager.checkAccessToken(tokenId, 'ohos.permission.APPROXIMATELY_LOCATION');
  return exact === 0 && approx === 0;
}

// 首次申请;被拒后回调 onDenied,由页面引导去设置
export function requestLocationPermission(
  ctx: common.UIAbilityContext,
  onGranted: () => void,
  onDenied: () => void
): void {
  const atManager = abilityAccessCtrl.createAtManager();
  atManager.requestPermissionsFromUser(ctx, LOCATION_PERMISSIONS,
    (err: BusinessError, data) => {
      if (err) { onDenied(); return; }
      const allGranted = data.authResults.every((r) => r === 0);
      allGranted ? onGranted() : onDenied();
    });
}

// 被拒后跳转应用设置页,让用户手动开启
export function openAppLocationSetting(ctx: common.UIAbilityContext): void {
  const want: Want = {
    bundleName: 'com.huawei.hmos.settings',
    abilityName: 'com.huawei.hmos.settings.MainAbility',
    uri: 'application_info_entry',
    parameters: { pushParams: ctx.abilityInfo.bundleName },
  };
  ctx.startAbility(want);
}

授权时机的判断V哥总结成一句:手表的首屏依赖定位,就启动即申请;手机的首屏不依赖定位,就用到再申请。 别把手机的"懒申请"照搬给手表,否则手表一开屏就是空页。


五、未授权必须有降级提示页——这是铁律

这是整篇最重要的一节,也是V哥栽过跟头的地方。

穿戴设备未授权定位时,Codelab 的做法是显示"位置信息授权页",提示用户去设置,而不是白屏或退出。V哥把这个降级页写成组件:

// products/wearable/src/main/ets/view/LocationFallbackPage.ets
@Component
export struct LocationFallbackPage {
  @State tip: string = '授权后为你显示当前城市的天气';

  build() {
    Column() {
      Text('需要定位权限')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#16283D')
      Text(this.tip)
        .fontSize(13)
        .fontColor('#6B7F96')
        .margin({ top: 8, bottom: 20 })
      Button('去设置开启')
        .width(160)
        .height(48)
        .onClick(() => {
          // 跳应用设置页,把决定权交回用户
          openAppLocationSetting(getContext() as common.UIAbilityContext);
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

授权降级流程图

V哥的铁律口诀,建议你贴在评审会上:

手表上没有"再看看",只有"能用"和"卸载"——授权页不做降级,用户下一步就是卸载。

这句话是分析出来的,不是编的。手表屏幕小、装应用成本高、用户耐心比手机薄。你给他一个白屏或"无权限退出",他不会去翻设置,他直接划掉卸载。所以降级页不是"锦上添花",是"保命底线":明确告诉用户缺什么、点哪里补。


六、穿戴真机调试要单独走

穿戴调试和手机不是一套流程。V哥实操时踩过:模拟器能跑,真机连不上。

要点就三条:

  1. 单独签名:穿戴 hap 要在 DevEco 里对 wearable 模块单独配置签名,不能和手机共用一套就以为万事大吉;
  2. IP 连接真机:手表走"设置 → 关于 → 软件版本"连点开启开发者选项,再开 HDC 调试 + Wi-Fi 调试,用 Tools → IP Connection 连;
  3. 圆形屏预览:圆形屏要在 Previewer 里切圆形模具看,方形布局在圆形上四角会被切,这一步只能在真机/圆形预览确认。

涉及真机表现(翻页手感、圆形屏裁切、授权弹窗时机),以真机实测为准。 V哥没有在多种手表真机上逐行验证,文中只给可溯源的官方事实与V哥整理的判断流程。


七、上线自检清单

  • 手表页是"重排"不是"缩放"?四角信息挪到安全区了吗?
  • 信息流单列、每屏 2-4 项,没有把手机多列直接搬过来?
  • 页面序列用竖向 Swiper().vertical(true) 承载,指示器按手表保留?
  • 入口 Ability 在 products/wearabledeviceType 配成 wearable
  • LOCATIONAPPROXIMATELY_LOCATION 两个权限同时声明且同时申请?
  • 授权时机对吗?手表首屏依赖定位 → 启动即申请;手机 → 用到再申请?
  • 未授权时有降级提示页,且能一键跳应用设置页?
  • 被拒后走 requestPermissionOnSetting 或跳转设置,不是直接放弃?
  • wearable 模块单独签名、能 IP 连真机?
  • 圆形屏在预览/真机上验证过四角不被裁?

参考与出处

本文涉及的事实性信息(API 名称、枚举取值、版本号、官方约束)来自以下官方文档,文中的结构、代码示例、决策流程与自检清单为本人整理编写:


最后一句:穿戴 UX 难的不是 API——SwiperrequestPermissionsFromUser 就那几行。难的是承认"手表是另一个产品":把页面重排、把授权时机改对、把降级页当成底线。这三件想清楚,手表端才立得住;想不清楚,装上去就是卸载。

Logo

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

更多推荐