鸿蒙APP开发:快来看摄日APP的相机专业模式:Camera API与参数控制

6 阅读5分钟

摄日的相机专业模式:Camera API与参数控制

如果你是摄影爱好者,推荐去鸿蒙应用市场搜一下**「摄日」**,下载体验体验。每日摄影挑战、完整参数记录、构图技法参考,一套走下来对摄影技巧的提升会有很大帮助。体验完再回来看这篇文章,你会更清楚相机专业模式和参数控制背后是怎么实现的。


写在前面

大家好,我是一名写了十多年Web前端的老兵。从jQuery时代一路走到React/Vue,CSS3动画、requestAnimationFrame、Web Animation API这些都算是看家本领。去年开始转战鸿蒙生态,用ArkTS开发App,这一路踩了不少坑,也积累了不少心得。

很多人觉得"前端转鸿蒙"应该很容易——都是写UI嘛,组件化、状态管理、生命周期,概念都差不多。但真正上手之后你会发现,相似的地方让你觉得亲切,不同的地方让你抓狂

比如:

  • 相机控制:Web里用getUserMedia,鸿蒙里用@ohos.camera,API体系完全不同。
  • 参数控制:Web里相机参数有限,鸿蒙里可以控制光圈、快门、ISO、白平衡等专业参数。
  • 数据存储:Web的localStorage到了ArkTS变成了@ohos.data.preferences

接下来这篇文章,我会用"摄日"的实际开发经历,带你看看鸿蒙的相机专业模式怎么实现——从相机参数控制到拍摄模式选择。


这篇文章聊什么

摄日这个App,核心要解决两个问题:

  1. 相机专业模式:控制光圈、快门、ISO、白平衡等参数
  2. 拍摄记录:记录每次拍摄的完整参数

对应到HarmonyOS的API,主要涉及:

  • @ohos.camera — 相机专业模式控制
  • @ohos.data.preferences — 拍摄记录存储

第一步:拍摄模式定义

// 拍摄模式
const SHOOTING_MODES = [
  { id: 'auto', name: '自动', desc: '全自动拍摄' },
  { id: 'aperture', name: '光圈优先', desc: '控制景深' },
  { id: 'shutter', name: '快门优先', desc: '控制运动模糊' },
  { id: 'manual', name: '手动', desc: '完全手动控制' },
  { id: 'bulb', name: 'B门', desc: '长时间曝光' }
];

// 白平衡预设
const WHITE_BALANCE = [
  { id: 'auto', name: '自动' },
  { id: 'daylight', name: '日光', temp: 5500 },
  { id: 'cloudy', name: '阴天', temp: 6500 },
  { id: 'shade', name: '阴影', temp: 7500 },
  { id: 'tungsten', name: '钨丝灯', temp: 3200 },
  { id: 'fluorescent', name: '荧光灯', temp: 4000 },
  { id: 'flash', name: '闪光灯', temp: 5500 }
];

// 拍摄参数
interface ShootingParams {
  aperture: string;
  shutterSpeed: string;
  iso: number;
  whiteBalance: string;
  exposureCompensation: number; // 曝光补偿(EV)
  focusMode: string;            // auto/manual
  meteringMode: string;         // matrix/center/spot
}

第二步:相机参数控制

鸿蒙相机API支持的参数控制:

import { camera } from '@kit.CameraKit';

// 设置相机参数
async function setCameraParams(
  captureSession: camera.CaptureSession,
  params: ShootingParams
) {
  // 设置曝光模式
  const exposureMode = params.shutterSpeed === 'auto'
    ? camera.ExposureMode.EXPOSURE_MODE_AUTO
    : camera.ExposureMode.EXPOSURE_MODE_MANUAL;

  // 设置ISO
  if (params.iso > 0) {
    // 通过PhotoCaptureSetting设置
  }

  // 设置白平衡
  const wbMode = params.whiteBalance === 'auto'
    ? camera.WhiteBalanceMode.AWB_MODE_AUTO
    : camera.WhiteBalanceMode.AWB_MODE_DAYLIGHT;
}

第三步:拍摄记录页面

@Entry
@Component
struct ShotRecordPage {
  @State mode: string = 'auto'
  @State aperture: string = 'f/5.6'
  @State shutterSpeed: string = '1/125'
  @State iso: number = 400
  @State whiteBalance: string = 'auto'
  @State evCompensation: number = 0
  @State notes: string = ''

  private apertures = ['f/1.4', 'f/2', 'f/2.8', 'f/4', 'f/5.6', 'f/8', 'f/11', 'f/16']
  private shutterSpeeds = ['30"', '15"', '8"', '4"', '2"', '1"', '1/2', '1/4',
    '1/8', '1/15', '1/30', '1/60', '1/125', '1/250', '1/500', '1/1000']

