无人机航拍服务是一个高度依赖移动端应用的行业场景——飞手需要在手机上管理航拍任务进度、预览云台画面、起飞前巡检设备、浏览空域资料、下载素材档案并管理设备台账。每一个环节都涉及不同的系统能力:相机预览需要 Camera Kit 的会话管理,素材下载需要 ArkWeb 的下载代理回调,数据可视化需要 Canvas 的 2D 绘制能力。HarmonyOS 在 6.1.1 版本中引入了 ArkWeb 下载双 URL 溯源能力(getOriginalUrl + getReferrerUrl),配合 Camera Kit 的 AUTO_FRAMING 影随人动和 PhotoSession 手动对焦三接口,以及 Canvas 的自定义图表绘制,可以在单页面文件中构建一个覆盖"任务管理→相机预览→起飞巡检→网页浏览→素材下载→个人中心"全链路的航拍服务应用。本文将逐段拆解这个"云镜"无人机航拍服务应用的代码实现,深入分析三大 HarmonyOS 套件在同一页面中的叠加架构与工程细节。


一、技术前置:HarmonyOS 开发栈与三大套件概述

1.1 ArkUI 声明式 UI 范式与状态管理

ArkUI 是 HarmonyOS 应用开发的核心 UI 框架,采用声明式编程范式。开发者通过 @Entry@Component 装饰器声明页面入口和组件结构,在 build() 方法中通过链式调用组织 ColumnRowStackScrollList 等容器组件构建视图树。ArkUI 的状态管理体系以 @State 为核心——任何被 @State 修饰的成员变量发生变化时,框架会自动追踪依赖该变量的所有视图节点并触发重渲染。@Observed 装饰器进一步扩展了状态追踪的粒度,使 class 实例的属性级变更也能被框架感知,这对于列表项数据的精细更新至关重要。
在这里插入图片描述

@Builder 装饰器用于声明可复用的视图片段,无需拆分为独立的 struct 组件,可以在当前组件内部定义和调用,极大降低了视图组织的复杂度。在本应用中,6 个 Tab 的内容区全部通过 @Builder 方法实现,每个 Tab 承载完全不同的布局结构和功能模块。

1.2 Camera Kit 相机服务套件

Camera Kit 是 HarmonyOS 提供的相机能力套件,其核心架构围绕以下组件展开:

在这里插入图片描述

  • CameraManager:相机管理器,通过 camera.getCameraManager(ctx) 获取,负责发现设备、创建输入流和会话。
  • CameraInput:相机输入流,绑定物理摄像头设备。
  • PreviewOutput:预览输出流,绑定到 XComponent 的 Surface 上实现实时预览。
  • VideoSession / PhotoSession:两种会话模式,分别承载不同的相机特性。

在 HarmonyOS 6.1.1 中,Camera Kit 引入了两项关键特性:

在这里插入图片描述
AUTO_FRAMING 影随人动:挂载于 VideoSession,通过 Control Center 控制中心实现。调用链路为 isControlCenterSupported()getSupportedEffectTypes()includes(AUTO_FRAMING)enableControlCenter(true),系统接管画面构图后,人物在移动时画面自动跟随平移缩放。在航拍场景中,当飞手在地面移动操作遥控器时,云台预览画面会自动跟随飞手位置,确保主体始终居中。

手动对焦三接口:挂载于 PhotoSession,包含 isFocusDistanceSupported()(能力查询)、setFocusDistance(value)(设置对焦距离 0.0~1.0)、getFocusDistance()(读回对焦距离)。在起飞前巡检场景中,飞手可以对焦到近距检查电池序列号、对焦到中距检查桨叶裂纹、对焦到远景观察起飞环境。

在这里插入图片描述

1.3 ArkWeb 套件与下载双 URL 溯源

ArkWeb 是 HarmonyOS 的 Web 组件套件,通过 Web 组件嵌入网页视图,配合 webview.WebviewController 控制器实现 URL 加载、页面控制和下载管理。在 API 24(HarmonyOS 6.1.1 对应版本)中,ArkWeb 引入了 WebDownloadDelegate 下载代理和双 URL 溯源能力。

WebDownloadDelegate 提供四个回调接口,覆盖下载的完整生命周期:

  1. onBeforeDownload:下载开始前触发,必须在此调用 item.start(path) 提供沙箱存储路径,否则下载任务永远停在 PENDING 状态。同时可获取建议文件名 getSuggestedFileName()
  2. onDownloadUpdated:下载进行中持续触发,通过 item.getPercentComplete() 获取进度百分比,用于刷新 UI 进度条。
  3. onDownloadFailed:下载失败时触发,通过 item.getGuid() 获取失败标识。
  4. onDownloadFinish:下载完成时触发,此回调中可以调用 6.1.1 新增的双 URL 溯源接口——item.getOriginalUrl() 获取下载项的原始 URL(文件直链来源),item.getReferrerUrl() 获取引用页 URL(触发下载的页面地址)。同时可通过 item.getTotalBytes() 获取文件总大小。

在这里插入图片描述
双 URL 溯源的价值在于:当飞手从不同网页下载同一文件时,原始 URL 记录了文件的实际来源地址,引用页 URL 记录了用户是从哪个页面发起的下载。这对于素材版权追溯和下载路径审计至关重要。

1.4 Canvas 2D 绘制能力

ArkUI 的 Canvas 组件暴露 CanvasRenderingContext2D 上下文,支持路径、弧线、填充、描边、渐变、文字等标准 2D Canvas API。本应用使用 Canvas 绘制两类图表:任务完成率进度环(drawRing)和近 12 次飞行架次时长折线图(drawLine),两者均通过 onReady 回调首绘,再配合呼吸定时器驱动的周期性重绘实现动画效果。

在这里插入图片描述

二、应用全景:无人机航拍服务架构总览

本应用以"云镜"为产品名,定位为面向持证飞手的无人机航拍任务与素材管理平台。整体采用 6 Tab 单排底部导航架构,三大套件(Camera Kit / ArkWeb / Canvas)在同一页面文件中叠加。

在这里插入图片描述

2.1 整体架构流程

┌──────────────────────────────────────────────────────────────┐
│                   云镜主页面 (Page)                            │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  头部区域 (headerMain)                                    │  │
│  │  天空青渐变 Banner + 品牌语 + Tab 联动副标题              │  │
│  │  三枚统计胶囊(任务数/月架次/素材库) + 呼吸圆点              │  │
│  │  搜索条 + 新建任务按钮                                    │  │
│  └────────────────────────────────────────────────────────┘  │
│  ─────────────────── 分割线 ──────────────────────────────  │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  内容区 (6 Tab 切换)                                      │  │
│  │                                                            │  │
│  │  Tab0 任务 │ 进度条清单 + Canvas完成率环/架次折线 + 电池卡 │  │
│  │  Tab1 相机 │ XComponent预览 + 影随人动 + 三效果枚举表      │  │
│  │  Tab2 对焦 │ 能力查询 + 三档巡检预设 + 滑杆 + FocusRecord   │  │
│  │  Tab3 网页 │ 地址栏 + 快捷站点横滑 + Web组件 + 主动下载行   │  │
│  │  Tab4 下载 │ 进行中进度卡 + 双URL溯源列表 + 代码预览卡     │  │
│  │  Tab5 我的 │ 飞手渐变大卡 + 设备清单 + 月度飞行柱状图      │  │
│  └────────────────────────────────────────────────────────┘  │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  底部导航栏 (tabBar) - 6 tab 单排自绘                     │  │
│  └────────────────────────────────────────────────────────┘  │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  弹窗系统 (Stack 覆盖层)                                  │  │
│  │  新建航拍任务 / 修改任务区域 / 取消任务确认               │  │
│  └────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────┘

