一、技术背景与行业价值

1.1 移动影像技术的演进脉络

在移动互联网深度渗透的今天,短视频已经成为内容创作领域最具活力的赛道之一。从早期的随手拍摄到如今的专业级移动影像创作,用户对拍摄质量的需求正在经历从"能拍"到"拍好"的质变。在这一演进过程中,相机能力始终是决定用户体验的核心要素。

HarmonyOS 作为面向全场景的分布式操作系统,其 Camera Kit 能力的每一次升级都备受开发者关注。HarmonyOS 6.1.1 版本中,Camera Kit 带来了两大突破性新能力:影随人动自动构图(AUTO_FRAMING)对焦距离检测与设置(getFocusDistance / setFocusDistance)。这两项能力的组合,标志着移动影像从"被动响应"向"主动智能"迈出了关键一步。

1.2 影随人动自动构图的技术意义

影随人动自动构图(AUTO_FRAMING)是一项基于计算机视觉的智能拍摄技术。传统的视频拍摄中,用户需要手动调整机位或使用稳定器来保持主体在画面中的合理位置。而自动构图技术通过实时检测画面中的人物主体,自动调整取景范围,确保人物始终处于画面的最佳构图位置。

从技术实现角度来看,AUTO_FRAMING 能力底层依赖于:

  • 实时人体检测与跟踪算法:在视频流中持续识别人物位置和姿态
  • 智能构图决策引擎:基于美学规则(如三分法、中心构图)动态计算最佳取景框
  • 平滑过渡插值:避免画面跳跃,保证视觉流畅性

这一能力的开放,意味着第三方应用开发者可以直接利用系统级的视觉算法能力,而无需自行训练和部署复杂的 AI 模型,大幅降低了智能拍摄应用的开发门槛。

1.3 对焦距离检测与设置的行业价值

对焦是影像创作中最基础也最核心的技术环节。传统移动设备的对焦系统大多采用反差对焦或相位对焦,对焦过程由系统自动完成,开发者只能间接触发对焦动作,无法精确控制对焦距离。

HarmonyOS 6.1.1 新增的 getFocusDistance()setFocusDistance() 接口,首次将对焦距离的精确控制权开放给应用层。这一变化具有深远的行业意义:

  • 专业拍摄场景:视频博主、影像创作者可以精确控制焦平面位置,实现创意性的虚化效果
  • 场景化预设:针对人像、风景、微距等不同场景预设对焦距离,一键切换拍摄模式
  • 自动化工作流:结合距离传感器或 AI 场景识别,自动调整对焦参数

对于短视频创作行业而言,手动对焦能力的开放意味着创作者可以在移动设备上实现以往只有专业相机才具备的对焦控制能力,极大地拓展了移动创作的表现力边界。

1.4 家庭短视频应用的场景契合度

本文以一款家庭短视频拍摄 App 为载体,深入解析如何将 HarmonyOS 6.1.1 Camera Kit 的两大新能力落地到实际产品中。家庭短视频场景具有以下典型特征,使其成为验证这些新特性的理想场景:

  • 用户群体广泛:从儿童到老人,用户技术水平参差不齐,需要智能化的辅助拍摄功能
  • 拍摄主体明确:以家庭成员为主要拍摄对象,人物跟踪需求强烈
  • 场景切换频繁:室内、户外、近景、远景等多种场景快速切换,对焦调整需求突出
  • 操作简便优先:家庭用户追求"拿起就拍"的体验,自动化能力价值显著

接下来,本文将从数据模型、存储管理、核心页面逻辑等多个维度,逐段解析代码实现,揭示如何将系统级相机能力转化为用户可感知的产品功能。


二、整体架构设计思想

2.1 分层架构设计

本应用采用经典的分层架构设计,将代码划分为数据模型层、常量层、存储管理层和页面表现层四个层次。各层职责清晰,依赖关系单向,保证了代码的可维护性和可扩展性。

┌─────────────────────────────────────────┐
│           页面表现层 (Pages)             │
│  拍摄首页 / 相册页 / 设置页 / 预设页 / 编辑页 │
├─────────────────────────────────────────┤
│         存储管理层 (StorageManager)       │
│   作品CRUD / 配置存取 / 预设管理 / 对焦距离  │
├─────────────────────────────────────────┤
│           常量层 (Constants)             │
│   存储键名 / 路由名 / 默认配置 / 选项枚举    │
├─────────────────────────────────────────┤
│          数据模型层 (Model)               │
│   FocusMode / VideoWork / CameraSetting  │
│   FocusPreset / IdGenerator              │
└─────────────────────────────────────────┘

2.2 设计模式运用

在架构设计中,运用了多种经典设计模式:

  • 单例模式StorageManager 采用静态方法 + 惰性初始化的方式实现单例,确保全局唯一的存储实例
  • 策略模式:对焦模式(自动/手动)的切换体现了策略模式思想,不同模式下对焦行为各异
  • 工厂模式IdGenerator 工具类封装了 ID 生成逻辑,相当于简单工厂
  • 观察者思想:页面状态通过 @State 装饰器驱动 UI 自动更新,体现了响应式编程思想

2.3 数据流向设计

整个应用的数据流向遵循单向数据流原则:

  1. 用户操作触发页面事件
  2. 页面调用存储管理层进行数据持久化
  3. 存储管理层更新本地 preferences
  4. 页面刷新状态变量,驱动 UI 重新渲染

这种设计使得数据变更路径清晰可追溯,便于调试和问题定位。


三、数据模型层深度解析

数据模型层是整个应用的基石,它定义了业务领域内的核心数据结构。好的数据模型设计能够准确反映业务语义,为上层业务逻辑提供清晰的数据契约。

3.1 FocusMode 枚举:对焦模式的类型安全抽象

export enum FocusMode {
  AUTO = 'auto',     // 自动对焦(点对焦后由系统决定对焦距离)
  MANUAL = 'manual'  // 手动对焦(由用户经 setFocusDistance 指定距离)
}

FocusMode 枚举是对焦模式的类型化抽象,它将两种对焦模式以枚举值的形式固定下来,避免了在代码中散落魔法字符串。

设计要点分析:

  • 字符串枚举而非数字枚举:使用 'auto''manual' 字符串值而非默认的数字值,有两个显著优势。一是持久化到存储时可读性更好,JSON 中存储的是有意义的字符串而非无意义的数字;二是与相机接口参数直接对应,无需额外的转换逻辑。

  • 注释体现业务语义:每个枚举值的注释不仅说明了其含义,还关联到了底层的技术实现(setFocusDistance),这使得代码具有自文档化的特性,开发者在阅读时能够立即理解其技术背景。

  • 扩展性设计:枚举结构天然支持扩展,如果未来新增其他对焦模式(如连续对焦、微距模式),只需增加新的枚举值即可,不会破坏现有代码结构。

3.2 VideoWork 接口:作品数据的完整建模

export interface VideoWork {
  workId: string;            // 作品ID
  title: string;             // 作品标题
  thumbnail: string;         // 缩略图(占位色块颜色值)
  duration: number;          // 作品时长(秒)
  createTime: number;        // 创建时间戳
  autoFramingUsed: boolean;  // 拍摄时是否开启影随人动自动构图
  focusDistance: number;     // 拍摄时对焦距离(米)
  resolution: string;        // 分辨率(1080P / 4K / 720P)
  frameRate: number;         // 帧率(24 / 30 / 60)
}

VideoWork 接口是拍摄作品的数据模型,它完整记录了一个短视频作品的所有元数据。

字段设计深度分析:

  • workId(作品ID):使用字符串类型而非数字类型,支持更灵活的 ID 生成策略(如时间戳 + 随机数的组合)。全局唯一标识符是数据 CRUD 操作的基础。

  • thumbnail(缩略图):设计为字符串类型,存储颜色值而非图片路径。这是一个巧妙的折中设计——在演示/原型阶段使用颜色占位可以降低开发复杂度,同时保持接口的扩展性;未来只需将此字段的语义从"颜色值"扩展为"图片路径"即可平滑升级。

  • autoFramingUsed(是否开启自动构图):这个布尔字段是 Camera Kit 新特性在数据层的直接映射。它记录了拍摄时的构图状态,使得作品不仅包含视频内容本身,还保留了创作时的技术参数,为后续的数据分析、智能推荐等功能奠定基础。

  • focusDistance(对焦距离):以米为单位的浮点数,精确记录拍摄时的对焦距离。这个字段是 setFocusDistance 接口在数据层的落点,体现了"创作参数可追溯"的设计理念。

  • resolution / frameRate:分辨率和帧率是视频质量的两个核心参数,与相机输出格式直接对应。将这些参数随作品一起保存,用户可以在作品详情中回顾拍摄时的技术配置。

接口设计思想:

VideoWork 接口的设计体现了"快照式记录"的思想——作品一旦生成,其元数据就被完整冻结。这种设计有几个好处:一是保证了作品数据的完整性和不可变性;二是即使后续相机配置发生变化,已拍摄作品的参数记录不会受影响;三是为数据分析提供了丰富的维度。

3.3 CameraSetting 接口:相机配置的结构化定义

export interface CameraSetting {
  defaultAutoFraming: boolean; // 默认自动构图开关(映射 cameraManager.setAutoFraming)
  focusMode: string;           // 对焦模式 'auto' | 'manual'
  resolution: string;          // 分辨率(映射 CameraFormat 输出格式)
  frameRate: number;           // 帧率
}

CameraSetting 接口定义了相机的全局配置项,每个配置项都直接映射到底层相机接口的参数。

设计亮点分析:

  • 一一映射原则:配置项与相机接口参数一一对应,这使得配置层与底层接口之间的转换成本极低。defaultAutoFraming 对应 setAutoFramingfocusMode 决定了使用自动对焦还是手动对焦(配合 setFocusDistance),resolution 对应 CameraFormat 输出格式。

  • 聚焦模式的类型选择focusMode 使用 string 类型而非 FocusMode 枚举类型,这是一个值得关注的设计决策。使用字符串类型的好处是与 JSON 序列化/反序列化天然兼容,从存储中读取时不需要额外的类型转换。但代价是失去了部分类型安全性。在实际项目中,可以根据团队对类型安全的要求进行权衡。

  • 命名中的"default"前缀defaultAutoFraming 字段名中的 “default” 前缀很重要,它明确表示这是一个"默认值"配置——用户每次进入拍摄页时的初始状态。用户在拍摄过程中可以临时切换自动构图开关,但不会影响这个默认配置,只有在设置页中修改才会持久化。这种"默认配置 + 临时调整"的模式是很多专业应用的常见设计。