  build() {
    Column() {
      // 拍摄模式选择
      Flex({ wrap: FlexWrap.Wrap }) {
        ForEach(SHOOTING_MODES, (m) => {
          Text(m.name)
            .fontSize(13)
            .padding(8)
            .margin(4)
            .borderRadius(8)
            .backgroundColor(this.mode === m.id ? '#10B981' : '#374151')
            .fontColor(this.mode === m.id ? '#FFF' : '#D1D5DB')
            .onClick(() => { this.mode = m.id })
        })
      }
      .margin({ bottom: 16 })

      // 光圈选择
      Text('光圈')
        .fontSize(14)
        .fontColor('#9CA3AF')
        .margin({ bottom: 8 })
      Flex({ wrap: FlexWrap.Wrap }) {
        ForEach(this.apertures, (ap: string) => {
          Text(ap)
            .fontSize(13)
            .padding(8)
            .margin(4)
            .borderRadius(8)
            .backgroundColor(this.aperture === ap ? '#F59E0B' : '#374151')
            .fontColor(this.aperture === ap ? '#FFF' : '#D1D5DB')
            .onClick(() => { this.aperture = ap })
        })
      }
      .margin({ bottom: 16 })

      // 快门速度选择
      Text('快门速度')
        .fontSize(14)
        .fontColor('#9CA3AF')
        .margin({ bottom: 8 })
      Flex({ wrap: FlexWrap.Wrap }) {
        ForEach(this.shutterSpeeds, (speed: string) => {
          Text(speed)
            .fontSize(13)
            .padding(8)
            .margin(4)
            .borderRadius(8)
            .backgroundColor(this.shutterSpeed === speed ? '#3B82F6' : '#374151')
            .fontColor(this.shutterSpeed === speed ? '#FFF' : '#D1D5DB')
            .onClick(() => { this.shutterSpeed = speed })
        })
      }
      .margin({ bottom: 16 })

      // ISO选择
      Row() {
        Text('ISO')
          .fontSize(14)
          .fontColor('#9CA3AF')
        Slider({ value: this.iso, min: 50, max: 6400, step: 1 })
          .onChange((v: number) => {
            const commonISOs = [50, 100, 200, 400, 800, 1600, 3200, 6400]
            this.iso = commonISOs.reduce((prev, curr) =>
              Math.abs(curr - v) < Math.abs(prev - v) ? curr : prev
            )
          })
          .layoutWeight(1)
        Text(`${this.iso}`)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .width(50)
          .textAlign(TextAlign.Center)
      }
      .width('100%')
      .margin({ bottom: 16 })

      // 曝光补偿
      Row() {
        Text('曝光补偿')
          .fontSize(14)
          .fontColor('#9CA3AF')
        Slider({ value: this.evCompensation, min: -3, max: 3, step: 0.3 })
          .onChange((v: number) => { this.evCompensation = Math.round(v * 10) / 10 })
          .layoutWeight(1)
        Text(`${this.evCompensation > 0 ? '+' : ''}${this.evCompensation}EV`)
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
          .width(60)
      }
      .width('100%')
      .margin({ bottom: 16 })

      Button('保存拍摄记录')
        .onClick(() => this.saveRecord())
        .width('100%')
        .backgroundColor('#10B981')
    }
    .width('100%')
    .height('100%')
    .padding(16)
    .backgroundColor('#111827')
  }

  async saveRecord() {
    const record = {
      id: `shot_${Date.now()}`,
      mode: this.mode,
      aperture: this.aperture,
      shutterSpeed: this.shutterSpeed,
      iso: this.iso,
      whiteBalance: this.whiteBalance,
      evCompensation: this.evCompensation,
      notes: this.notes,
      timestamp: Date.now()
    };

    const store = await preferences.getPreferences(getContext(), 'sheri_data');
    let records = JSON.parse(await store.get('shots', '[]') as string);
    records.push(record);
    await store.set('shots', JSON.stringify(records));
    await store.flush();
  }
}

第四步:12种构图技法

const COMPOSITION_TECHNIQUES = [
  { id: 'rule_of_thirds', name: '三分法', desc: '将画面分成3x3网格,主体放在交叉点' },
  { id: 'center', name: '居中构图', desc: '主体放在画面正中央' },
  { id: 'symmetry', name: '对称构图', desc: '利用对称元素创造平衡感' },
  { id: 'leading_lines', name: '引导线', desc: '用线条引导视线到主体' },
  { id: 'frame', name: '框架构图', desc: '用前景元素框住主体' },
  { id: 'diagonal', name: '对角线', desc: '利用对角线创造动感' },
  { id: 'triangle', name: '三角形', desc: '三角形构图创造稳定感' },
  { id: 'golden_ratio', name: '黄金比例', desc: '1.618:1的完美比例' },
  { id: 'negative_space', name: '留白', desc: '大量空白突出主体' },
  { id: 'fill_frame', name: '填满画面', desc: '主体占满整个画面' },
  { id: 'pattern', name: '重复图案', desc: '利用重复元素创造节奏' },
  { id: 'break_pattern', name: '打破规律', desc: '在重复中加入变化' }
];

总结

这篇文章围绕"摄日"的相机专业模式,讲解了三个核心主题:

  1. 拍摄模式:自动、光圈优先、快门优先、手动、B门五种模式
  2. 参数控制:光圈、快门、ISO、白平衡、曝光补偿的UI选择器
  3. 构图技法:12种常用构图方法的参考

相机专业模式的核心是参数之间的关系——光圈控制景深,快门控制运动模糊,ISO控制噪点。理解这个"曝光三角",就能拍出更好的照片。


如果你也是摄影爱好者,希望这篇文章能帮你理解摄日背后的相机控制逻辑。去鸿蒙应用市场下载体验一下吧,有问题欢迎交流。