2.2 三特性叠加关系

┌──────────────────┐  ┌──────────────────┐  ┌──────────────────┐
│   Camera Kit     │  │     ArkWeb       │  │     Canvas       │
│    (特性 A)      │  │    (特性 B)      │  │    (特性 C)      │
├──────────────────┤  ├──────────────────┤  ├──────────────────┤
│ AUTO_FRAMING     │  │ WebDownload      │  │  drawRing        │
│ 影随人动         │  │ Delegate 四回调  │  │  完成率进度环    │
│                  │  │                  │  │                  │
│ setFocusDistance │  │ getOriginalUrl   │  │  drawLine        │
│ getFocusDistance │  │ getReferrerUrl   │  │  架次时长折线图  │
└────────┬─────────┘  └────────┬─────────┘  └────────┬─────────┘
         │                     │                     │
         └─────────────────────┼─────────────────────┘
                               │
                      ┌────────┴────────┐
                      │  统一状态管理    │
                      │  @State 驱动     │
                      │  @Observed 响应  │
                      │  式数据模型      │
                      └─────────────────┘

2.3 页面生命周期与下载代理绑定流程

aboutToAppear()
    │
    ├── setupDownloadDelegate() → 下载代理注册
    │       │
    │       ├── onBeforeDownload → item.start(沙箱路径)
    │       ├── onDownloadUpdated → 刷新进度条
    │       ├── onDownloadFailed → 置失败文案
    │       └── onDownloadFinish → getOriginalUrl + getReferrerUrl
    │                              → 新建 DownloadRecord 置顶
    │
    ├── 种子对焦记录 (3条 FocusRecord 置顶)
    │
    └── 启动呼吸动画定时器 (1s 心跳)
            │
            ├── breath 翻转
            ├── ringReady → drawRing() 重绘
            └── lineReady → drawLine() 重绘

aboutToDisappear()
    │
    ├── clearInterval(timer)
    └── releaseSession() → 释放相机资源

三、逐段代码深度解析

3.1 模块导入层

import { camera } from '@kit.CameraKit';
import { abilityAccessCtrl } from '@kit.AbilityKit';
import { webview } from '@kit.ArkWeb';
import { BusinessError } from '@kit.BasicServicesKit';

四行 import 分别引入了本应用所需的全部系统能力。

第一行从 @kit.CameraKit 导入 camera 命名空间,它是相机能力的统一入口,涵盖 CameraManagerCameraInputPreviewOutputVideoSessionPhotoSessionCameraPositionControlCenterEffectType 等全部相机相关类型和枚举。在本应用中,Camera Kit 承担两项职责:VideoSession 上的 AUTO_FRAMING 影随人动能力链,以及 PhotoSession 上的手动对焦三接口。

第二行从 @kit.AbilityKit 导入 abilityAccessCtrl,用于动态申请 ohos.permission.CAMERA 权限。HarmonyOS 的权限模型将涉及用户隐私的权限划分为 user_grant 级别,要求必须在运行时弹窗申请,不能仅在 module.json5 配置文件中声明即可使用。相机权限正是典型的 user_grant 权限。

第三行从 @kit.ArkWeb 导入 webview 命名空间,它是 ArkWeb 套件的核心模块,提供 WebviewController(Web 视图控制器)和 WebDownloadDelegate(下载代理)两个关键类。WebviewController 负责 URL 加载、页面导航和应用侧主动发起下载等操作;WebDownloadDelegate 负责监听网页内触发的下载事件,通过四个回调覆盖下载全生命周期。

第四行从 @kit.BasicServicesKit 导入 BusinessError 类型,用于统一捕获异步操作中的错误对象,提供 codemessage 两个字段供错误处理和日志记录使用。

3.2 颜色系统:深色主题的设计语言

interface ColorPalette {
  bg: string;
  card: string;
  dark: string;
  title: string;
  sub: string;
  text3: string;
  cyan: string;
  cyanD: string;
  orange: string;
  blue: string;
  green: string;
  red: string;
  line: string;
  tabOn: string;
  mask: string;
  cyanFade: string;
}

ColorPalette 接口定义了 16 个语义化颜色槽位,比前述应用多了一个 cyanFade 字段——这是天空青的渐隐色(rgba(59,168,196,0.07)),专门用于折线图渐变填充的终点,使填充区域从天空青自然过渡到近乎透明,产生"天空消散"的视觉效果。

const COLORS: ColorPalette = {
  bg: '#0F1418',
  card: '#1A2229',
  dark: '#232E38',
  title: '#E8F1F5',
  sub: '#A3B8C4',
  text3: '#6E8290',
  cyan: '#3BA8C4',
  cyanD: '#2A8AA6',
  orange: '#E8874B',
  blue: '#4E9BE3',
  green: '#4EC98A',
  red: '#E05E5E',
  line: '#26333D',
  tabOn: '#3BA8C4',
  mask: 'rgba(0,0,0,0.6)',
  cyanFade: 'rgba(59,168,196,0.07)'
};

色板取名为"云层石墨 + 天空青 + 护航橙",背景色 #0F1418 是极深的石墨黑色调,模拟阴天时云层的厚重感;卡片色 #1A2229 和深层色 #232E38 形成三层深浅梯度。主题色 #3BA8C4(天空青)灵感来自晴朗天空的青蓝色,用于所有主色元素——Tab 激活态、进度环弧线、折线主线、品牌渐变等。辅助暖色 #E8874B(护航橙)用于警示信息和交互引导——电池低电量提醒、编辑操作按钮、对焦手动模式标识等。信息蓝 #4E9BE3 专门用于下载记录中的原始 URL 展示,与引用页 URL 的灰色形成区分。

这种"零硬编码"原则确保全部 Builder 方法中所有色值通过 COLORS.xxx 引用,主题切换只需修改一个常量对象。

3.3 常量数据段

Tab 元信息
interface TabMeta {
  icon: string;
  label: string;
}

const TAB_LIST: TabMeta[] = [
  { icon: '🛩️', label: '任务' },
  { icon: '📷', label: '相机' },
  { icon: '🎯', label: '对焦' },
  { icon: '🌐', label: '网页' },
  { icon: '📥', label: '下载' },
  { icon: '👤', label: '我的' }
];

6 个 Tab 遵循"任务管理→相机操作→起飞巡检→资料浏览→素材下载→个人中心"的产品逻辑。每个 Tab 使用 emoji 图标避免引入外部图片资源,中文标签简洁明了。

Tab 联动副标题
const TAB_SUBS: string[] = [
  '航拍任务进度与飞行架次总览',
  '云台预览与影随人动跟拍',
  '起飞前巡检手动对焦三接口',
  '空域资料与航拍素材站浏览',
  '素材下载双 URL 溯源档案',
  '飞手资质与航拍设备台账'
];