3.4 FocusPreset 接口:对焦预设的场景化封装

export interface FocusPreset {
  presetId: string;      // 预设ID
  presetName: string;    // 预设名称
  focusDistance: number; // 对焦距离(米),应用时直接写入 setFocusDistance 接口
  sceneDesc: string;     // 适用场景说明
}

FocusPreset 接口定义了对焦预设的数据结构,是对焦距离能力的场景化封装。

设计深度解析:

  • 预设模式的价值:对焦预设是"将技术参数转化为场景语言"的典型设计。对于普通用户来说,“2.0米对焦距离"是一个抽象的技术参数,但"人像特写”、"室内访谈"这样的场景化名称则直观易懂。预设模式将技术参数包装成用户可理解的场景概念,大幅降低了使用门槛。

  • sceneDesc 字段的作用:场景说明字段不仅是展示用的描述文本,它还承担了"知识传递"的功能——用户在使用预设的过程中,逐渐理解不同对焦距离适用于什么场景,从而从"小白用户"向"进阶用户"成长。

  • 核心参数的单一性:每个预设只包含一个核心参数(focusDistance),这是刻意的简化设计。在 MVP 阶段,专注于单一维度的场景化封装可以快速验证产品价值;未来可以逐步扩展,将分辨率、帧率、自动构图等参数都纳入预设体系,形成"拍摄模式"的完整概念。

3.5 IdGenerator 工具类:分布式ID生成

export class IdGenerator {
  static gen(): string {
    return `v_${Date.now()}_${Math.floor(Math.random() * 10000)}`
  }

  static genPresetId(): string {
    return `p_${Date.now()}_${Math.floor(Math.random() * 10000)}`
  }
}

IdGenerator 是一个简洁但实用的 ID 生成工具类。

实现机制分析:

  • 时间戳 + 随机数组合Date.now() 提供毫秒级时间戳,保证了 ID 的时序性和大致唯一性;Math.floor(Math.random() * 10000) 提供 0-9999 的随机后缀,进一步降低了同一毫秒内生成重复 ID 的概率。

  • 前缀区分实体类型:作品 ID 以 v_ 开头(video),预设 ID 以 p_ 开头(preset)。这种前缀设计有几个实用价值:一是调试时一眼就能识别 ID 属于哪种实体;二是如果未来多种实体数据存储在同一张表或同一个数组中,前缀可以避免冲突;三是体现了命名的规范性。

  • 静态方法设计:所有方法都是静态方法,无需实例化即可使用。这符合工具类的设计惯例,调用方代码更简洁。

局限性与扩展思考:

当前实现对于本地应用来说已经足够,但如果未来扩展到云端同步场景,可能需要考虑更健壮的分布式 ID 方案,如 UUID、雪花算法等。不过按照 YAGNI(You Aren’t Gonna Need It)原则,当前的简洁实现是合理的。


四、常量层设计哲学

常量层是应用中所有"魔法值"的归处。一个设计良好的常量层不仅能提升代码的可读性和可维护性,还能作为应用的"配置中心",让参数调整变得简单安全。

4.1 存储键名常量

static readonly STORAGE_WORK_LIST = 'video_work_list_v1'
static readonly STORAGE_CAMERA_SETTING = 'camera_setting_v1'
static readonly STORAGE_FOCUS_PRESET = 'focus_preset_list_v1'
static readonly STORAGE_CURRENT_FOCUS = 'current_focus_distance_v1'

存储键名常量定义了本地持久化中使用的所有键名。

设计细节解析:

  • 命名规范:键名采用 模块_实体_版本号 的命名模式。STORAGE_ 前缀标识这是存储相关的常量,中间部分描述存储的内容,_v1 后缀表示版本号。这种命名方式在团队协作中非常有效,任何人看到键名都能立即理解其含义和归属。

  • 版本号机制:每个键名都带有 _v1 版本后缀,这是一个具有前瞻性的设计。当应用升级导致数据结构变化时,可以通过递增版本号来创建新的存储键,同时保留旧数据的迁移逻辑。如果没有版本号,直接修改数据结构可能导致老用户升级后数据解析失败。

  • 集中管理:所有存储键名集中定义在一个地方,避免了在代码各处散落字符串字面量。当需要修改键名时,只需改一处即可,不会出现遗漏。

4.2 路由名常量

static readonly ROUTE_INDEX = 'pages/Index'
static readonly ROUTE_CAMERA_SETTING = 'pages/CameraSettingPage'
static readonly ROUTE_ALBUM = 'pages/AlbumPage'
static readonly ROUTE_WORK_EDIT = 'pages/WorkEditPage'
static readonly ROUTE_FOCUS_PRESET = 'pages/FocusPresetPage'

路由名常量将页面路径集中管理。

设计价值:

  • 解耦页面引用:页面之间跳转时不直接使用路径字符串,而是引用常量。这样即使页面文件路径发生变化,只需修改常量定义即可,所有引用处自动生效。

  • IDE 支持:使用常量可以获得 IDE 的自动补全和重构支持。如果手动输入路径字符串,拼写错误只能在运行时发现;而使用常量,拼写错误会在编译期就被捕获。

  • 导航参数契约:结合路由名和参数约定,可以形成清晰的页面间导航契约。例如从相册页跳转到作品编辑页时携带 workId 参数,这种约定在常量层附近集中注释说明最为合适。

4.3 默认配置常量

static readonly DEFAULT_AUTO_FRAMING = true
static readonly DEFAULT_FOCUS_MODE = 'auto'
static readonly DEFAULT_RESOLUTION = '1080P'
static readonly DEFAULT_FRAME_RATE = 30
static readonly DEFAULT_FOCUS_DISTANCE = 2.0

默认配置常量定义了各项相机参数的初始值。

产品决策的体现:

  • 自动构图默认开启DEFAULT_AUTO_FRAMING = true 表明产品设计者认为自动构图是一项对家庭用户友好的功能,默认开启可以让用户立即体验到智能拍摄的便利。

  • 自动对焦为默认模式DEFAULT_FOCUS_MODE = 'auto' 符合大多数用户的使用习惯。手动对焦是专业功能,作为可选项提供给有需要的用户。

  • 1080P + 30fps 的平衡选择:默认分辨率 1080P、帧率 30fps 是在画质、文件体积、性能消耗之间的平衡选择。4K 画质更好但文件更大、对设备性能要求更高;720P 体积更小但画质下降明显。1080P 30fps 是目前短视频平台的主流规格。

  • 2.0米默认对焦距离:2.0米是一个比较"中庸"的距离,既适合自拍,也适合拍摄中景画面。这个距离上大多数镜头都能获得不错的成像质量。

4.4 对焦距离范围常量

static readonly FOCUS_DISTANCE_MIN = 0.1
static readonly FOCUS_DISTANCE_MAX = 10.0
static readonly FOCUS_DISTANCE_STEP = 0.1

对焦距离范围常量定义了手动对焦的可调节范围和精度。

技术参数的产品化封装:

  • 0.1米微距下限:0.1米(10厘米)的最近对焦距离已经接近大多数手机镜头的微距能力极限。这个下限保证了用户可以拍摄近距离的细节特写。

  • 10.0米远景上限:10米的上限对于手机拍摄来说已经足够覆盖绝大多数日常场景。超过10米后,对于手机镜头来说基本都处于超焦距范围,对焦距离的变化对成像影响不大。

  • 0.1米步长精度:0.1米的步长在精度和操作性之间取得了平衡。步长太小(如0.01米)会导致滑块操作过于敏感,用户难以精确控制;步长太大(如1米)则精度不够,无法满足精细对焦的需求。0.1米是一个经过实践验证的合理步长。

4.5 动画时长常量

static readonly TAP_FOCUS_BOX_DURATION = 1500      // 点对焦框显示时长
static readonly SUBJECT_TRACK_DURATION = 1600      // 主体跟踪动画时长
static readonly SUBJECT_TRACK_INTERVAL = 2000      // 主体跟踪间隔

动画时长常量控制着各种 UI 动画的时间参数。

用户体验的量化表达:

  • 1500毫秒对焦框显示:点对焦框显示1.5秒后自动消失,这个时长经过了精心的 UX 设计——足够用户看清对焦位置,又不会长时间遮挡画面影响拍摄。

  • 1600毫秒跟踪动画 + 2000毫秒间隔:主体跟踪动画采用"移动1.6秒 + 暂停2秒"的节奏。这种设计模拟了真实拍摄中人物间歇性移动的状态,动画时长足够让用户清晰地看到"跟随"效果,间隔时长则避免了画面过于频繁变化造成的视觉疲劳。

4.6 默认对焦预设数据

static readonly MOCK_PRESET_LIST: FocusPreset[] = [
  { presetId: 'p_mock_1', presetName: '人像特写', focusDistance: 0.5, sceneDesc: '近距离人物面部特写,背景自然虚化' },
  { presetId: 'p_mock_2', presetName: '室内访谈', focusDistance: 1.2, sceneDesc: '室内固定机位访谈,人物半身构图' },
  { presetId: 'p_mock_3', presetName: '户外跟拍', focusDistance: 3.0, sceneDesc: '户外运动跟拍,配合影随人动自动构图' },
  { presetId: 'p_mock_4', presetName: '风景远景', focusDistance: 8.0, sceneDesc: '远景风光拍摄,超焦距大范围清晰' }
]

默认对焦预设数据是应用首次启动时注入的初始数据。

预设设计的用户教育价值:

这四个预设不是随意设定的,而是覆盖了家庭短视频拍摄的典型场景,每一个都有明确的产品意图:

  • 人像特写(0.5m):展示了近摄时的背景虚化效果,让用户直观理解"对焦距离影响景深"这一概念
  • 室内访谈(1.2m):这是家庭记录中最常见的场景之一,预设值对应室内常见的拍摄距离
  • 户外跟拍(3.0m):与自动构图功能形成联动,说明手动对焦和自动构图可以配合使用
  • 风景远景(8.0m):展示了超焦距拍摄的概念,扩展用户对创作可能性的认知

