《2026鸿蒙NEXT纯血开发与AI辅助》第六章:「微距」项目启动——工程创建与整体架构设计-卓伊凡
从这一章开始,我们的专栏正式进入实战阶段。
前五章我们做了很多基础工作:下载 DevEco Studio、理解项目模板、熟悉 IDE 界面、跑通第一个 Hello World。这些不是浪费时间,而是在打地基。地基打好了,接下来盖楼的每一层才会稳。
这一章,我们要做的事情很明确:把「微距」这个项目真正创建出来,把整体架构设计清楚,把后续开发的路线图确定下来。
一、「微距」到底是个什么样的产品
在动手写代码之前,我们先把产品定义清楚。
很多开发者一上来就急着写代码,结果写到一半发现方向偏了,再回头调整成本巨大。所以这一节的每一个字,都请你认真看。
1.1 产品一句话定义
「微距」是一款基于 HarmonyOS 6.0 AR Engine 和 3D 空间化能力的智能测量工具,让用户的手机变成随身携带的多功能测量仪。
1.2 核心使用场景
我帮你梳理了四个最典型的使用场景,这些场景不是凭空想出来的,而是真实生活中经常遇到的:
场景一:家居量尺寸
你准备买一张新沙发,但不确定客厅能不能放得下。传统做法是翻箱倒柜找卷尺,然后一个人拉着尺子,另一个人帮忙看数据,折腾半天。用「微距」,打开应用,对准墙角,用手指在屏幕上点两个点,长度直接显示在 AR 画面上。如果是一个人操作,就更有用了,因为你不需要找人帮忙拉尺子。
场景二:搬家估体积
要搬家了,搬家公司问你大概有多少立方米的东西,你一懵。用「微距」,扫描几个大件家具,应用自动计算体积,还能把多个物体的体积累加起来,给你一个合理估算。这个场景是真实刚需,被搬家公司的报价单逼出来过的人都懂。
场景三:快递测量
你在闲鱼上卖了个东西,需要填快递运费。运费是按体积重和实重取大值算的,你得知道包裹的长宽高。用「微距」扫一下,长宽高和体积一目了然。
场景四:装修验房
装修完验房,墙面是不是平的?用「微距」的水平检测功能扫一下,哪里不平一目了然。这是专业功能,但用手机就能做,门槛降低了一百倍。
1.3 目标用户画像
维度 |
描述 |
年龄段 |
20-45 岁 |
城市分布 |
一二线城市为主,有租房、装修、网购需求的用户 |
行为特征 |
经常网购家具、喜欢 DIY 家居改造、关注智能工具 |
痛点 |
需要测量但手边没卷尺、一个人无法完成测量、需要估算体积 |
1.4 与竞品的差异化
市面上已经有 AR 测量类的应用,比如 iOS 的 Measure 应用,也有第三方测量 App。但它们有几个共同的问题:
- 单设备操作:只能用一台手机,不能两台设备协同测量大空间
- 无材质识别:只能测尺寸,不能告诉你被测物体的材质
- 数据不上云也不做本地加密:要么数据全上云有隐私风险,要么只存本地换手机就丢
而「微距」基于鸿蒙的能力,有三个核心差异化优势:
- 多设备协同测量:两个人各拿一台鸿蒙设备站在不同位置,数据自动拼接,测量更大更准
- 端侧 AI 材质识别:摄像头扫一下,自动识别木材、金属、玻璃等材质,离线可用,隐私安全
- 数据安全存储:测量数据端侧加密存储,不上云,通过鸿蒙分布式能力在多设备间安全同步
这三个点,也是后续参加创新赛时的核心得分点。
二、创建「微距」工程
产品定义清楚了,接下来就是正式创建工程。
2.1 选择哪个模板
我们在第五章详细对比过三种原生方案。对于「微距」这个项目,我们选择 Empty Ability。
为什么不选 Native C++?因为 AR Engine 的 API 在 ArkTS 层已经有完整封装,我们不需要直接操作 C++ 层的 AR 接口。Empty Ability 够用,而且依赖关系最简洁,构建速度最快。
为什么不选 CloudDev Empty Ability?因为「微距」的核心功能都在端侧完成,不依赖云服务。数据存储也是本地为主,不需要云数据库。
所以,Empty Ability,干净利落。
2.2 工程配置
打开 DevEco Studio,点击 Create Project,选择 Empty Ability 模板,然后填写以下配置:
配置项 |
填写内容 |
说明 |
Project name |
MicroDistance |
项目名称,也就是「微距」的英文名 |
Bundle name |
com.zhuoyifan.microdistance |
应用包名,遵循反向域名规范 |
Save location |
D:\Projects\MicroDistance |
项目保存路径 |
Module name |
entry |
入口模块名,保持默认 |
Device type |
Phone、Tablet |
勾选手机和平板,因为「微距」需要在大屏上有更好的体验 |
Enable Native C++ |
不勾选 |
不涉及 C++ 开发 |
点击 Finish,等待工程初始化完成。
2.3 初始化验证
工程创建完成后,注意观察底部 Build 输出区。你应该看到的是 BUILD SUCCESSFUL,而不是第四章那种红色报错。
这里我强调一个习惯:每次创建新工程,第一件事永远是确认构建成功。 这是你的基线。如果构建失败,后面写的所有代码都是无效的。先排错,再开发。
三、项目的整体架构设计
工程跑通了,接下来是架构设计。
很多教程在这一步会直接开始写页面代码,但我们不这样做。因为「微距」不是一个演示 Demo,它是一个有完整功能模块的应用。如果不先把架构想清楚,写到后面一定会乱。
3.1 功能模块划分
我们把「微距」拆成五个核心功能模块:
┌─────────────────────────────────────────────┐ │ 「微距」应用 │ ├───────────┬───────────┬───────────┬──────────┤ │ AR测量 │ 3D空间 │ 协同测量 │ 工具集 │ │ ┌─────┐ │ ┌─────┐ │ ┌─────┐ │ ┌─────┐ │ │ │ 长度 │ │ │空间建│ │ │设备发│ │ │水平仪│ │ │ │ 面积 │ │ │模 │ │ │现连接│ │ │高度计│ │ │ │ 体积 │ │ │3D标注│ │ │数据拼│ │ │材质识│ │ │ │ 角度 │ │ │ │ │ │接 │ │ │别 │ │ │ └─────┘ │ └─────┘ │ └─────┘ │ └─────┘ │ ├───────────┴───────────┴───────────┴──────────┤ │ 数据管理层 │ │ 测量记录存储 / 历史管理 / 导出 │ └─────────────────────────────────────────────┘
这五个模块的职责分别是:
AR 测量:核心功能模块。包含长度测量、面积测量、体积测量、角度测量。用户通过摄像头对准真实世界,在屏幕上选点,实时计算并显示测量结果。
3D 空间:扫描真实空间,生成 3D 点云模型,可以在模型上标注尺寸。这个模块面向的是更专业的使用场景,比如装修前的空间规划。
协同测量:两台鸿蒙设备通过分布式能力连接,各自从不同角度测量,数据自动拼接。这个模块是「微距」区别于市面上所有其他测量 App 的核心竞争力。
工具集:包含水平仪、高度计、材质识别等辅助功能。这些是独立的小工具,和核心测量功能互补。
数据管理:所有测量记录的存储、历史查看、数据导出。测量完成不是结束,用户需要回看和分享。
3.2 页面导航结构
根据功能模块,我们设计应用的页面导航结构:
应用入口(Index.ets - 启动页) │ ▼ 主页面(MainPage.ets - TabBar 导航) │ ├── Tab 1:测量页(MeasurePage.ets) │ ├── 长度测量模式 │ ├── 面积测量模式 │ ├── 体积测量模式 │ └── 角度测量模式 │ ├── Tab 2:空间页(SpacePage.ets) │ ├── 3D 扫描入口 │ ├── 历史模型列表 │ └── 模型详情/标注 │ ├── Tab 3:工具页(ToolsPage.ets) │ ├── 水平仪 │ ├── 高度计 │ └── 材质识别 │ └── Tab 4:记录页(HistoryPage.ets) ├── 测量记录列表 ├── 记录详情 └── 数据导出
这是一个典型的底部四 Tab 导航结构。用户进入主页面后,通过底部的 Tab 栏切换四个核心功能区。
3.3 数据模型设计
在写任何 UI 代码之前,我们先把数据模型定义清楚。这是很多新手容易跳过的步骤,但没有数据模型,后面状态管理和数据持久化会很混乱。
测量记录模型(MeasRecord)
// 测量记录 class MeasRecord { id: string; // 唯一标识 type: MeasType; // 测量类型:长度/面积/体积/角度 value: number; // 测量结果数值 unit: string; // 单位:米/平方米/立方米/度 points: Point3D[]; // 测量选点坐标列表 material: string; // 被测物体材质(AI识别结果) createTime: number; // 创建时间戳 location: Location; // 测量时的地理位置 imageUrl: string; // 测量截图 tags: string[]; // 用户自定义标签 } // 测量类型枚举 enum MeasType { LENGTH = 'length', AREA = 'area', VOLUME = 'volume', ANGLE = 'angle' } // 3D 空间点 class Point3D { x: number; y: number; z: number; }
3D 空间模型(SpaceModel)
// 3D 空间扫描结果 class SpaceModel { id: string; name: string; // 空间名称 pointCloud: Point3D[]; // 点云数据 annotations: Annotation[]; // 空间标注列表 createTime: number; deviceModel: string; // 扫描设备型号 } // 空间标注 class Annotation { id: string; type: AnnotationType; // 标注类型 position: Point3D; // 标注位置 content: string; // 标注内容 measurement: MeasRecord; // 关联的测量数据 } enum AnnotationType { TEXT = 'text', // 文字标注 DIMENSION = 'dimension', // 尺寸标注 MATERIAL = 'material' // 材质标注 }
协同测量会话(CollabSession)
// 多设备协同测量会话 class CollabSession { sessionId: string; deviceList: DeviceInfo[]; // 参与设备列表 measurements: MeasRecord[]; // 会话中的测量数据 mergedData: MeasRecord[]; // 拼接后的数据 status: SessionStatus; } class DeviceInfo { deviceId: string; deviceName: string; deviceType: string; position: Point3D; // 设备相对位置 } enum SessionStatus { CONNECTING = 'connecting', MEASURING = 'measuring', MERGING = 'merging', COMPLETED = 'completed' }
3.4 技术选型与依赖
「微距」项目涉及的技术能力,以及对应使用的鸿蒙 SDK 和 API:
功能需求 |
使用的鸿蒙能力 |
API / SDK |
AR 测量核心 |
AR Engine |
|
3D 场景展示 |
ArkUI 3D 组件 |
|
摄像头调用 |
Camera Kit |
|
多设备协同 |
分布式软总线 |
|
材质识别 |
端侧 AI 视觉 |
|
传感器(水平仪) |
Sensor Kit |
|
地理位置 |
Location Kit |
|
数据持久化 |
关系型数据库 |
|
文件存储 |
文件管理 |
|
这个表很重要。它明确了我们后面每个功能模块要用到什么能力,开发的时候按图索骥就行。
四、本章的代码实现:搭建页面导航骨架
架构设计完成了,接下来我们做第一个实际开发任务:把应用的页面导航骨架搭起来。
这一节的目标是:应用启动后,出现一个底部四个 Tab 的主页面,每个 Tab 对应一个功能模块的占位页面。
4.1 创建页面文件
首先,在 entry/src/main/ets/pages/ 目录下创建以下文件:
pages/ ├── Index.ets # 应用入口(自动生成,暂时保留不改) ├── MainPage.ets # 主页面(TabBar 容器) ├── MeasurePage.ets # 测量页 ├── SpacePage.ets # 空间页 ├── ToolsPage.ets # 工具页 └── HistoryPage.ets # 记录页
4.2 实现 MainPage(TabBar 主页面)
MainPage.ets 是整个应用的核心容器,承载底部导航和四个页面的切换。
import MeasurePage from './MeasurePage'; import SpacePage from './SpacePage'; import ToolsPage from './ToolsPage'; import HistoryPage from './HistoryPage'; @Entry @Component struct MainPage { @State currentIndex: number = 0; // Tab 数据配置 private tabItems: TabItem[] = [ { title: '测量', icon: $r('app.media.ic_measure'), selectedIcon: $r('app.media.ic_measure_selected') }, { title: '空间', icon: $r('app.media.ic_space'), selectedIcon: $r('app.media.ic_space_selected') }, { title: '工具', icon: $r('app.media.ic_tools'), selectedIcon: $r('app.media.ic_tools_selected') }, { title: '记录', icon: $r('app.media.ic_history'), selectedIcon: $r('app.media.ic_history_selected') } ]; // 构建 TabBar 内容 @Builder TabContentBuilder(index: number) { if (index === 0) { MeasurePage() } else if (index === 1) { SpacePage() } else if (index === 2) { ToolsPage() } else { HistoryPage() } } build() { Tabs({ barPosition: BarPosition.End, index: this.currentIndex }) { ForEach(this.tabItems, (item: TabItem, tabIndex: number) => { TabContent() { this.TabContentBuilder(tabIndex) } .tabBar(this.TabBarBuilder(tabIndex)) }) } .onChange((index: number) => { this.currentIndex = index; }) .barMode(BarMode.Fixed) .backgroundColor('#F5F5F5') } // Tab 栏样式构建器 @Builder TabBarBuilder(index: number) { Column() { Image(this.currentIndex === index ? this.tabItems[index].selectedIcon : this.tabItems[index].icon) .width(24) .height(24) .objectFit(ImageFit.Contain) Text(this.tabItems[index].title) .fontSize(10) .fontColor(this.currentIndex === index ? '#007AFF' : '#999999') .margin({ top: 2 }) } .padding({ top: 8, bottom: 8 }) .width('100%') .alignItems(HorizontalAlign.Center) .onClick(() => { this.currentIndex = index; }) } } // Tab 项数据接口 interface TabItem { title: string; icon: Resource; selectedIcon: Resource; }
4.3 实现四个占位页面
四个功能页面目前先用最简单的占位代码,后续章节再逐一实现。
MeasurePage.ets(测量页)
@Component export default struct MeasurePage { build() { Column() { // 顶部标题栏 Row() { Text('AR 测量') .fontSize(20) .fontWeight(FontWeight.Bold) } .width('100%') .padding({ left: 16, top: 48, right: 16, bottom: 16 }) .backgroundColor('#FFFFFF') // 测量模式选择区域(占位) Column() { Text('测量模块即将上线') .fontSize(16) .fontColor('#999999') } .width('100%') .layoutWeight(1) .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) } .width('100%') .height('100%') } }
SpacePage.ets(空间页)
@Component export default struct SpacePage { build() { Column() { Row() { Text('3D 空间') .fontSize(20) .fontWeight(FontWeight.Bold) } .width('100%') .padding({ left: 16, top: 48, right: 16, bottom: 16 }) .backgroundColor('#FFFFFF') Column() { Text('3D 空间模块即将上线') .fontSize(16) .fontColor('#999999') } .width('100%') .layoutWeight(1) .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) } .width('100%') .height('100%') } }
ToolsPage.ets(工具页)
@Component export default struct ToolsPage { build() { Column() { Row() { Text('工具集') .fontSize(20) .fontWeight(FontWeight.Bold) } .width('100%') .padding({ left: 16, top: 48, right: 16, bottom: 16 }) .backgroundColor('#FFFFFF') Column() { Text('工具模块即将上线') .fontSize(16) .fontColor('#999999') } .width('100%') .layoutWeight(1) .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) } .width('100%') .height('100%') } }
HistoryPage.ets(记录页)
@Component export default struct HistoryPage { build() { Column() { Row() { Text('测量记录') .fontSize(20) .fontWeight(FontWeight.Bold) } .width('100%') .padding({ left: 16, top: 48, right: 16, bottom: 16 }) .backgroundColor('#FFFFFF') Column() { Text('记录模块即将上线') .fontSize(16) .fontColor('#999999') } .width('100%') .layoutWeight(1) .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) } .width('100%') .height('100%') } }
4.4 修改应用入口
找到 entry/src/main/ets/entryability/EntryAbility.ets,确认 onWindowStageCreate 中加载的是 MainPage:
import { UIAbility } from '@kit.AbilityKit'; import { window } from '@kit.ArkUI'; import { hilog } from '@kit.PerformanceAnalysisKit'; export default class EntryAbility extends UIAbility { onWindowStageCreate(windowStage: window.WindowStage): void { // 应用启动时加载主页面 windowStage.loadContent('pages/MainPage', (err, data) => { if (err.code) { hilog.error(0x0000, 'microdistance', 'Failed to load content. Cause: %{public}s', JSON.stringify(err) ?? ''); return; } hilog.info(0x0000, 'microdistance', 'Succeeded in loading content. Data: %{public}s', JSON.stringify(data) ?? ''); }); } }
4.5 添加 Tab 图标资源
在 entry/src/main/resources/base/media/ 目录下,需要准备以下图标文件:
media/ ├── ic_measure.png # 测量 Tab 默认图标 ├── ic_measure_selected.png # 测量 Tab 选中图标 ├── ic_space.png # 空间 Tab 默认图标 ├── ic_space_selected.png # 空间 Tab 选中图标 ├── ic_tools.png # 工具 Tab 默认图标 ├── ic_tools_selected.png # 工具 Tab 选中图标 ├── ic_history.png # 记录 Tab 默认图标 ├── ic_history_selected.png # 记录 Tab 选中图标
在正式的开发中,你需要自己制作或从图标库中导出这些图标。作为教学演示,你可以先用简单的纯色方块替代,确保功能跑通后再替换为正式图标。
五、运行验证
代码写完了,我们验证一下。
启动本地真机模拟器,点击 Run,你应该看到:
- 应用启动后直接进入主页面
- 底部有四个 Tab:测量、空间、工具、记录
- 点击不同的 Tab,页面会切换,每个页面显示各自的标题和"即将上线"占位文字
- 选中的 Tab 图标和文字颜色会高亮
如果这四步都能正常运作,说明你的页面导航骨架已经搭建成功。