TAB_SUBS 数组与 TAB_LIST 一一对应,在头部 Banner 中根据 currentTab 索引显示当前 Tab 的功能描述,让用户始终知道当前所在页面的定位和用途。

Camera Kit 枚举与预设
interface EffectInfo {
  type: number;
  name: string;
  desc: string;
}

const EFFECT_INFOS: EffectInfo[] = [
  { type: 0, name: 'BEAUTY', desc: '美颜 · since 20' },
  { type: 1, name: 'PORTRAIT', desc: '人像 · since 20' },
  { type: 2, name: 'AUTO_FRAMING', desc: '影随人动 · 6.1.1 新增' }
];

EFFECT_INFOS 对应 ControlCenterEffectType 枚举,三种效果中 AUTO_FRAMING(type=2)是本应用核心使用特性。枚举表中标注了 since 206.1.1 新增,明确区分了既有能力和新增能力,在相机 Tab 的效果类型卡片中用于教育性展示。

interface FocusPreset {
  label: string;
  distance: number;
  scene: string;
}

const FOCUS_PRESETS: FocusPreset[] = [
  { label: '近距', distance: 0.2, scene: '0.2 · 电池序列号核验' },
  { label: '中距', distance: 0.5, scene: '0.5 · 桨叶裂纹检查' },
  { label: '远景', distance: 0.9, scene: '0.9 · 起飞环境观察' }
];

对焦预设分三档,对应无人机起飞前巡检的三种典型场景:近距离对焦检查电池序列号和接口触点、中距离对焦检查桨叶裂纹和电机状态、远距离对焦观察起飞环境和净空障碍物。distance 值域 0.0~1.0,与 Camera Kit 的 setFocusDistance 参数对齐。

ArkWeb 快捷站点
interface QuickSite {
  icon: string;
  name: string;
  url: string;
}

const QUICK_SITES: QuickSite[] = [
  { icon: '🚁', name: '大疆社区', url: 'https://community.dji.com' },
  { icon: '📸', name: '天空之城', url: 'https://www.skypixel.com' },
  { icon: '🛩️', name: 'UOM 平台', url: 'https://uom.caac.gov.cn' },
  { icon: '📋', name: '民航局专栏', url: 'https://www.caac.gov.cn' }
];

快捷站点列表包含四个无人机航拍行业的高频网站:大疆社区(设备讨论和固件更新)、天空之城(航拍作品展示和素材站)、UOM 平台(民航局无人机空域申报系统)和民航局专栏(政策法规)。在网页 Tab 中以横滑卡片形式展示,点击即加载对应 URL。

Canvas 图表数据
const FLIGHT_LINE_DATA: number[] = [24, 18, 32, 27, 35, 21, 38, 29, 33, 25, 36, 31];
const FLIGHT_LABELS: string[] = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'];
const FLIGHT_LINE_MAX: number = 45;

const MONTH_NAME: string[] = ['04', '05', '06', '07', '08', '09'];
const MONTH_FLIGHTS: number[] = [18, 24, 29, 26, 35, 31];
const MONTH_MAX: number = 40;

折线图数据覆盖近 12 次飞行架次的单架时长(分钟),最高 38 分钟(第 7 架次),最低 18 分钟(第 2 架次),波动趋势清晰。柱状图数据覆盖近 6 个月的飞行架次次数,8 月达到峰值 35 次。

3.4 辅助函数群

应用在 struct 外部定义了 9 个辅助函数,覆盖状态配色、字节数换算和语义转换等通用逻辑。

任务状态配色
function missionStateColor(s: string): string {
  if (s === '已完成') { return COLORS.green; }
  if (s === '执行中') { return COLORS.cyan; }
  if (s === '素材整理') { return COLORS.orange; }
  return COLORS.text3;
}

将航拍任务的四种状态映射为语义色:已完成用通过绿(成功/完成)、执行中用天空青(主色/进行中)、素材整理用护航橙(注意/处理中)、待起飞用灰青(弱化/待命)。

字节数换算
function fmtBytes(bytes: number): string {
  if (bytes >= 1073741824) { return (bytes / 1073741824).toFixed(1) + ' GB'; }
  if (bytes >= 1048576) { return (bytes / 1048576).toFixed(1) + ' MB'; }
  if (bytes >= 1024) { return (bytes / 1024).toFixed(1) + ' KB'; }
  return bytes.toString() + ' B';
}

fmtBytesgetTotalBytes() 返回的字节数换算为人类可读格式——B/KB/MB/GB,保留一位小数。这个函数在 onDownloadFinish 回调中使用,将下载完成后的文件大小格式化后存入 DownloadRecord

其余函数包括 readStateColor(对焦校验结果配色)、framingStateColor(影随人动状态配色)、distanceLabel(对焦距离→巡检景别文案)、dlStateColor(下载状态配色)、sessionLabel(会话模式中文标签)、batteryColor(电池电量配色)、gearKindColor(设备类别配色),均遵循"语义→视觉"的统一映射模式。

3.5 数据模型:@Observed 响应式实体

航拍任务条目
@Observed export class MissionItem {
  name: string;
  area: string;
  progress: number;
  state: string;
  constructor(name: string, area: string, progress: number, state: string) {
    this.name = name;
    this.area = area;
    this.progress = progress;
    this.state = state;
  }
}

MissionItem 封装航拍任务的核心信息——任务名、拍摄区域、进度(0~100)和状态(待起飞/执行中/素材整理/已完成)。使用 @Observed 装饰后,修改 item.areaitem.progress 属性即可触发列表对应行的重渲染,无需替换整个数组。种子数据包含 7 条航拍任务,涵盖滨江夜景巡查、婚庆跟拍、光伏电站巡检等真实场景。

下载记录(双 URL 溯源)
@Observed export class DownloadRecord {
  fileName: string;
  fileSize: string;
  finishTime: string;
  originalUrl: string;
  referrerUrl: string;
  constructor(fileName: string, fileSize: string, finishTime: string,
    originalUrl: string, referrerUrl: string) {
    this.fileName = fileName;
    this.fileSize = fileSize;
    this.finishTime = finishTime;
    this.originalUrl = originalUrl;
    this.referrerUrl = referrerUrl;
  }
}

DownloadRecord 是 ArkWeb 双 URL 溯源特性的数据载体,携带五要素:建议文件名(getSuggestedFileName)、文件大小(getTotalBytes 换算)、完成时间、原始 URL(getOriginalUrl,文件直链来源)和引用页 URL(getReferrerUrl,触发下载的页面)。种子数据包含 6 条下载记录,涵盖 4K 航拍素材、空域数据包、固件更新、预设文件、飞行日志和交付包等真实下载场景。

此外还有 FocusRecord(对焦校验记录)、GearItem(航拍设备)和 BatteryItem(智能电池状态)三个 @Observed 数据模型,全部采用全参构造函数确保数据完整性。

3.6 页面结构体与状态管理