这四个预设相当于内置的"教程",用户在使用预设的过程中自然学习到不同场景下的对焦参数选择。


五、存储管理层架构解析

存储管理层是连接页面逻辑和本地持久化的桥梁。StorageManager 类采用单例模式设计,封装了所有与数据持久化相关的操作,为上层提供了简洁的异步 API。

5.1 单例初始化机制

export class StorageManager {
  private static store: preferences.Preferences | null = null

  static async init(context: Context | undefined): Promise<void> {
    if (StorageManager.store === null) {
      if (context === undefined) {
        throw new Error('StorageManager.init: context 为空,无法初始化 preferences')
      }
      StorageManager.store = await preferences.getPreferences(context, 'shortvideo_pref')
    }
  }

  private static ensureStore(): preferences.Preferences {
    if (StorageManager.store === null) {
      throw new Error('StorageManager 未初始化,请先调用 init()')
    }
    return StorageManager.store
  }
}

初始化机制深度解析:

  • 惰性初始化(Lazy Initialization)store 静态变量初始为 null,只有在首次调用 init() 时才会真正创建 preferences 实例。这种设计避免了应用启动时就进行不必要的存储初始化,加快了启动速度。

  • 幂等性保证init() 方法内部先判断 store === null,确保多次调用 init() 不会重复创建实例。这一点很重要,因为每个页面的 aboutToAppear 中都会调用 init(),如果不做幂等处理,会导致重复初始化。

  • Context 参数的可选处理context 参数类型为 Context | undefined,这是因为 getHostContext() 的返回类型是可选的。代码中对 undefined 情况做了显式检查并抛出有意义的错误信息,而不是让错误在 getPreferences 调用时才暴露,提升了调试效率。

  • 守卫方法 ensureStoreensureStore() 是一个私有守卫方法,所有存取操作在执行前都会调用它来确保存储实例已初始化。这种"防御式编程"的做法可以在开发阶段尽早发现问题——如果忘记调用 init() 就直接存取数据,会得到清晰的错误提示而不是莫名其妙的空指针异常。

单例模式的权衡:

单例模式的优点是全局唯一、访问方便、节省资源。但也要注意其潜在缺点:一是不利于单元测试(难以 mock);二是全局状态可能导致隐式耦合。对于本应用这样的中小型项目,单例模式带来的便利性大于其弊端,是合理的选择。

5.2 作品列表 CRUD 操作

5.2.1 getWorkList - 读取作品列表
static async getWorkList(): Promise<VideoWork[]> {
  const store = StorageManager.ensureStore()
  const raw = await store.get(AppConstants.STORAGE_WORK_LIST, '[]') as string
  try {
    return JSON.parse(raw) as VideoWork[]
  } catch (e) {
    return []
  }
}

函数作用:从本地存储中读取全部拍摄作品列表。

参数与返回值

  • 参数:无
  • 返回值:Promise<VideoWork[]>,异步返回作品数组。即使存储中没有数据或解析失败,也会返回空数组而非 nullundefined

实现细节分析:

  • 默认值策略store.get() 的第二个参数 '[]' 是默认值。当键不存在时返回空数组的 JSON 字符串,确保了后续 JSON.parse 不会因为 nullundefined 而出错。

  • 容错处理try-catch 包裹 JSON.parse 调用,防止因数据损坏(如用户手动修改了存储文件、版本不兼容等)导致整个应用崩溃。解析失败时返回空数组,实现了优雅降级。

  • 类型断言as VideoWork[] 是 TypeScript 的类型断言。由于 JSON.parse 返回的是 any 类型,需要断言为具体的接口类型以获得类型检查和 IDE 提示。需要注意的是,这只是编译期的类型标注,运行时并不会真正校验每个字段的类型。

5.2.2 saveWorkList - 保存作品列表
static async saveWorkList(list: VideoWork[]): Promise<void> {
  const store = StorageManager.ensureStore()
  await store.put(AppConstants.STORAGE_WORK_LIST, JSON.stringify(list))
  await store.flush()
}

函数作用:将整个作品列表保存到本地存储。

参数与返回值

  • 参数:list: VideoWork[] - 要保存的作品列表
  • 返回值:Promise<void>,保存完成的异步信号

实现细节分析:

  • 整体覆盖策略:采用"整体读取-修改-整体写回"的策略,而不是单条记录的增删改。对于数据量不大的场景(家庭短视频应用的作品数量通常在几十到几百条),这种方式实现简单且性能足够。

  • flush 的必要性put() 方法只是将数据写入内存缓存,flush() 才会将数据真正持久化到磁盘。调用 flush() 确保了数据不会因为应用意外退出而丢失。对于重要数据(如用户拍摄的作品),每次修改后都应调用 flush()

5.2.3 upsertWork - 新增或更新作品
static async upsertWork(work: VideoWork): Promise<void> {
  const list = await StorageManager.getWorkList()
  const idx = list.findIndex(w => w.workId === work.workId)
  if (idx >= 0) {
    list[idx] = work
  } else {
    list.unshift(work)
  }
  await StorageManager.saveWorkList(list)
}

函数作用:新增或更新单个作品。如果作品已存在(相同 workId)则更新,不存在则添加到列表开头。

参数与返回值

  • 参数:work: VideoWork - 要新增或更新的作品对象
  • 返回值:Promise<void>

实现细节分析:

  • Upsert 模式:upsert 是 update + insert 的合成词,表示"更新或插入"。这种设计简化了上层调用——调用方不需要关心作品是否已存在,统一调用 upsertWork 即可。

  • 新作品置顶策略list.unshift(work) 将新作品添加到数组开头,保证了作品列表按"最新在前"的顺序排列。这符合用户的心理预期——刚拍摄的作品应该最先看到。

  • 先读后写的原子性问题:当前实现是"读取整个列表 → 修改内存中的数组 → 写回整个列表"。在单线程的 JavaScript 环境中,这个操作序列本身不会有并发问题。但如果未来引入多线程或多实例场景,可能需要考虑加锁或使用更细粒度的存储方案。

5.2.4 deleteWork - 删除作品
static async deleteWork(workId: string): Promise<void> {
  const list = await StorageManager.getWorkList()
  const filtered = list.filter(w => w.workId !== workId)
  await StorageManager.saveWorkList(filtered)
}

函数作用:根据作品ID删除单个作品。

参数与返回值

  • 参数:workId: string - 要删除的作品ID
  • 返回值:Promise<void>

实现细节分析:

  • filter 的不可变操作:使用 filter 方法生成新数组,而不是在原数组上 splice。这种不可变数据操作的风格更符合函数式编程思想,避免了副作用,代码可读性也更好。

  • 删除不存在的 ID:如果传入的 workId 不在列表中,filter 会返回原数组的拷贝,保存后数据不变。这个行为是"幂等"的——删除一个不存在的作品不会报错,只是什么都不做。这种设计简化了上层逻辑,调用方无需先检查是否存在。

5.3 相机配置存取

5.3.1 getCameraSetting - 读取相机配置
static async getCameraSetting(): Promise<CameraSetting> {
  const store = StorageManager.ensureStore()
  const raw = await store.get(AppConstants.STORAGE_CAMERA_SETTING, '{}') as string
  try {
    const cfg = JSON.parse(raw) as CameraSetting
    return StorageManager.fillCameraDefault(cfg)
  } catch (e) {
    return StorageManager.defaultCameraSetting()
  }
}

函数作用:从存储中读取相机配置,并自动填充缺失的默认值。

参数与返回值

  • 参数:无
  • 返回值:Promise<CameraSetting>,完整的相机配置对象(所有字段都有有效值)

实现细节分析:

  • 默认值填充机制:这是这个函数最核心的设计——读取配置后调用 fillCameraDefault() 补全缺失字段。这样做的好处是:即使存储中的配置数据是旧版本(缺少新增的配置项),读取后也能自动补全,保证上层代码拿到的配置对象总是完整的。

  • 空对象作为默认值store.get() 的默认值是 '{}' 而不是完整的默认配置 JSON。这是因为完整的默认配置由 defaultCameraSetting() 统一管理,如果在这里也写一份默认值的 JSON,会造成重复,未来修改默认值时容易遗漏。

5.3.2 defaultCameraSetting - 默认配置工厂
static defaultCameraSetting(): CameraSetting {
  return {
    defaultAutoFraming: AppConstants.DEFAULT_AUTO_FRAMING,
    focusMode: AppConstants.DEFAULT_FOCUS_MODE,
    resolution: AppConstants.DEFAULT_RESOLUTION,
    frameRate: AppConstants.DEFAULT_FRAME_RATE
  }
}

函数作用:生成一份全新的默认相机配置。

参数与返回值

  • 参数:无
  • 返回值:CameraSetting,全新的默认配置对象

设计分析:

  • 常量的二次封装:虽然默认值已经在 AppConstants 中定义,但这里又提供了一个工厂函数。这样做的价值在于:将分散的常量组装成结构化的配置对象,上层调用方不需要知道每个默认值对应哪个常量,直接调用函数就能拿到完整的配置对象。

  • 非异步方法:这个方法是同步的,不需要 await。因为它只是在内存中构造一个对象,不涉及 IO 操作。这意味着在一些无法使用 async 的场景(如变量初始化)也可以直接调用。

5.3.3 fillCameraDefault - 默认值补全
private static fillCameraDefault(cfg: CameraSetting): CameraSetting {
  return {
    defaultAutoFraming: cfg?.defaultAutoFraming ?? AppConstants.DEFAULT_AUTO_FRAMING,
    focusMode: cfg?.focusMode || AppConstants.DEFAULT_FOCUS_MODE,
    resolution: cfg?.resolution || AppConstants.DEFAULT_RESOLUTION,
    frameRate: cfg?.frameRate || AppConstants.DEFAULT_FRAME_RATE
  }
}

函数作用:为不完整的配置对象填充默认值,确保返回的配置对象所有字段都有有效值。

参数与返回值

  • 参数:cfg: CameraSetting - 可能不完整的配置对象
  • 返回值:CameraSetting - 补全后的完整配置对象

技术细节分析:

  • 空值合并运算符 ??defaultAutoFraming 使用了 ??(空值合并运算符),只有当值为 nullundefined 时才使用默认值。这意味着 false 是一个有效的值——用户明确关闭自动构图,不应该被默认值覆盖。

  • 逻辑或运算符 ||focusModeresolutionframeRate 使用了 ||(逻辑或),当值为 falsy(空字符串、0、null、undefined 等)时使用默认值。对于这些字段,空字符串被视为"无效值",应该回退到默认值。

  • 两种运算符的选择差异:为什么布尔字段用 ?? 而字符串/数字字段用 ||?这是因为布尔字段的 false 是有效值,而字符串字段的 ''(空字符串)通常不被视为有效值。这个细节体现了对业务语义的精确把握。

  • 可选链 ?.cfg?.defaultAutoFraming 中的可选链操作符确保了即使 cfgnullundefined,代码也不会报错,而是安全地使用默认值。

5.4 对焦预设存取

5.4.1 getPresetList - 读取预设列表
static async getPresetList(): Promise<FocusPreset[]> {
  const store = StorageManager.ensureStore()
  const raw = await store.get(AppConstants.STORAGE_FOCUS_PRESET, '[]') as string
  try {
    const list = JSON.parse(raw) as FocusPreset[]
    if (list.length === 0) {
      await StorageManager.savePresetList(AppConstants.MOCK_PRESET_LIST)
      return AppConstants.MOCK_PRESET_LIST
    }
    return list
  } catch (e) {
    await StorageManager.savePresetList(AppConstants.MOCK_PRESET_LIST)
    return AppConstants.MOCK_PRESET_LIST
  }
}

函数作用:读取对焦预设列表。首次启动或数据损坏时,自动注入默认预设数据。

参数与返回值

  • 参数:无
  • 返回值:Promise<FocusPreset[]>,预设列表数组

实现细节分析:

  • 首次启动自动注入:当读取到空列表时,自动将 MOCK_PRESET_LIST 写入存储并返回。这种设计使得应用首次启动时就有可用的预设数据,用户不需要从零开始创建,降低了新用户的上手门槛。

  • 双路径注入:无论是正常解析后发现为空(list.length === 0),还是解析异常(catch 分支),都会执行默认数据注入。这种"双通道保障"确保了即使存储数据损坏,用户也不会看到空列表——至少有四个默认预设可用。

  • 副作用的争议:在 get 方法中执行 save 操作(写入默认数据)是一种副作用。纯函数主义者可能不认同这种做法,但从产品体验角度,这种"自动初始化"的设计确实带来了更好的用户体验。关键在于这个副作用是良性的、幂等的(首次注入后后续读取不会再触发写入)。

5.5 当前对焦距离存取

5.5.1 getCurrentFocusDistance - 读取当前对焦距离
static async getCurrentFocusDistance(): Promise<number> {
  const store = StorageManager.ensureStore()
  const raw = await store.get(AppConstants.STORAGE_CURRENT_FOCUS, `${AppConstants.DEFAULT_FOCUS_DISTANCE}`) as string
  const value = parseFloat(raw)
  return isNaN(value) ? AppConstants.DEFAULT_FOCUS_DISTANCE : value
}

函数作用:读取全局当前对焦距离。这个值是预设应用后写入的,拍摄页每次显示时读取并应用到相机。

参数与返回值

  • 参数:无
  • 返回值:Promise<number>,当前对焦距离(米)

实现细节分析:

  • 字符串存储数字:preferences 接口支持多种数据类型,但这里选择用字符串存储数字,再通过 parseFloat 转换。这样做的原因可能是为了统一存储格式,或者是为了与其他字符串类型的存储保持一致的处理方式。

  • isNaN 容错parseFloat 可能返回 NaN(如果存储的内容不是有效数字),这时使用默认值兜底。这是一个健壮性设计,防止因数据异常导致页面显示异常。

  • 全局状态的设计考量:将"当前对焦距离"作为全局状态单独存储,而不是作为相机配置的一部分,是一个值得关注的设计决策。这是因为对焦距离是一个"易变"的状态——用户在拍摄过程中随时可能调整,从预设页应用后也会立即改变。将其从配置中独立出来,语义上更清晰(配置是"半永久"的,当前对焦距离是"瞬时"的)。


六、拍摄首页核心逻辑深度解析

拍摄首页是整个应用的核心页面,也是 Camera Kit 两大新特性的主要落地场景。这个页面集成了影随人动自动构图、对焦距离调节、点对焦、拍摄等核心功能。

6.1 页面状态设计

@State autoFraming: boolean = true          // 影随人动自动构图开关
@State focusDistance: number = AppConstants.DEFAULT_FOCUS_DISTANCE // 当前对焦距离(米)
@State setting: CameraSetting = StorageManager.defaultCameraSetting()
@State isCapturing: boolean = false         // 模拟拍摄进行中
@State workCount: number = 0                // 相册作品数

// 主体框(模拟影随人动跟踪目标)
@State subjectX: number = 120
@State subjectY: number = 140

// 点对焦框
@State showFocusBox: boolean = false
@State focusBoxX: number = 0
@State focusBoxY: number = 0

状态设计思想分析:

  • 状态分类:页面状态可以分为几类:相机功能状态(autoFramingfocusDistancesetting)、操作状态(isCapturing)、UI 动画状态(subjectXsubjectYshowFocusBoxfocusBoxXfocusBoxY)、统计信息(workCount)。分类清晰的状态设计有助于理解和维护。

  • @State 装饰器的响应式特性:所有状态变量都使用 @State 装饰器标记,这意味着它们的变化会自动触发 UI 重新渲染。这是 ArkUI 声明式 UI 的核心特性——开发者只需要关注数据状态的变化,不需要手动操作 UI 控件。

  • 模拟状态的必要性subjectXsubjectY 等状态是为了模拟影随人动效果而设计的。在真实的 Camera Kit 实现中,主体跟踪由相机底层完成,应用层不需要维护这些状态。但在演示/模拟场景下,这些状态让开发者能够在不依赖真实相机硬件的情况下展示功能效果。

6.2 生命周期管理

6.2.1 aboutToAppear - 页面初始化
aboutToAppear(): void {
  this.initCameraState()
}

函数作用:页面即将出现时调用,执行一次性初始化。

设计分析:

  • 单一职责aboutToAppear 只做一件事——调用 initCameraState()。这种简洁的写法将具体逻辑委托给命名更清晰的方法,提高了可读性。

  • 与 onPageShow 的区别:在 ArkUI 中,aboutToAppear 在页面首次创建时调用一次,而 onPageShow 在每次页面显示时(包括从其他页面返回)都会调用。两者的分工是:aboutToAppear 做一次性初始化,onPageShow 做每次显示都需要的刷新。

6.2.2 onPageShow - 页面显示刷新
onPageShow(): void {
  // 从设置页 / 对焦预设页 / 相册页返回时刷新相机状态
  this.initCameraState()
}

函数作用:每次页面显示时调用,刷新相机状态。

设计分析:

  • 跨页面状态同步:用户可能在设置页修改了相机配置、在对焦预设页应用了新的对焦距离、在相册页删除了作品。这些操作都会影响拍摄首页的显示状态,因此每次返回时都需要重新读取最新数据。

  • 与 aboutToAppear 复用同一方法:两个生命周期钩子调用同一个 initCameraState() 方法。这种代码复用方式避免了重复逻辑,确保了初始化和刷新的一致性。

6.2.3 onPageHide - 页面隐藏清理
onPageHide(): void {
  this.stopSubjectTracking()
}

函数作用:页面隐藏时调用,停止主体跟踪动画。

设计分析:

  • 资源释放意识:当页面不再可见时,及时停止定时器动画,避免不必要的 CPU 消耗和内存占用。这是良好的编程习惯——有借有还,再借不难。

  • 与 aboutToDisappear 的分工onPageHide 在页面被覆盖时调用(如跳转到其他页面),aboutToDisappear 在页面被销毁时调用(如返回上一页)。两者的清理程度不同:onPageHide 只停止动画(页面还在内存中,回来时可以快速恢复),aboutToDisappear 则做更彻底的清理。

6.2.4 aboutToDisappear - 页面销毁清理
aboutToDisappear(): void {
  this.stopSubjectTracking()
  if (this.focusBoxTimer >= 0) {
    clearTimeout(this.focusBoxTimer)
    this.focusBoxTimer = -1
  }
}

函数作用:页面即将销毁时调用,执行彻底的资源清理。

设计分析:

  • 双重保险:再次调用 stopSubjectTracking() 确保跟踪定时器被清理,即使之前 onPageHide 没有被正确触发(比如某些特殊的页面跳转场景)。

  • 对焦框定时器清理:除了跟踪定时器,还需要清理对焦框的自动消失定时器。如果页面销毁时这个定时器还在,回调执行时页面已经不存在,可能导致异常或内存泄漏。

  • 定时器标记约定:使用 -1 作为"定时器不存在"的标记值,这是一种常见的约定。setIntervalsetTimeout 返回的定时器 ID 都是正整数,因此用 -1 表示无效状态是安全的。

6.3 initCameraState - 初始化核心方法

private async initCameraState(): Promise<void> {
  try {
    await StorageManager.init(this.getUIContext().getHostContext())
    this.setting = await StorageManager.getCameraSetting()
    this.autoFraming = this.setting.defaultAutoFraming
    this.focusDistance = await StorageManager.getCurrentFocusDistance()
    const list = await StorageManager.getWorkList()
    this.workCount = list.length
  } catch (e) {
    console.error('初始化相机状态失败:' + JSON.stringify(e))
  }
  this.startSubjectTracking()
}

函数作用:初始化相机状态,包括读取配置、读取当前对焦距离、获取作品数量,并启动主体跟踪动画。

参数与返回值

  • 参数:无
  • 返回值:Promise<void>

执行流程分析:

initCameraState
    │
    ├─► StorageManager.init(确保存储已初始化)
    │
    ├─► getCameraSetting(读取相机配置)
    │    │
    │    └─► 设置 setting 状态
    │         └─► autoFraming = setting.defaultAutoFraming
    │
    ├─► getCurrentFocusDistance(读取当前对焦距离)
    │    │
    │    └─► 设置 focusDistance 状态
    │
    ├─► getWorkList(读取作品列表)
    │    │
    │    └─► 设置 workCount = list.length
    │
    └─► startSubjectTracking(启动主体跟踪动画)