@Entry
@Component
struct Page1295 {

struct 内部将成员变量分为七个组:Tab 状态、弹窗状态、动画状态、航拍任务数据、Camera 成员、Canvas 成员、ArkWeb 成员。

Camera Kit 成员
private previewController: XComponentController = new XComponentController();
private cameraInput?: camera.CameraInput;
private previewOutput?: camera.PreviewOutput;
private videoSession?: camera.VideoSession;
private photoSession?: camera.PhotoSession;
@State surfaceReady: boolean = false;
@State sessionMode: string = 'idle';
@State framingState: string = '未查询';
@State framingSupported: boolean = false;
@State focusSupported: boolean = false;
@State focusDistance: number = 0.5;
@State focusRecords: FocusRecord[] = [];
@State permState: string = '未申请';

控制器类成员(previewControllercameraInputpreviewOutputvideoSessionphotoSession)声明为 private,因为它们不直接参与 UI 渲染;而面向 UI 的状态变量全部使用 @StatesessionMode 有三个值:idle(空闲)、video(影随人动会话)、photo(手动对焦会话),控制模式切换按钮的高亮状态和对焦 Tab 的按钮可用性。

Canvas 成员
private ringCtx: CanvasRenderingContext2D = new CanvasRenderingContext2D(new RenderingContextSettings(true));
private lineCtx: CanvasRenderingContext2D = new CanvasRenderingContext2D(new RenderingContextSettings(true));
@State ringReady: boolean = false;
@State lineReady: boolean = false;

两个 Canvas 上下文分别用于进度环和折线图。ringReadylineReady 是 Canvas 就绪标志,仅在 onReady 回调后置为 true,呼吸定时器中根据这两个标志决定是否调用重绘——避免在 Canvas 尚未就绪时调用绘制方法导致异常。

ArkWeb 成员
private webController: webview.WebviewController = new webview.WebviewController();
private downloadDelegate: webview.WebDownloadDelegate = new webview.WebDownloadDelegate();
@State urlInput: string = QUICK_SITES[0].url;
@State webUrl: string = QUICK_SITES[0].url;
@State dlName: string = '';
@State dlPercent: number = 0;
@State dlState: string = '空闲';
@State downloadRecords: DownloadRecord[] = DOWNLOAD_LIST;

ArkWeb 成员是本应用最核心的特性组。webController 是 Web 视图控制器,负责加载 URL 和主动发起下载。downloadDelegate 是下载代理实例,在 aboutToAppear 中注册四个回调后绑定到 webController

urlInputwebUrl 采用双状态分离设计——urlInput 是地址栏的输入值(用户正在编辑),webUrl 是 Web 组件实际加载的值(点击"前往"后同步),这种分离避免了用户输入过程中 Web 组件频繁重新加载。

3.7 生命周期方法

aboutToAppear
aboutToAppear() {
  this.setupDownloadDelegate();
  this.focusRecords.unshift(new FocusRecord(0.9, 0.9, '已生效'));
  this.focusRecords.unshift(new FocusRecord(0.5, 0.52, '读回偏差'));
  this.focusRecords.unshift(new FocusRecord(0.2, 0.2, '已生效'));
  this.timer = setInterval(() => {
    this.breath = !this.breath;
    if (this.ringReady) { this.drawRing(); }
    if (this.lineReady) { this.drawLine(); }
  }, 1000);
}

aboutToAppear 完成三项初始化:

  1. 下载代理注册:调用 setupDownloadDelegate() 注册 WebDownloadDelegate 的四个回调并绑定到 webController,使网页内触发的下载进入回调链路。
  2. 种子对焦记录:预先注入 3 条对焦校验记录(远景 0.9 已生效、中距 0.5 读回偏差、近距 0.2 已生效),展示"已生效"和"读回偏差"两种典型状态。
  3. 呼吸定时器:每 1000ms 翻转 breath 并根据 Canvas 就绪状态重绘进度环和折线图,实现"呼吸"动画效果。
aboutToDisappear 与 switchTab
aboutToDisappear() {
  clearInterval(this.timer);
  this.releaseSession();
}

switchTab(idx: number) {
  if (this.currentTab === 1 && idx !== 1) {
    this.releaseSession();
  }
  this.currentTab = idx;
}

页面销毁时清理定时器并释放相机资源。Tab 切换时,离开相机 Tab(index=1)会主动释放会话——因为相机是独占资源,不释放会阻止其他相机应用使用。

3.8 Camera Kit 方法群

Camera Kit 方法群包括权限申请、影随人动模式启动、AUTO_FRAMING 能力链、手动对焦模式切换、会话释放、能力查询和设置读回校验七个方法。

影随人动模式
async startVideoMode() {
  if (!this.surfaceReady) { this.framingState = 'Surface 未就绪'; return; }
  if (this.sessionMode === 'video') { return; }
  if (this.sessionMode === 'photo') { await this.releaseSession(); }
  const granted = await this.requestCameraPermission();
  if (!granted) { this.framingState = '权限被拒'; return; }
  try {
    const ctx = this.getUIContext().getHostContext();
    const manager = camera.getCameraManager(ctx);
    let device: camera.CameraDevice | undefined = undefined;
    for (const d of manager.getSupportedCameras()) {
      if (d.cameraPosition === camera.CameraPosition.CAMERA_POSITION_BACK) {
        device = d; break;
      }
    }
    const capability = manager.getSupportedOutputCapability(device, camera.SceneMode.NORMAL_VIDEO);
    const profile = capability.previewProfiles.length > 0 ? capability.previewProfiles[0] : undefined;
    this.cameraInput = manager.createCameraInput(device);
    await this.cameraInput.open();
    this.previewOutput = manager.createPreviewOutput(profile,
      this.previewController.getXComponentSurfaceId());
    this.videoSession = manager.createSession<camera.VideoSession>(camera.SceneMode.NORMAL_VIDEO);
    this.videoSession.on('error', (err: BusinessError) => { console.error(`session error: ${err.code}`); });
    this.videoSession.beginConfig();
    this.videoSession.addInput(this.cameraInput);
    this.videoSession.addOutput(this.previewOutput);
    await this.videoSession.commitConfig();
    this.queryFraming(this.videoSession);
    await this.videoSession.start();
    this.sessionMode = 'video';
  } catch (e) {
    const err = e as BusinessError;
    this.framingState = `会话失败(${err.code})`;
    await this.releaseSession();
  }
}

startVideoMode 是影随人动模式的完整启动流程,步骤极为丰富。首先进行前置检查:确认 Surface 已就绪、当前不是 video 模式(避免重复启动)、如果处于 photo 模式则先释放。然后申请 CAMERA 权限。获取权限后进入核心流程:获取相机管理器 → 选择后摄 → 获取预览 Profile → 创建输入流并打开 → 创建预览输出绑定到 Surface → 创建 VideoSession 并配置 → 在 start() 之前调用 queryFraming() 查询并启用影随人动 → 启动会话 → 更新 sessionModevideo

AUTO_FRAMING 能力链
queryFraming(session: camera.VideoSession) {
  if (!session.isControlCenterSupported()) {
    this.framingState = '控制中心不支持';
    this.framingSupported = false;
    return;
  }
  const effects = session.getSupportedEffectTypes();
  this.framingSupported = effects.includes(camera.ControlCenterEffectType.AUTO_FRAMING);
  if (!this.framingSupported) { this.framingState = 'AUTO_FRAMING 未声明'; return; }
  try {
    session.enableControlCenter(true);
    this.framingState = '影随人动已启用';
  } catch (e) {
    this.framingState = `接管失败(${(e as BusinessError).code})`;
  }
}

三步能力链:第一步 isControlCenterSupported() 查询控制中心支持性;第二步 getSupportedEffectTypes() 获取效果枚举列表并检查是否包含 AUTO_FRAMING;第三步 enableControlCenter(true) 请求系统接管画面构图。每一步都有状态文案反馈,UI 层通过 framingStateColor() 映射为颜色(天空青=已启用、护航橙=能力缺失、警示红=失败)。

手动对焦三接口
queryFocusSupport() {
  if (this.photoSession === undefined) { this.focusSupported = false; return; }
  try {
    this.focusSupported = this.photoSession.isFocusDistanceSupported();
  } catch (e) {
    this.focusSupported = false;
  }
}

applyFocus() {
  if (this.photoSession === undefined) {
    this.focusRecords.unshift(new FocusRecord(this.focusDistance, -1, '失败(未启动拍照会话)'));
    return;
  }
  try {
    this.photoSession.setFocusDistance(this.focusDistance);
    const readBack = this.photoSession.getFocusDistance();
    const ok = Math.abs(readBack - this.focusDistance) < 0.01 ? '已生效' : '读回偏差';
    this.focusRecords.unshift(new FocusRecord(this.focusDistance, readBack, ok));
    if (this.focusRecords.length > 20) { this.focusRecords.pop(); }
  } catch (e) {
    const err = e as BusinessError;
    this.focusRecords.unshift(new FocusRecord(this.focusDistance, -1, `失败(${err.code})`));
  }
}

手动对焦三接口构成"查询→设置→读回→校验"闭环:isFocusDistanceSupported() 同步查询能力,setFocusDistance(value) 设置对焦距离,getFocusDistance() 读回实际值,通过 Math.abs(readBack - set) < 0.01 判断是否在容差范围内。每次操作生成 FocusRecord 并置顶到时间线,最多保留 20 条。

3.9 ArkWeb 方法群:下载双 URL 溯源

下载代理注册
setupDownloadDelegate() {
  this.downloadDelegate.onBeforeDownload((item: webview.WebDownloadItem) => {
    const hostCtx = this.getUIContext().getHostContext();
    const dir = hostCtx ? hostCtx.filesDir : '';
    this.dlName = item.getSuggestedFileName();
    this.dlPercent = 0;
    this.dlState = '已开始';
    item.start(dir + '/' + item.getSuggestedFileName());
  });
  this.downloadDelegate.onDownloadUpdated((item: webview.WebDownloadItem) => {
    this.dlPercent = item.getPercentComplete();
    this.dlState = '正在下载 ' + item.getPercentComplete() + '%';
  });
  this.downloadDelegate.onDownloadFailed((item: webview.WebDownloadItem) => {
    this.dlState = '下载失败 · ' + item.getGuid();
    this.dlPercent = 0;
  });
  this.downloadDelegate.onDownloadFinish((item: webview.WebDownloadItem) => {
    const originalUrl: string = item.getOriginalUrl();
    const referrerUrl: string = item.getReferrerUrl();
    this.downloadRecords.unshift(new DownloadRecord(
      item.getSuggestedFileName(),
      fmtBytes(item.getTotalBytes()),
      '刚刚', originalUrl, referrerUrl));
    this.dlState = '下载完成';
    this.dlPercent = 100;
  });
  try {
    this.webController.setDownloadDelegate(this.downloadDelegate);
  } catch (error) {
    console.error(`ErrorCode: ${(error as BusinessError).code}, Message: ${(error as BusinessError).message}`);
  }
}

setupDownloadDelegate 是 ArkWeb 双 URL 溯源特性的核心方法,注册 WebDownloadDelegate 的四个回调:

onBeforeDownload(下载开始前):这是最关键的回调——必须在此调用 item.start(path) 提供沙箱存储路径,否则下载任务永远停在 PENDING 状态。通过 getHostContext().filesDir 获取应用沙箱目录,拼接建议文件名构建完整路径。同时提取建议文件名并更新 UI 状态为"已开始"。

onDownloadUpdated(下载进行中):持续触发,通过 item.getPercentComplete() 获取进度百分比,更新 dlPercentdlState 驱动下载 Tab 的进度条和状态文案实时刷新。

onDownloadFailed(下载失败):通过 item.getGuid() 获取失败标识,置失败文案并清零进度。

onDownloadFinish(下载完成):这是双 URL 溯源的核心回调。调用 item.getOriginalUrl() 获取下载项的原始 URL(文件直链来源地址),调用 item.getReferrerUrl() 获取引用页 URL(触发下载的页面地址),调用 item.getTotalBytes() 获取文件总大小并经 fmtBytes() 换算为人类可读格式。将五要素(文件名+大小+时间+原始URL+引用页URL)封装为 DownloadRecordunshift 到下载记录列表顶部。

最后通过 webController.setDownloadDelegate(this.downloadDelegate) 将下载代理绑定到 Web 控制器——只有绑定后,网页内触发的下载才会进入上述回调链路。绑定操作包裹在 try-catch 中,消除可能的抛错告警。

URL 加载与主动下载
loadUrl() {
  let url = this.urlInput.trim();
  if (url === '') { return; }
  if (!url.startsWith('https://') && !url.startsWith('http://')) {
    url = 'https://' + url;
  }
  this.urlInput = url;
  this.webUrl = url;
}

triggerDownload(url: string) {
  try {
    this.dlName = url.slice(url.lastIndexOf('/') + 1);
    this.dlPercent = 0;
    this.dlState = '已发起下载请求';
    this.webController.startDownload(url);
  } catch (error) {
    this.dlState = '发起失败 ' + (error as BusinessError).code;
  }
}

loadUrl 实现地址栏的"前往"功能:校验协议前缀(无 http(s):// 时自动补 https://),然后同步 urlInputwebUrltriggerDownload 实现应用侧主动发起下载——无需用户在网页内点击下载链接,直接调用 webController.startDownload(url) 即可触发下载流程,下载事件同样进入 Delegate 的四回调链路。这为"下载空域数据包"和"下载 4K 航拍样片"两个快捷按钮提供了实现基础。

3.10 Canvas 绘制方法群

任务完成率进度环
drawRing() {
  const ctx = this.ringCtx;
  ctx.antialias = true;
  const cx = 90;
  const cy = 90;
  const r = 64;
  const progress = this.missionRate() / 100;
  const breathVal = this.breath ? 1.0 : 0.88;

  // 外圈呼吸光环
  ctx.globalAlpha = this.breath ? 0.42 : 0.14;
  ctx.beginPath();
  ctx.arc(cx, cy, r + 12, 0, Math.PI * 2);
  ctx.strokeStyle = COLORS.cyanD;
  ctx.lineWidth = 3;
  ctx.stroke();
  ctx.globalAlpha = 1;

  // 背景环
  ctx.beginPath();
  ctx.arc(cx, cy, r, 0, Math.PI * 2);
  ctx.strokeStyle = COLORS.dark;
  ctx.lineWidth = 13;
  ctx.stroke();

  // 进度弧
  ctx.beginPath();
  ctx.arc(cx, cy, r, -Math.PI / 2, -Math.PI / 2 + Math.PI * 2 * progress * breathVal);
  ctx.strokeStyle = COLORS.cyan;
  ctx.lineWidth = 13;
  ctx.lineCap = 'round';
  ctx.stroke();

  // 中心百分比
  ctx.fillStyle = COLORS.title;
  ctx.font = 'bold 26px sans-serif';
  ctx.textAlign = 'center';
  ctx.fillText(Math.round(progress * 100).toString() + '%', cx, cy + 2);

  // 中心副标签
  ctx.font = '10px sans-serif';
  ctx.fillStyle = COLORS.text3;
  ctx.fillText('今日任务完成率', cx, cy + 24);
}

drawRing 绘制五层环形进度图:

第一层:外圈呼吸光环。在进度环外测画一圈半径 r+12 的细环,透明度随 breath 在 0.42 和 0.14 之间波动,产生"光晕呼吸"效果。绘制完成后 globalAlpha 复位为 1,确保不影响后续图层。

第二层:背景环。用 COLORS.dark 画一个完整的粗环(lineWidth=13),作为进度弧的底色。

第三层:进度弧。从 12 点方向(-Math.PI/2)起笔,弧长为 2π × progress × breathValbreathVal 随呼吸在 1.0 和 0.88 之间波动,使弧长产生 12% 的微变——模拟"心跳"般的进度波动。使用 lineCap = 'round' 圆头线帽,让弧段端点圆润。COLORS.cyan 天空青是主色。

第四层:中心百分比大字。在环心绘制 Math.round(progress * 100) + '%' 的粗体大字,数据源是 missionRate() 方法计算的任务平均完成率。

第五层:中心副标签。在百分比下方绘制"今日任务完成率"小字。

飞行架次时长折线图
drawLine() {
  const ctx = this.lineCtx;
  ctx.antialias = true;
  const w = ctx.width > 0 ? ctx.width : 340;
  const h = 168;
  const pad = 26;
  const max = FLIGHT_LINE_MAX;
  const n = FLIGHT_LINE_DATA.length;
  const stepX = (w - pad * 2) / (n - 1);
  const plotH = h - pad * 2 - 14;
  const baseY = h - pad - 14;

  // 背景网格
  ctx.strokeStyle = COLORS.line;
  ctx.lineWidth = 1;
  for (let i = 0; i <= 3; i++) {
    const y = pad + plotH * i / 3;
    ctx.beginPath();
    ctx.moveTo(pad, y);
    ctx.lineTo(w - pad, y);
    ctx.stroke();
  }

  // 天空青渐变填充
  const grad = ctx.createLinearGradient(0, pad, 0, baseY);
  grad.addColorStop(0, COLORS.cyan);
  grad.addColorStop(1, COLORS.cyanFade);
  ctx.beginPath();
  ctx.moveTo(pad, baseY);
  for (let i = 0; i < n; i++) {
    const x = pad + i * stepX;
    const y = baseY - (FLIGHT_LINE_DATA[i] / max) * plotH;
    ctx.lineTo(x, y);
  }
  ctx.lineTo(w - pad, baseY);
  ctx.closePath();
  ctx.fillStyle = grad;
  ctx.fill();

  // 折线主线
  ctx.beginPath();
  for (let i = 0; i < n; i++) {
    const x = pad + i * stepX;
    const y = baseY - (FLIGHT_LINE_DATA[i] / max) * plotH;
    if (i === 0) { ctx.moveTo(x, y); } else { ctx.lineTo(x, y); }
  }
  ctx.strokeStyle = COLORS.cyan;
  ctx.lineWidth = 2;
  ctx.stroke();

  // 数据点(末点呼吸放大)
  for (let i = 0; i < n; i++) {
    const x = pad + i * stepX;
    const y = baseY - (FLIGHT_LINE_DATA[i] / max) * plotH;
    ctx.beginPath();
    ctx.arc(x, y, i === n - 1 ? (this.breath ? 4.5 : 3.5) : 3, 0, Math.PI * 2);
    ctx.fillStyle = COLORS.card;
    ctx.fill();
    ctx.strokeStyle = COLORS.cyan;
    ctx.lineWidth = 1.5;
    ctx.stroke();
  }

  // 横轴架次标签
  ctx.font = '9px sans-serif';
  ctx.textAlign = 'center';
  ctx.fillStyle = COLORS.text3;
  for (let i = 0; i < n; i += 2) {
    const x = pad + i * stepX;
    ctx.fillText(FLIGHT_LABELS[i], x, h - 4);
  }
}

drawLine 绘制五层折线图:

第一层:背景网格。3 条水平线均匀分布,使用 COLORS.line 深色分割线色。

第二层:天空青渐变填充。使用 createLinearGradient 从顶部天空青到底部 cyanFade(近乎透明),沿折线路径绘制闭合区域并填充渐变,形成面积图效果。

第三层:折线主线。天空青描边,线宽 2px,从第一个点 moveTo 开始 lineTo 连接。

第四层:数据点。每个数据点画白心青边小圆点(半径 3px),末点(最新架次)在 breath 为 true 时放大到 4.5px,产生脉冲效果。

第五层:横轴标签。每隔一个数据点绘制架次序号,避免拥挤。

3.11 Builder 群:页面骨架

头部 Banner

头部使用天空青渐变背景(linearGradientcyanDcyan 再到 cyanD),包含品牌语"云镜" + Tab 联动副标题 + 三枚统计胶囊(任务数/月架次/素材库大小)+ 呼吸圆点状态。下方是搜索条和"新建任务"按钮的横排布局。

任务 Tab

任务 Tab 是首页,包含四块内容:

  1. 航拍任务进度条清单ForEach 遍历 missionList,每个任务项包含名称、状态胶囊、区域、进度条和"改"/"取消"操作按钮。进度条颜色通过 missionStateColor() 按状态着色。
  2. Canvas 完成率环卡:左侧 180×180 的 Canvas 进度环,右侧是统计文字和图例。onReady 回调置 ringReady = true 并首绘。
  3. 近 12 次飞行架次时长折线卡:全宽 Canvas 折线图,onReadylineReady = true 并首绘。
  4. 电池电量状态卡ForEach 遍历 batteryList(四仓电池),每仓显示电量进度条、状态胶囊和百分比。充电中的仓位 emoji 透明度随呼吸变化。
相机 Tab

相机 Tab 独占高度,包含授权状态卡、模式切换行(影随人动/手动对焦)、XComponent 预览本体(layoutWeight(1) 占满中间)、以及滚动信息区(影随人动能力链状态卡 + 效果枚举表)。

对焦 Tab

对焦 Tab 是手动对焦三接口的操作面板:能力查询卡 → 三档巡检预设 → 焦距滑杆卡 → 设置/读回按钮行 → 对焦巡检记录时间线。与相机 Tab 共用同一 XComponent Surface。

网页 Tab

网页 Tab 独占高度,包含地址栏、快捷站点横滑、Web 组件本体(layoutWeight(1))和主动下载演示行。Web 组件加载 webUrl 指定的 URL,网页内点击下载链接自动进入 Delegate 四回调。

下载 Tab

下载 Tab 包含三块内容:

  1. 进行中任务卡:文件名 + Progress 进度条 + 百分比 + 状态文案,实时反映 onDownloadUpdated 的进度刷新。
  2. 已完成双 URL 溯源记录列表ForEach 遍历 downloadRecords,每条记录展示序号、文件名、大小+时间、🔗原始URL(蓝色 monospace)和📄引用页URL(灰色 monospace)两行。
  3. onDownloadFinish 代码预览卡:深色底 monospace 两行核心调用代码(getOriginalUrl()getReferrerUrl()),配以四回调流程说明。
我的 Tab

飞手渐变大卡(天空青渐变背景,含持证等级/飞行小时/累计架次)+ 设备清单行(左色条按类别着色)+ 月度飞行柱状图(传统 Column+ForEach,breath 联动柱高微动)。

底部导航栏

6 Tab 单排自绘,选中态天空青粗体,未选中态暗青灰常规体,顶部有 1px 分割线。

3.12 弹窗系统

三个全屏弹窗通过 Stack 覆盖层叠加在主界面上方:

  • panelAdd:新建航拍任务弹窗,包含任务名输入框、拍摄区域输入框和初始进度滑杆,确认时创建 MissionItemunshift 置顶。
  • panelEdit:修改任务区域弹窗,包含区域输入框和进度滑杆,确认时直接修改 MissionItemareaprogress 属性——@Observed 使属性级变更自动触发列表行重渲染。
  • panelDel:取消任务确认弹窗,红色警示按钮,确认时 splice 移除对应条目。

四、三特性技术对比表

维度Camera Kit 特性一Camera Kit 特性二ArkWeb 特性Canvas 特性一Canvas 特性二
能力名称AUTO_FRAMING 影随人动手动对焦三接口下载双 URL 溯源drawRing 进度环drawLine 折线图
宿主模块VideoSession ControlCenterPhotoSession ManualFocusWebDownloadDelegateCanvasRenderingContext2DCanvasRenderingContext2D
核心APIisControlCenterSupported / getSupportedEffectTypes / enableControlCenterisFocusDistanceSupported / setFocusDistance / getFocusDistanceonBeforeDownload / onDownloadUpdated / onDownloadFailed / onDownloadFinish + getOriginalUrl / getReferrerUrlarc / stroke / fillTextcreateLinearGradient / moveTo / lineTo
调用模式同步查询 + 同步启用同步查询 + 同步设置 + 同步读回异步回调(事件驱动)同步绘制同步绘制
返回数据布尔值 + 枚举数组布尔值 + 数字WebDownloadItem 对象(含 URL/大小/文件名/进度)无(直接渲染)无(直接渲染)
UI呈现能力链状态卡 + 枚举表预设档位 + 滑杆 + 校验时间线进度卡 + 双URL溯源列表 + 代码预览卡进度环 + 统计文字 + 图例网格 + 渐变面积 + 折线 + 数据点
容错策略三步降级try-catch + 读回差值校验四回调全覆盖 + start() 强制落盘 + try-catchonReady首绘 + 就绪标志onReady首绘 + 就绪标志
状态联动framingState / framingSupportedfocusSupported / focusDistance / focusRecordsdlName / dlPercent / dlState / downloadRecordsbreath(弧长波动)breath(末点放大)
航拍场景飞手地面移动时云台自动跟拍起飞前巡检电池/桨叶/环境素材下载来源追溯与版权审计任务完成率可视化飞行架次时长趋势追踪
生命周期管理releaseSession() 统一释放releaseSession() 统一释放aboutToAppear 注册绑定clearInterval 停止重绘clearInterval 停止重绘
API版本6.1.1 新增6.1.1 新增API 24 (6.1.1)ArkUI 基础ArkUI 基础
资源独占性cameraInput 独占与 VideoSession 互斥Web 组件内下载触发

五、深度总结

5.1 架构设计亮点

本应用在单页面文件中实现了 Camera Kit、ArkWeb、Canvas 三大套件的深度叠加,每个套件都构建了完整的能力链路:

Camera Kit 侧,VideoSession 和 PhotoSession 两种会话模式互斥切换。AUTO_FRAMING 影随人动通过"能力查询→效果枚举→启用接管"三步链路实现,每一步都有状态文案反馈和颜色映射。手动对焦三接口通过"设置→读回→差值校验"的闭环验证对焦操作的真实生效,时间线记录让操作历史可追溯。在航拍场景中,影随人动用于飞手地面移动时的云台自动跟拍,手动对焦用于起飞前的电池序列号、桨叶裂纹和起飞环境巡检——每个特性都映射到明确的业务需求。

ArkWeb 侧WebDownloadDelegate 的四个回调覆盖了下载的完整生命周期,从 onBeforeDownload 的沙箱路径落盘,到 onDownloadUpdated 的实时进度刷新,到 onDownloadFailed 的失败处理,再到 onDownloadFinish 的双 URL 溯源——形成了完整的下载事件处理闭环。getOriginalUrl()getReferrerUrl() 的双 URL 溯源能力使每个下载素材都携带"文件来自哪里"和"从哪个页面下载的"两条溯源线索,对于素材版权追溯和下载路径审计具有实际的产品价值。同时,应用侧主动下载(startDownload)和网页内被动下载(Delegate 回调)走同一套处理链路,保证了行为一致性。

Canvas 侧drawRing 进度环通过五层绘制(呼吸光环→背景环→进度弧→中心百分比→副标签)实现丰富的视觉层次,breath 联动弧长波动产生"心跳"效果。drawLine 折线图通过五层绘制(网格→渐变面积→折线→数据点→标签)实现标准的数据可视化呈现,末点呼吸放大突出最新数据。Canvas 就绪标志(ringReady / lineReady)确保重绘只在 Canvas 初始化完成后执行,避免空指针异常。

5.2 ArkWeb 双 URL 溯源的工程价值

getOriginalUrl()getReferrerUrl() 是 ArkWeb 在 API 24 中新增的关键能力。在本应用的下载 Tab 中,每条下载记录都展示了两行 URL:原始 URL 用信息蓝色 monospace 字体显示文件直链来源,引用页 URL 用灰色 monospace 字体显示触发下载的页面。这种双 URL 展示让飞手可以清楚地追溯每个素材的来源——是从天空之城的作品页下载的航拍素材?还是从 UOM 平台下载的空域数据?还是从大疆社区下载的固件更新?这种溯源能力在素材管理和版权审计场景中具有不可替代的价值。

5.3 状态管理的工程实践

  • urlInput / webUrl 双状态分离:用户编辑地址栏时只更新 urlInput,点击"前往"后才同步到 webUrl,避免输入过程中 Web 组件频繁重新加载。
  • Canvas 就绪标志ringReadylineReadyonReady 回调后置为 true,呼吸定时器中根据标志决定是否重绘,避免 Canvas 未就绪时调用绘制方法。
  • @Observed 精细追踪MissionItemareaprogress 属性修改直接触发列表行重渲染,无需替换整个数组。
  • 先清空引用再异步释放releaseSession() 先将控制器变量置为 undefined,再按顺序异步释放,防止 await 期间二次调用。

5.4 健壮性设计

  • 权限链路:CAMERA 权限动态申请,申请结果布尔化处理。
  • 相机链路:会话创建全链 try-catch,失败时调用 releaseSession 清理,能力查询异常时降级为"不支持"。
  • 下载链路onBeforeDownload 中必须调用 start() 提供沙箱路径,否则下载永远停在 PENDING——这是 ArkWeb 下载代理的关键约束。setDownloadDelegate 绑定包裹 try-catch,startDownload 主动下载也包裹 try-catch。
  • URL 协议校验loadUrl() 自动补 https:// 前缀,避免无协议 URL 导致 Web 组件加载失败。

5.5 产品与技术的深度融合

本应用将每个 HarmonyOS 特性精确映射到无人机航拍场景的真实需求:AUTO_FRAMING 影随人动用于飞手地面移动时的云台跟拍,手动对焦三档预设用于起飞前巡检,WebDownloadDelegate 四回调用于素材下载管理,双 URL 溯源用于素材来源追溯,Canvas 进度环用于任务完成率可视化,折线图用于飞行架次时长追踪。这种"技术能力→业务场景"的精确映射,使应用既有技术深度又有产品温度,是 HarmonyOS 全场景开发理念在无人机航拍垂直领域的优秀实践。

附录:DevEco Studio 创建新项目与查看 SDK 版本

本章节演示如何使用 DevEco Studio 创建一个 HarmonyOS 新项目,并查看当前 IDE 已安装的 SDK 版本,适合作为其他技术博文的补充操作指南。


一、创建新项目

1.1 进入欢迎界面

启动 DevEco Studio 后,首先看到的是欢迎界面。左侧导航栏默认选中 “项目”,右侧提供三个主要入口:

  • 新建项目:从头创建新项目
  • 打开项目:打开本地已有项目
  • 克隆仓库:从 Git 等版本控制拉取代码

点击 “新建项目” 按钮,进入项目创建向导。

在这里插入图片描述

1.2 选择项目模板

在弹出的"新建项目"对话框中,左侧分类标签提供了两种项目类型:

类型说明
应用(Application)开发标准的 HarmonyOS 应用,具备完整的 Ability 生命周期
元服务(Atomic Service)开发轻量级的原子化服务,无需安装即可使用

选择 “应用” 标签后,右侧展示多种模板。对于大多数场景,推荐选择 “Empty Ability” —— 这是一个最基础的入门模板,仅包含 Hello World 功能,适合从零开始构建应用。

在这里插入图片描述

1.3 配置项目信息

点击 “下一步” 后,进入项目配置界面,需要填写以下核心参数:

配置项示例值说明
项目名称(Project name)rollboat应用的项目名称,建议使用英文命名
包名(Bundle name)com.rollboat.myapplication应用唯一标识,采用反向域名格式
保存路径(Save location)D:\CodeFactory\rollboat项目本地存储路径,避免使用中文和空格
兼容 SDK(Compatible SDK)6.1.1(24)目标 HarmonyOS API 版本,点击"查看参考"可了解各版本差异
模块名称(Module name)entry主模块名称,默认 entry 为应用入口模块
设备类型(Device types)☑ Phone勾选目标设备:Phone / Tablet / 2in1 / Car / Wearable / TV

右侧预览区会实时展示当前模板的默认效果 —— 一个居中显示的 “Hello World” 文本。

在这里插入图片描述

1.4 完成创建

确认配置无误后,点击右下角 “完成” 按钮,IDE 将自动执行以下操作:

  1. 生成项目骨架(Stage 模型目录结构)
  2. 执行 ohpm install 安装依赖
  3. 运行 Hvigor 构建初始化(Build Init

构建日志中显示 “退出代码为 0” 表示项目初始化成功。

在这里插入图片描述

1.5 项目结构概览

创建完成后,左侧项目面板展示的是标准的 Stage 模型 目录结构:

rollboat/
├── .hvigor/                   # Hvigor 构建工具缓存
├── .idea/                     # IDE 配置文件
├── AppScope/                  # 应用级全局配置
│   └── app.json5
├── entry/                     # 主模块(入口模块)
│   ├── src/main/ets/
│   │   ├── entryability/      # Ability 生命周期管理
│   │   │   └── EntryAbility.ets
│   │   └── pages/             # UI 页面
│   │       └── Index.ets      # 首页(默认 Hello World)
│   ├── src/main/resources/    # 资源文件
│   ├── module.json5           # 模块配置
│   └── build-profile.json5    # 构建配置
├── oh_modules/                # OHPM 依赖包
├── build-profile.json5        # 工程构建配置
├── hvigorfile.ts              # Hvigor 构建脚本
└── oh-package.json5           # 包管理配置

核心文件 Index.ets 的默认代码如下,采用 ArkTS 声明式 UI 语法:

@Entry
@Component
struct Index {
  @State message: string = 'Hello World';

  build() {
    RelativeContainer() {
      Text(this.message)
        .id('HelloWorld')
        .fontSize($r('app.float.page_text_font_size'))
        .fontWeight(FontWeight.Bold)
        .alignRules({
          center: { anchor: '__container__', align: VerticalAlign.Center },
          middle: { anchor: '__container__', align: HorizontalAlign.Center }
        })
        .onClick(() => {
          this.message = 'Welcome';
        })
    }
    .height('100%')
    .width('100%')
  }
}
关键语法作用
@Entry标记为页面入口,可用于路由跳转
@Component声明为自定义组件
@State状态变量,数据变更时自动触发 UI 刷新
RelativeContainer相对布局容器,替代传统线性布局
.onClick()点击事件,此处点击后文本变为 “Welcome”

打开右侧 Previewer(预览器),选择 Phone 设备,即可实时预览 Hello World 效果,无需连接真机或启动模拟器。

在这里插入图片描述


二、查看 SDK 版本

2.1 查看 HarmonyOS SDK

DevEco Studio 安装时已内置 HarmonyOS SDK,无需单独下载。通过以下路径查看:

文件 → 设置 → HarmonyOS SDK(或快捷键 Ctrl + Alt + S 搜索 “HarmonyOS SDK”)

在设置面板中,可以看到当前已安装的 SDK 版本信息:

名称阶段状态
HarmonyOS 6.1.1Release✅ 已安装

界面顶部提示:“HarmonyOS SDK 已经包含在 IDE,无需单独安装”,省去了手动配置 SDK 的繁琐步骤。

在这里插入图片描述

2.2 查看 ArkUI-X SDK(跨平台扩展)

如果项目需要将 ArkUI 框架扩展到多个 OS 平台(Android / iOS / OpenHarmony),还需要配置 ArkUI-X SDK。路径如下:

文件 → 设置 → 语言和框架 → ArkUI-X

在这里可以查看已安装和可选的 ArkUI-X SDK 版本:

版本SDK 版本号阶段状态
API Version 246.1.1.100Release✅ 已安装
API Version 236.1.0.28Beta1未安装
API Version 226.0.2.112Release未安装

安装路径示例:D:\DevTools\ArkUI-X\sdk

说明:ArkUI-X 允许开发者使用一套 ArkTS 主代码,同时构建多平台应用。如果仅开发 HarmonyOS 原生应用,无需额外安装 ArkUI-X SDK。

在这里插入图片描述


三、小结

步骤操作关键点
创建项目欢迎页 → 新建项目 → 选择 Empty Ability 模板 → 配置项目信息 → 完成使用 Stage 模型 + ArkTS 语言
查看 SDK设置 → HarmonyOS SDKSDK 已内置,无需手动安装
跨平台扩展设置 → ArkUI-X根据需要安装对应 API 版本

至此,DevEco Studio 的项目创建与 SDK 环境确认全部完成,可以开始 HarmonyOS 应用的功能开发。


本文基于 DevEco Studio 6.1.1 Release 版本编写,不同版本界面可能存在细微差异。

Logo

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

更多推荐