关键设计点:

  • StorageManager.init 的位置:在每次 initCameraState 中都调用 StorageManager.init(),这是因为无法保证在页面出现时存储已经被初始化过。由于 init() 内部有幂等判断,多次调用是安全的。

  • 错误隔离:整个数据读取过程包裹在 try-catch 中,任何一步失败都不会导致页面崩溃,只是状态停留在默认值。这体现了"前端容错"的设计思想——宁可显示默认数据,也不能让页面白屏。

  • 跟踪动画始终启动startSubjectTracking() 调用放在 try-catch 外面,意味着无论数据读取是否成功,都会启动跟踪动画。这是合理的——即使数据读取失败,UI 展示功能也应该正常工作。

6.4 影随人动自动构图实现

6.4.1 startSubjectTracking - 启动跟踪
private startSubjectTracking(): void {
  if (this.trackTimer >= 0) {
    return
  }
  this.trackTimer = setInterval(() => {
    if (this.autoFraming) {
      this.moveSubject()
    }
  }, AppConstants.SUBJECT_TRACK_INTERVAL)
}

函数作用:启动主体跟踪定时器,周期性地移动主体位置(模拟影随人动效果)。

参数与返回值

  • 参数:无
  • 返回值:无

实现细节分析:

  • 防重复启动:函数开头先检查 trackTimer >= 0,如果定时器已经存在则直接返回。这是一个重要的防护措施,防止多次调用导致创建多个定时器,造成动画速度异常或内存泄漏。

  • 条件触发动画:定时器内部判断 this.autoFraming,只有当自动构图开启时才执行移动。这样设计的好处是:定时器始终在运行,开关切换时不需要启停定时器,只需要改变条件判断结果。这种方式比"开关打开时启动定时器、关闭时停止"的方式更简单,也避免了频繁启停定时器可能带来的问题。

  • 间隔控制:间隔时间由 SUBJECT_TRACK_INTERVAL = 2000 常量控制,即每2秒触发一次主体移动。这个频率既不会让画面变化太快造成眩晕,也不会太慢以至于用户感受不到"跟踪"效果。

6.4.2 stopSubjectTracking - 停止跟踪
private stopSubjectTracking(): void {
  if (this.trackTimer >= 0) {
    clearInterval(this.trackTimer)
    this.trackTimer = -1
  }
}

函数作用:停止主体跟踪定时器。

参数与返回值

  • 参数:无
  • 返回值:无

实现细节分析:

  • 安全清理:先检查定时器是否存在,再执行清理。清理后将 trackTimer 重置为 -1,标记为"已停止"状态。这种"清理 + 重置标记"的模式是定时器管理的标准做法。

  • 与 start 配对使用startSubjectTrackingstopSubjectTracking 是一对配对的方法,分别在页面显示和隐藏时调用。这种对称性设计使得资源管理清晰可控。

6.4.3 moveSubject - 主体移动动画
private moveSubject(): void {
  this.getUIContext().animateTo({
    duration: AppConstants.SUBJECT_TRACK_DURATION,
    curve: Curve.EaseInOut
  }, () => {
    this.subjectX = 16 + Math.random() * 200
    this.subjectY = 56 + Math.random() * 220
  })
}

函数作用:通过动画将主体框移动到一个新的随机位置,模拟人物在画面中移动时取景框跟随的效果。

参数与返回值

  • 参数:无
  • 返回值:无

实现细节分析:

  • animateTo 动画 API:使用 animateTo 显式动画 API,将状态变化包裹在动画闭包中。ArkUI 的动画系统会自动计算起始状态和结束状态之间的插值,生成平滑的过渡动画。

  • EaseInOut 缓动曲线Curve.EaseInOut 是最常用的缓动曲线之一——动画开始时缓慢加速,结束时缓慢减速,中间匀速。这种曲线模拟了真实物理世界中物体的运动规律,看起来更自然。

  • 随机位置计算

    • subjectX = 16 + Math.random() * 200:X 坐标范围 16~216
    • subjectY = 56 + Math.random() * 220:Y 坐标范围 56~276

    这些数值是根据取景区域的大小计算的,确保主体框始终在画面范围内,不会移出边界。

  • 模拟 vs 真实:在真实的 Camera Kit AUTO_FRAMING 实现中,主体位置由相机底层的人体检测算法实时确定,应用层不需要手动计算。这里的随机移动只是为了演示效果,让用户能够直观看到"取景框跟随主体移动"的视觉效果。

6.4.4 onToggleAutoFraming - 自动构图开关切换
private onToggleAutoFraming(on: boolean): void {
  this.autoFraming = on
  if (on) {
    this.moveSubject()
    promptAction.showToast({ message: '影随人动已开启,取景框将跟随主体', duration: 1200 })
  } else {
    this.getUIContext().animateTo({ duration: 400, curve: Curve.EaseOut }, () => {
      this.subjectX = 120
      this.subjectY = 140
    })
    promptAction.showToast({ message: '影随人动已关闭', duration: 1000 })
  }
}

函数作用:处理自动构图开关的切换事件,更新状态并给出视觉反馈。

参数与返回值

  • 参数:on: boolean - 开关是否打开
  • 返回值:无

核心特性映射:

真实调用方式:await cameraManager.setAutoFraming(on)

本函数中的 this.autoFraming = on 即对应该接口调用的状态更新。

行为细节分析:

  • 开启时的即时反馈:打开开关后立即调用一次 moveSubject(),让用户马上看到主体移动的效果,而不是等待下一个定时器周期(2秒后)。这种"即时反馈"设计很重要——用户操作后应该立即看到结果,否则会怀疑功能是否生效。

  • 关闭时的归位动画:关闭开关后,主体框以 400ms 的动画平滑回到画面中心位置(120, 140)。这个"归位"动作用户体验很好——它清晰地传达了"自动构图关闭,主体不再被跟踪,回到固定位置"的状态变化。如果没有动画,主体框瞬间跳回中心,用户体验会比较突兀。

  • Toast 提示:开关切换后显示 Toast 提示,告知用户功能状态。Toast 是轻量级的反馈方式,不会打断用户操作,同时又能提供明确的状态确认。

6.5 对焦距离调节实现

6.5.1 onFocusDistanceChange - 对焦距离变化处理
private onFocusDistanceChange(value: number, mode: SliderChangeMode): void {
  this.focusDistance = Math.round(value * 10) / 10
  if (mode === SliderChangeMode.End || mode === SliderChangeMode.Click) {
    StorageManager.saveCurrentFocusDistance(this.focusDistance).then(() => {
      promptAction.showToast({ message: `对焦距离已设为 ${this.focusDistance.toFixed(1)}m`, duration: 1000 })
    }).catch(() => {
      promptAction.showToast({ message: '对焦距离保存失败' })
    })
  }
}

函数作用:处理对焦距离滑块的变化事件,实时更新显示值,并在用户结束操作时保存到存储。

参数与返回值

  • 参数:
    • value: number - 滑块当前值(对焦距离,单位米)
    • mode: SliderChangeMode - 变化模式(开始/拖动中/结束/点击)
  • 返回值:无

核心特性映射:

真实调用方式:await cameraManager.setFocusDistance(value)

滑块拖动过程中实时更新 focusDistance 状态对应实时对焦调节;
拖动结束时保存到存储对应确认对焦距离设置。

实现细节分析:

  • 精度处理Math.round(value * 10) / 10 将值保留一位小数。这是因为对焦距离的步长是 0.1 米,理论上滑块值应该正好是 0.1 的整数倍,但浮点数计算可能产生精度误差(如 2.0 变成 1.9999999),通过四舍五入确保显示值的精确性。

  • 保存时机的选择:只有当 modeEnd(拖动结束)或 Click(点击跳转)时才执行保存操作。如果在拖动过程中(Move 模式)每次变化都保存,会造成频繁的 IO 操作,影响性能。这种"拖动时实时预览、结束时确认保存"的模式是滑块控件的标准交互模式。

  • Promise 链式调用:保存操作是异步的,使用 .then().catch() 分别处理成功和失败情况。保存成功后显示 Toast 告知用户新的对焦距离,保存失败也有对应的错误提示。

  • 实时更新与异步保存的分离:UI 状态(focusDistance)是同步更新的,用户拖动滑块时看到的数值变化是即时的;而持久化操作是异步的,在后台进行。这种设计保证了 UI 的响应性,用户不会因为存储延迟而感到卡顿。

6.5.2 onTapFocus - 点对焦处理
private onTapFocus(event: ClickEvent): void {
  this.focusBoxX = event.x
  this.focusBoxY = event.y
  this.showFocusBox = true
  if (this.focusBoxTimer >= 0) {
    clearTimeout(this.focusBoxTimer)
  }
  this.focusBoxTimer = setTimeout(() => {
    this.showFocusBox = false
    this.focusBoxTimer = -1
  }, AppConstants.TAP_FOCUS_BOX_DURATION)
}

函数作用:处理取景区域的点击事件,在点击位置显示对焦框,模拟点对焦效果。

参数与返回值

  • 参数:event: ClickEvent - 点击事件对象,包含点击坐标等信息
  • 返回值:无

核心特性映射:

真实调用方式:点对焦由系统自动选择对焦点并联动 getFocusDistance()
手动场景可再经 setFocusDistance 微调。

本函数以 1.5s 对焦框动画模拟该过程。

实现细节分析:

  • 点击坐标的获取event.xevent.y 是点击位置相对于组件的坐标。将这两个值直接赋值给 focusBoxXfocusBoxY,对焦框就会显示在用户点击的位置。

  • 对焦框重置逻辑:每次点击时,如果之前的对焦框定时器还在运行(focusBoxTimer >= 0),先清除旧的定时器,再创建新的。这样做的效果是:连续点击时,对焦框会移动到最新的点击位置,并重新计时1.5秒后消失。

  • 1.5秒自动消失:对焦框显示 1500 毫秒(1.5秒)后自动消失。这个时长设计考虑了两个因素:一是用户需要足够的时间确认对焦位置;二是对焦框不能长时间遮挡画面影响拍摄构图。

  • 模拟性质说明:在真实相机应用中,点对焦不仅是视觉效果,还会触发实际的对焦动作——相机会调整镜头使点击位置成像清晰。在本模拟实现中,只展示了视觉效果(对焦框),没有实际调整对焦距离。如果要更完整地模拟,可以根据点击位置估算对焦距离并调用 setFocusDistance

6.6 拍摄功能实现

6.6.1 doCapture - 执行拍摄
private async doCapture(): Promise<void> {
  if (this.isCapturing) {
    return
  }
  this.isCapturing = true
  promptAction.showToast({ message: '拍摄中...', duration: 800 })
  await this.sleep(AppConstants.CAPTURE_DURATION)
  const paletteIdx = Math.floor(Math.random() * AppConstants.THUMBNAIL_PALETTE.length)
  const work: VideoWork = {
    workId: IdGenerator.gen(),
    title: `短视频作品 ${this.formatTime(Date.now())}`,
    thumbnail: AppConstants.THUMBNAIL_PALETTE[paletteIdx],
    duration: AppConstants.MOCK_DURATION_MIN +
      Math.floor(Math.random() * (AppConstants.MOCK_DURATION_MAX - AppConstants.MOCK_DURATION_MIN)),
    createTime: Date.now(),
    autoFramingUsed: this.autoFraming,
    focusDistance: this.focusDistance,
    resolution: this.setting.resolution,
    frameRate: this.setting.frameRate
  }
  try {
    await StorageManager.upsertWork(work)
    this.workCount += 1
    promptAction.showToast({ message: '拍摄完成,已保存到相册', duration: 1200 })
  } catch (e) {
    promptAction.showToast({ message: '保存作品失败' })
  } finally {
    this.isCapturing = false
  }
}

函数作用:模拟拍摄过程,生成一个新的作品并保存到本地存储。

参数与返回值

  • 参数:无
  • 返回值:Promise<void>

执行流程分析:

doCapture
    │
    ├─► 防重复:isCapturing 检查
    │
    ├─► 设置 isCapturing = true
    │
    ├─► 显示"拍摄中"提示
    │
    ├─► 模拟拍摄耗时(sleep 800ms)
    │
    ├─► 构造 VideoWork 对象
    │    ├─ 生成作品ID
    │    ├─ 生成标题(基于时间)
    │    ├─ 随机选择缩略图颜色
    │    ├─ 随机生成视频时长
    │    ├─ 记录当前时间
    │    ├─ 记录 autoFramingUsed(自动构图状态)
    │    ├─ 记录 focusDistance(对焦距离)
    │    ├─ 记录 resolution(分辨率)
    │    └─ 记录 frameRate(帧率)
    │
    ├─► 保存作品到存储
    │    ├─ 成功:workCount + 1,显示成功提示
    │    └─ 失败:显示失败提示
    │
    └─► finally:isCapturing = false(无论成功失败都重置)

关键设计点:

  • 防重复点击:函数开头检查 isCapturing,如果正在拍摄中则直接返回。这是防止用户快速多次点击拍摄按钮导致重复生成作品的必要防护。

  • 元数据快照:拍摄时将当前的自动构图状态、对焦距离、分辨率、帧率等参数全部记录到作品中。这相当于给作品拍了一张"技术参数快照",用户后续可以在作品详情中查看当时的拍摄设置。

  • finally 块的使用isCapturing = false 放在 finally 块中,确保无论拍摄成功还是失败,状态都会被重置。如果只放在 try 的最后,一旦 catch 中有 return 或抛出新异常,状态就无法正确重置。

  • 模拟数据的真实性:虽然是模拟拍摄,但生成的数据具有一定的真实感——时长在5-25秒之间随机,缩略图从预设色板中选择,标题包含时间戳。这些细节让演示效果更接近真实应用。


七、UI布局与交互设计解析

拍摄首页的 UI 布局采用了经典的移动端拍摄应用布局结构:顶部功能栏 + 中间取景区 + 底部控制区。这种布局符合用户的使用习惯,操作效率高。

7.1 顶部功能栏

顶部栏包含标题和三个功能入口:预设、相册、设置。

Row() {
  Text('短视频拍摄')
    .fontSize(22)
    .fontWeight(FontWeight.Bold)
    .fontColor('#1A1A1A')
  Blank()
  // 对焦预设入口
  Text('预设')
    // ... 样式与点击事件
  // 相册入口
  Text(`相册 ${this.workCount}`)
    // ... 样式与点击事件
  // 设置入口
  Text('设置')
    // ... 样式与点击事件
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
.backgroundColor('#FFFFFF')

设计分析:

  • 标题左对齐:标题位于左侧,使用较大字号和粗体,清晰表明当前页面功能。

  • 功能入口右对齐:三个功能入口(预设、相册、设置)统一放在右侧,使用相同的胶囊样式,视觉上形成一组操作按钮。

  • 相册数量角标:相册入口显示作品数量 this.workCount,让用户不进入相册页也能知道有多少作品。这种"信息前置"的设计减少了用户的操作成本。

  • 统一的胶囊按钮样式:所有顶部入口都采用 F0F2F5 背景色 + 8px 圆角的胶囊样式,保持视觉一致性。浅灰色背景既不会过于抢眼,又能清晰传达"可点击"的视觉暗示。

7.2 取景模拟区

取景区是页面的核心视觉区域,采用 Stack 堆叠布局,依次叠加了网格线、状态提示、主体框和对焦框。

7.2.1 三分网格线
Column().width(1).height('100%').backgroundColor('#26FFFFFF').position({ x: '33.3%', y: 0 })
Column().width(1).height('100%').backgroundColor('#26FFFFFF').position({ x: '66.6%', y: 0 })
Row().height(1).width('100%').backgroundColor('#26FFFFFF').position({ x: 0, y: '33.3%' })
Row().height(1).width('100%').backgroundColor('#26FFFFFF').position({ x: 0, y: '66.6%' })

设计分析:

  • 三分构图辅助:网格线将画面水平和垂直各分为三等份,形成"三分法"构图辅助线。这是摄影中最经典的构图法则之一——将主体放在三分线的交点位置,画面更具美感。

  • 半透明白色:使用 #26FFFFFF(白色,透明度约15%)作为网格线颜色,既清晰可见,又不会过于干扰画面内容。

  • position 绝对定位:使用 position 进行绝对定位,精确控制每条线的位置。x: '33.3%'y: '33.3%' 确保了在不同尺寸的屏幕上,三分线的比例始终正确。

7.2.2 顶部状态提示
Text(this.autoFraming ? '影随人动 · 跟踪中' : '自动构图已关闭')
  .fontSize(11)
  .fontColor('#FFFFFF')
  .backgroundColor(this.autoFraming ? '#334C7DFF' : '#66000000')
  .borderRadius(8)
  .padding({ left: 8, right: 8, top: 3, bottom: 3 })
  .width('100%')
  .textAlign(TextAlign.Center)
  .position({ x: 0, y: 8 })

设计分析:

  • 状态可视化:用不同的文字和背景色区分自动构图的开启/关闭状态。开启时显示蓝色背景的"影随人动 · 跟踪中",关闭时显示半透明黑色背景的"自动构图已关闭"。

  • 颜色的语义化:蓝色(#4C7DFF)是应用的主题色,代表"功能激活";黑色代表"功能未激活"。用户通过颜色就能快速识别当前状态。

  • 顶部居中显示:状态条位于取景区顶部,居中显示,宽度占满。这个位置不会遮挡主要拍摄区域,同时又足够醒目。

7.2.3 模拟人物主体框
Column() {
  Circle({ width: 26, height: 26 }).fill('#DDDDDD')
  Column()
    .width(44)
    .height(52)
    .borderRadius(14)
    .backgroundColor('#DDDDDD')
    .margin({ top: 4 })
}
.width(96)
.height(132)
.justifyContent(FlexAlign.Center)
.border({ width: 2, color: this.autoFraming ? '#FFD54F' : '#88999999' })
.borderRadius(6)
.position({ x: this.subjectX, y: this.subjectY })

设计分析:

  • 极简人物造型:用圆形(头部)+ 圆角矩形(身体)组成抽象的人物形象。这种极简风格的好处是加载快、实现简单,同时足以传达"人物主体"的概念。

  • 边框颜色的状态指示:主体框的边框颜色随自动构图状态变化——开启时是金黄色(#FFD54F),关闭时是灰色。金黄色有"高亮、选中、跟踪中"的视觉暗示,与相机取景器中的对焦框颜色类似,用户容易理解。

  • position 动态定位position({ x: this.subjectX, y: this.subjectY }) 将主体框定位到由状态变量控制的坐标位置。当 subjectXsubjectY 变化时,主体框随之移动,配合 animateTo 动画就形成了"跟踪"效果。

7.2.4 点对焦框
if (this.showFocusBox) {
  Column() {
    Text('对焦')
      .fontSize(9)
      .fontColor('#FFFFFF')
      .backgroundColor('#664C7DFF')
      .borderRadius(6)
      .padding({ left: 4, right: 4, top: 1, bottom: 1 })
  }
  .width(56)
  .height(56)
  .justifyContent(FlexAlign.Center)
  .border({ width: 1.5, color: '#4C7DFF' })
  .borderRadius(4)
  .position({ x: this.focusBoxX - 28, y: this.focusBoxY - 28 })
}

设计分析:

  • 条件渲染:使用 if (this.showFocusBox) 条件渲染,对焦框只在用户点击后的 1.5 秒内显示。这种方式比"始终渲染但透明度为0"更节省渲染资源。

  • 居中定位技巧position({ x: this.focusBoxX - 28, y: this.focusBoxY - 28 }) 中减去 28 是因为对焦框宽高为 56,减去一半才能让框的中心对准点击位置。这是绝对定位中让元素居中的常用技巧。

  • 主题色统一:对焦框使用主题色 #4C7DFF 作为边框色,与整体视觉风格保持一致。

7.3 纵向对焦调节条

右侧的纵向对焦调节条是手动对焦功能的主要交互入口。

Column() {
  Text('对焦')
    .fontSize(11)
    .fontColor('#666666')
  Slider({
    value: this.focusDistance,
    min: AppConstants.FOCUS_DISTANCE_MIN,
    max: AppConstants.FOCUS_DISTANCE_MAX,
    step: AppConstants.FOCUS_DISTANCE_STEP,
    direction: Axis.Vertical,
    reverse: true
  })
    .layoutWeight(1)
    .width(24)
    .blockColor('#4C7DFF')
    .selectedColor('#4C7DFF')
    .trackColor('#E0E0E0')
    .onChange((value: number, mode: SliderChangeMode) => this.onFocusDistanceChange(value, mode))
  Text(`${this.focusDistance.toFixed(1)}m`)
    .fontSize(11)
    .fontColor('#4C7DFF')
    .fontWeight(FontWeight.Medium)
}
.width(56)

设计分析:

  • 垂直布局的选择:对焦调节条采用垂直方向(Axis.Vertical)而非水平方向。这是一个符合拍摄场景人体工学的设计——用户手持手机拍摄时,右手拇指在右侧垂直方向滑动更自然,也不容易遮挡取景画面。

  • reverse 反向reverse: true 使得滑块"向上滑动数值增大、向下滑动数值减小"。这与用户的直觉一致——“向上推 = 推远(对焦距离变大),向下拉 = 拉近(对焦距离变小)”。

  • 数值实时显示:滑块下方实时显示当前对焦距离数值,让用户在调节过程中随时知道精确的对焦距离。数值使用主题色高亮显示,增强视觉反馈。

  • "对焦"标签:顶部的"对焦"文字标签明确了这个控件的功能,避免用户困惑。

7.4 自动构图开关卡片

Row() {
  Column() {
    Text('影随人动自动构图')
      .fontSize(15)
      .fontColor('#333333')
      .fontWeight(FontWeight.Medium)
    Text('开启后取景框自动跟随主体,拍摄更省心')
      .fontSize(11)
      .fontColor('#999999')
      .margin({ top: 4 })
  }
  .alignItems(HorizontalAlign.Start)
  .layoutWeight(1)

  Toggle({ type: ToggleType.Switch, isOn: this.autoFraming })
    .selectedColor('#4C7DFF')
    .onChange((on: boolean) => this.onToggleAutoFraming(on))
}

设计分析:

  • 卡片式布局:开关组件放在白色卡片中,与周围的浅灰色背景形成层次区分。卡片式设计是移动端应用的常见模式,视觉上清晰利落。

  • 标题 + 描述的双层结构:主标题"影随人动自动构图"说明功能名称,副标题"开启后取景框自动跟随主体,拍摄更省心"进一步解释功能价值。这种"功能名 + 利益点"的文案结构能有效帮助用户理解功能。

  • 开关在右侧:Toggle 开关位于卡片右侧,符合移动端设置项的标准布局(左标签右控件)。用户的视线从左到右移动,先读到功能说明,再看到开关控件,认知流顺畅。

7.5 底部控制区

底部控制区左侧显示对焦信息,右侧是拍摄按钮。

Row() {
  // 对焦信息
  Column() {
    Text(`对焦距离 ${this.focusDistance.toFixed(1)}m`)
      .fontSize(13)
      .fontColor('#333333')
    Text(this.setting.focusMode === 'manual' ? '手动对焦模式' : '自动对焦模式')
      .fontSize(11)
      .fontColor(this.setting.focusMode === 'manual' ? '#FF9800' : '#4CAF50')
      .margin({ top: 4 })
    Text(`${this.setting.resolution} · ${this.setting.frameRate}fps`)
      .fontSize(11)
      .fontColor('#999999')
      .margin({ top: 4 })
  }

  // 拍摄按钮
  Stack() {
    Circle({ width: 72, height: 72 })
      .fill('#FFFFFF')
      .stroke('#4C7DFF')
      .strokeWidth(4)
    Circle({ width: 54, height: 54 })
      .fill(this.isCapturing ? '#F44336' : '#4C7DFF')
  }
  .margin({ right: 16 })
  .onClick(() => this.doCapture())
}

设计分析:

  • 对焦信息三行展示:左侧对焦信息区域用三行文字展示关键参数:

    • 第一行:对焦距离(最核心的参数,字号最大)
    • 第二行:对焦模式(自动/手动,用颜色区分状态)
    • 第三行:分辨率和帧率(次要信息,灰色小字)

    这种信息层级设计符合"重要信息优先"的原则——用户最先看到最关心的内容。

  • 对焦模式的颜色语义:自动对焦模式用绿色(#4CAF50),表示"系统自动处理,放心使用";手动对焦模式用橙色(#FF9800),表示"需要用户手动操作,请注意"。颜色的语义化设计让用户一眼就能识别当前模式。

  • 双层圆形拍摄按钮:拍摄按钮采用经典的"外圈+内圈"双层圆形设计,外圈是白色填充+蓝色描边,内圈是实心蓝色。这种设计模仿了真实相机的快门按钮,用户一看就知道"这是拍照按钮"。

  • 拍摄中的状态变化:拍摄中内圈变成红色(#F44336),从蓝色变为红色是一种强烈的状态变化信号,清晰地传达"正在拍摄中"的状态。红色也与录像时的"REC"指示灯颜色呼应,符合用户的心理模型。


八、其他页面功能解析

8.1 相册页 - 作品列表展示

相册页展示所有已拍摄的作品,每个作品卡片显示缩略图、标题、时长、构图状态、对焦距离、分辨率、帧率和拍摄时间。

8.1.1 WorkCard 作品卡片组件
@Builder
WorkCard(w: VideoWork): void {
  Row() {
    // 缩略占位色块
    Column() {
      Text('▶')
        .fontSize(18)
        .fontColor('#FFFFFF')
    }
    .width(88)
    .height(66)
    .borderRadius(8)
    .backgroundColor(w.thumbnail || '#4C7DFF')
    .justifyContent(FlexAlign.Center)

    // 作品信息
    Column() {
      Text(w.title || '未命名作品')
        // ... 标题样式
      Row() {
        Text(`时长 ${w.duration}`)
        Text(w.autoFramingUsed ? '影随人动' : '普通构图')
          .fontColor(w.autoFramingUsed ? '#4C7DFF' : '#9E9E9E')
        Text(`对焦 ${w.focusDistance.toFixed(1)}m`)
      }
      Row() {
        Text(this.formatTime(w.createTime))
        Blank()
        Text(`${w.resolution} · ${w.frameRate}fps`)
      }
    }
    .layoutWeight(1)
  }
}

设计分析:

  • 左右结构:卡片采用左缩略图、右文字信息的经典列表布局。缩略图提供视觉识别,文字提供详细信息。

  • 播放图标暗示:缩略图中央的 ▶ 播放图标暗示这是一个视频作品,而非普通图片。这个小图标虽小,却能有效传达内容类型。

  • 构图状态的高亮显示:如果作品使用了影随人动自动构图,“影随人动"标签用主题色蓝色高亮;否则用灰色显示"普通构图”。这种设计让自动构图功能在作品列表中也有存在感,强化了功能价值的感知。

  • 信息密度控制:作品卡片在有限的空间内展示了标题、时长、构图状态、对焦距离、拍摄时间、分辨率、帧率共7项信息。信息虽然多,但通过字号(14px标题、11px次要信息)、颜色(深灰标题、浅灰次要)、布局(上下两行)的层次设计,并不显得拥挤。

8.2 相机设置页 - 参数配置

相机设置页提供了默认自动构图开关、对焦模式选择、分辨率选择、帧率选择四项配置。

8.2.1 saveConfig - 保存配置
private async saveConfig(): Promise<void> {
  const cfg: CameraSetting = {
    defaultAutoFraming: this.defaultAutoFraming,
    focusMode: this.focusMode,
    resolution: AppConstants.RESOLUTION_OPTIONS[this.resolutionIndex],
    frameRate: AppConstants.FRAME_RATE_OPTIONS[this.frameRateIndex]
  }
  try {
    await StorageManager.saveCameraSetting(cfg)
    promptAction.showToast({ message: '相机配置已保存', duration: 1000 })
    router.back({ url: AppConstants.ROUTE_INDEX, params: { settingConfig: cfg } })
  } catch (e) {
    promptAction.showToast({ message: '保存失败' })
  }
}

函数作用:将当前页面的配置状态组装成 CameraSetting 对象,保存到存储后返回拍摄首页。

参数与返回值

  • 参数:无
  • 返回值:Promise<void>

实现细节分析:

  • 索引到值的转换:页面状态中 resolutionIndexframeRateIndex 存储的是数组索引(用于 Select 组件),保存时需要从常量数组中取出对应的实际值。这是 UI 状态和业务数据之间的典型转换。

  • 保存后返回:保存成功后调用 router.back() 返回上一页(拍摄首页),并通过 params 将新的配置对象传递回去。虽然拍摄首页在 onPageShow 中会重新从存储读取配置,但通过路由参数传递可以让首页更快地更新状态(不需要等异步读取完成)。

  • 失败提示:保存失败时显示错误提示,且不执行页面跳转,让用户可以重试或检查配置。

8.3 对焦预设页 - 预设管理

对焦预设页是手动对焦能力的场景化延伸,用户可以管理和应用各种对焦预设。

8.3.1 applyPreset - 应用预设
private async applyPreset(preset: FocusPreset): Promise<void> {
  try {
    await StorageManager.saveCurrentFocusDistance(preset.focusDistance)
    promptAction.showToast({
      message: `已应用「${preset.presetName}」,对焦 ${preset.focusDistance.toFixed(1)}m`,
      duration: 1200
    })
    router.back({ url: AppConstants.ROUTE_INDEX, params: { appliedPreset: preset } })
  } catch (e) {
    promptAction.showToast({ message: '应用预设失败' })
  }
}

函数作用:将选中的预设对焦距离写入全局当前对焦距离,然后返回拍摄首页。

参数与返回值

  • 参数:preset: FocusPreset - 要应用的预设对象
  • 返回值:Promise<void>

核心特性映射:

真实调用方式:await cameraManager.setFocusDistance(preset.focusDistance)

本函数中的 saveCurrentFocusDistance(preset.focusDistance) 即对应
setFocusDistance 接口调用的持久化记录。

设计分析:

  • 应用即返回:点击"应用到拍摄页"后立即返回拍摄首页,符合用户的操作预期——用户应用预设就是为了去拍摄,不需要停留在预设页。

  • Toast 反馈内容:Toast 消息同时包含预设名称和对焦距离数值,既确认了应用的是哪个预设,又告知了具体的对焦参数,信息完整。

  • 全局状态的桥梁作用saveCurrentFocusDistance 将对焦距离写入全局存储,拍摄首页在 onPageShow 时读取。这种通过全局存储传递状态的方式,解耦了预设页和拍摄页——两者不需要直接引用,只需要约定存储键名即可。

8.4 作品编辑页 - 元数据展示

作品编辑页允许用户修改作品标题,同时展示完整的拍摄元数据。

8.4.1 元数据展示区
Column() {
  Text('拍摄元数据')
    .fontSize(14)
    .fontWeight(FontWeight.Medium)
    .fontColor('#333333')
    .width('100%')

  this.MetaRow('作品时长', `${this.work.duration}`)
  this.MetaRow('构图状态', this.work.autoFramingUsed ? '影随人动自动构图' : '普通构图')
  this.MetaRow('对焦距离', `${this.work.focusDistance.toFixed(1)} m`)
  this.MetaRow('分辨率', this.work.resolution)
  this.MetaRow('帧率', `${this.work.frameRate} fps`)
  this.MetaRow('拍摄时间', this.formatTime(this.work.createTime))
}

设计分析:

  • 元数据的价值:将拍摄时的技术参数(构图状态、对焦距离、分辨率、帧率)完整展示给用户,不仅满足了"回顾拍摄设置"的功能需求,还具有教育意义——用户可以通过对比不同作品的参数和效果,逐渐理解各项参数对成片质量的影响。

  • 构图状态的特殊高亮:在 MetaRow 中,构图状态为"影随人动自动构图"时用主题色蓝色显示,其他元数据用深灰色。这种差异化处理突出了核心特性(影随人动)的存在感。

  • 只读设计:元数据区域是只读的,用户只能查看不能修改。这是合理的设计——拍摄参数是作品的固有属性,一旦拍摄完成就不应该再改动,否则元数据就失去了真实性。


九、核心特性技术对比

为了更直观地理解 HarmonyOS 6.1.1 Camera Kit 两大新特性带来的变化,下面从多个维度进行对比分析。

9.1 自动构图开启 vs 关闭对比

对比维度自动构图开启(AUTO_FRAMING = true)自动构图关闭(AUTO_FRAMING = false)
技术原理相机底层实时检测人体主体,动态调整取景范围固定取景范围,无主体检测与跟踪
调用接口cameraManager.setAutoFraming(true)cameraManager.setAutoFraming(false)
算力消耗较高,需要持续运行人体检测算法较低,仅需常规视频采集
适用场景运动跟拍、Vlog自拍、儿童记录等主体移动场景固定机位拍摄、风景拍摄、主体静止场景
用户操作无需手动调整机位,主体始终保持在画面中需要用户手动构图,主体移动时需跟随调整
画面稳定性取景框平滑移动,过渡自然固定不动,画面最稳定
创作自由度降低构图门槛,新手也能拍出稳定画面需要用户具备构图知识,但创意空间更大
电池影响额外的 AI 计算增加电量消耗无额外消耗
典型产品形态家庭记录仪、儿童陪伴相机、运动相机专业拍摄、固定监控、风景摄影

9.2 自动对焦 vs 手动对焦对比

对比维度自动对焦(FocusMode.AUTO)手动对焦(FocusMode.MANUAL)
核心接口系统自动控制,点对焦触发自动选焦getFocusDistance() / setFocusDistance(value)
对焦依据反差检测 / 相位检测,系统自动判断用户根据拍摄意图手动指定距离
操作便捷性高,半按快门或点击即可对焦低,需要用户精确调节对焦距离
对焦精度取决于算法精度,通常能满足日常需求取决于用户操作,可实现精确的创意对焦
适用场景日常抓拍、家庭记录、运动拍摄人像特写、微距摄影、视频创意拍摄
学习成本几乎为零,人人都会用需要理解对焦距离、景深等概念
创作可能性基础拍摄需求,创意空间有限可实现前景虚化、焦平面切换等高级效果
响应速度需要对焦过程(通常 0.1-0.5 秒)预设参数下可瞬时到位,无需等待对焦
低光表现低光环境下对焦可能变慢或失败不受光线影响,手动指定即可
配合预设作用有限(自动对焦本身就是自动的)价值极大,可预设多种场景的对焦距离

9.3 四种组合模式的适用场景

将自动构图和对焦模式两两组合,可以得到四种使用模式,各有其适用场景:

模式自动构图对焦模式适用场景典型用户
智能全自动模式开启自动对焦家庭日常记录、儿童跟拍、老人使用普通家庭用户
智能手动对焦模式开启手动对焦户外跟拍 + 预设对焦、Vlog创作进阶创作者
固定自动对焦模式关闭自动对焦固定机位访谈、风景拍摄通用场景
专业全手动模式关闭手动对焦人像特写、创意短片、微距拍摄专业创作者

十、技术总结与行业展望

10.1 技术价值总结

HarmonyOS 6.1.1 Camera Kit 的两大新特性,标志着移动影像能力从"系统黑盒"向"开发者可控"的重要转变。

影随人动自动构图(AUTO_FRAMING)的技术价值:

  • 算法能力下放:将原本需要专业团队研发的人体检测、跟踪、构图算法作为系统能力开放,大幅降低了智能拍摄应用的开发门槛。中小团队无需组建 AI 算法团队,也能为用户提供专业级的智能拍摄体验。

  • 系统级性能优化:作为系统级能力,自动构图可以与相机 ISP(图像信号处理)流水线深度整合,获得比应用层实现更好的性能和更低的功耗。

  • 一致性体验:所有使用 Camera Kit 的应用都可以提供一致的自动构图体验,用户不需要在不同应用之间重新适应。

对焦距离检测与设置的技术价值:

  • 控制权的开放getFocusDistancesetFocusDistance 接口的开放,意味着开发者首次获得了对焦系统的精确控制权。在此之前,开发者只能间接触发对焦,无法知道也无法控制具体的对焦距离。

  • 创作维度的扩展:手动对焦能力为移动影像创作打开了新的维度——焦平面控制。创作者可以精确控制画面中哪个平面清晰、哪个平面虚化,实现更丰富的视觉表达。

  • 场景化能力的基础:对焦预设、场景模式等高级功能都建立在手动对焦能力之上。没有精确的对焦距离控制,这些场景化功能就无从谈起。

10.2 行业应用前景

Camera Kit 新特性的开放,将对多个行业产生深远影响:

短视频创作行业:

  • 创作民主化:自动构图降低了拍摄门槛,让更多人能够拍出稳定、专业的视频内容,进一步推动 UGC(用户生成内容)的繁荣。
  • 专业能力下沉:手动对焦等专业功能进入移动端,让移动设备不再只是"记录工具",也可以成为"创作工具"。
  • AI 与创作的融合:自动构图是 AI 辅助创作的典型案例,未来还会有更多 AI 能力(如智能曝光、智能调色、智能剪辑)与拍摄过程深度融合。

家庭影像行业:

  • 儿童成长记录:影随人动功能特别适合儿童拍摄场景,家长不需要时刻调整机位,相机自动跟踪孩子的活动,捕捉每一个精彩瞬间。
  • 老人友好设计:自动化程度越高,老年用户的使用门槛越低。自动构图 + 自动对焦的"双自动"组合,让老人也能轻松记录家庭生活。

教育与培训行业:

  • 课程录制:自动构图可以让讲师在教室内自由走动,摄像头自动跟踪,无需专人操作摄像机。
  • 实验记录:手动对焦预设可以针对不同的实验场景(如近距离拍摄实验器材)预设对焦参数,一键切换。

安防与监控行业:

  • 智能追踪:自动构图技术可以应用于安防监控,自动跟踪画面中的异动目标,提供更清晰的画面。
  • 重点区域对焦:手动对焦能力可以针对特定区域(如出入口、收银台)精确设置对焦距离,确保关键区域成像清晰。

10.3 开发建议

基于对 HarmonyOS 6.1.1 Camera Kit 新特性的实践,以下是一些开发建议:

1. 渐进式采用新特性

不要试图一次性应用所有新特性。建议先从自动构图开始——它对用户体验的提升最直接,开发成本也最低(只需一个开关)。待用户熟悉后,再逐步引入手动对焦、对焦预设等更专业的功能。

2. 做好能力检测与降级兼容

不是所有设备都支持新特性。在调用新接口前,务必通过 CameraFormat 中的能力标记(如 AUTO_FRAMING)检测设备是否支持。对于不支持的设备,提供降级方案或隐藏相关功能入口,避免功能不可用造成用户困惑。

3. 预设模式降低使用门槛

手动对焦虽然功能强大,但普通用户可能不知道如何设置。提供预设模式(如人像、风景、微距)是降低使用门槛的有效手段。用场景化的语言包装技术参数,让用户根据场景选择,而不是根据参数选择。

4. 关注性能与功耗

自动构图等 AI 功能会增加设备的算力消耗和电量消耗。在产品设计时要注意:

  • 提供开关让用户自主选择是否开启
  • 在长时间拍摄场景中注意功耗控制
  • 合理设置检测频率和动画效果,避免过度消耗资源

5. 元数据记录与利用

拍摄时记录下完整的技术参数(构图状态、对焦距离、分辨率、帧率等),这些元数据不仅可以用于作品展示,还可以:

  • 用于智能推荐(分析用户偏好的拍摄参数)
  • 用于快速复刻(“用上次的设置再拍一条”)
  • 用于数据分析(哪些参数组合产出的作品更受欢迎)

6. 注重用户教育

新功能的价值需要用户感知到才能发挥作用。在产品中巧妙地融入用户教育:

  • 预设的场景说明让用户理解不同参数的适用场景
  • Toast 提示和状态标签让用户知道功能正在生效
  • 作品元数据让用户看到功能对成片的影响

10.4 结语

HarmonyOS 6.1.1 Camera Kit 的影随人动自动构图和对焦距离检测与设置能力,为移动影像应用打开了新的想象空间。从系统能力到产品功能,从技术参数到用户体验,中间需要经过精心的设计和打磨。

本文通过一个家庭短视频拍摄 App 的完整实现,从数据模型、存储管理到页面逻辑,逐层解析了如何将 Camera Kit 新特性落地到实际产品中。希望这些代码分析和设计思考能够为正在进行 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、测试、元服务和应用上架分发等。

更多推荐