在鸿蒙(HarmonyOS Next)开发中,如果系统自带的相机界面无法满足需求(例如需要自定义取景框、添加实时滤镜、或做证件扫描),我们就需要使用 Camera Kit 来开发自定义相机。

本文将带你从零实现一个具备实时预览拍照功能的自定义相机页面。


1. 核心流程概览

开发自定义相机主要分为 5 个步骤:

  1. 权限申请:相机和麦克风权限。
  2. 创建会话 (Session):管理相机的输入流和输出流。
  3. 配置输入 (Input):选择相机设备(前置/后置)。
  4. 配置输出 (Output)
    • PreviewOutput: 用于在屏幕上显示实时画面(绑定 XComponent)。
    • PhotoOutput: 用于捕获静态照片。
  1. 启动与控制:开启会话,处理拍照动作。

2. 权限配置

2.1 module.json5

entry/src/main/module.json5 中添加权限:

"requestPermissions": [
  {
    "name": "ohos.permission.CAMERA",
    "reason": "$string:camera_reason",
    "usedScene": { "abilities": ["EntryAbility"], "when": "always" }
  },
  {
    "name": "ohos.permission.MICROPHONE",
    "reason": "$string:mic_reason",
    "usedScene": { "abilities": ["EntryAbility"], "when": "always" }
  },
  {
    "name": "ohos.permission.WRITE_MEDIA", // 保存图片需要
    "reason": "$string:media_reason",
    "usedScene": { "abilities": ["EntryAbility"], "when": "always" }
  }
]

2.2 动态申请

在页面加载时(aboutToAppear),记得使用 abilityAccessCtrl 弹窗申请权限。


3. UI 布局:XComponent

鸿蒙相机预览必须使用 XComponent 组件,因为它能提供底层的 Surface 给 Camera Kit 渲染画面。

// CameraPage.ets
@Entry
@Component
struct CameraPage {
  private mXComponentController: XComponentController = new XComponentController();
  private surfaceId: string = '';

  build() {
    Stack() {
      // 1. 预览区域
      XComponent({
        id: 'cameraPreview',
        type: XComponentType.SURFACE,
        controller: this.mXComponentController
      })
        .onLoad(() => {
          // 获取 surfaceId,这是连接相机和 UI 的关键
          this.mXComponentController.setXComponentSurfaceSize({ surfaceWidth: 1080, surfaceHeight: 1920 });
          this.surfaceId = this.mXComponentController.getXComponentSurfaceId();
          // 初始化相机
          this.initCamera();
        })
        .width('100%')
        .height('100%')

      // 2. 拍照按钮
      Button()
        .width(80)
        .height(80)
        .backgroundColor(Color.White)
        .borderRadius(40)
        .margin({ bottom: 50 })
        .onClick(() => {
          this.takePhoto();
        })
    }
    .width('100%')
    .height('100%')
    .alignContent(Alignment.Bottom)
    .backgroundColor(Color.Black)
  }
}

4. 核心逻辑:Camera Kit

创建一个 CameraService.ts 来封装复杂的相机逻辑。

import { camera } from '@kit.CameraKit';
import { image } from '@kit.ImageKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { fileIo } from '@kit.CoreFileKit';

export class CameraService {
  private cameraManager: camera.CameraManager | undefined = undefined;
  private cameraInput: camera.CameraInput | undefined = undefined;
  private previewOutput: camera.PreviewOutput | undefined = undefined;
  private photoOutput: camera.PhotoOutput | undefined = undefined;
  private session: camera.PhotoSession | undefined = undefined;

  // 初始化相机
  async initCamera(surfaceId: string) {
    // 1. 获取 CameraManager
    this.cameraManager = camera.getCameraManager(getContext(this));
    
    // 2. 获取支持的相机设备列表(0通常是后置,1是前置)
    const cameras = this.cameraManager.getSupportedCameras();
    const backCamera = cameras[0]; // 简化逻辑,默认取第一个

    if (!backCamera) return;

    // 3. 创建 Input
    this.cameraInput = this.cameraManager.createCameraInput(backCamera);
    await this.cameraInput.open();

    // 4. 获取支持的输出配置 (Profile)
    const profiles = backCamera.capability;
    const previewProfile = profiles.previewProfiles[0]; // 预览分辨率
    const photoProfile = profiles.photoProfiles[0];     // 拍照分辨率

    // 5. 创建 Output
    // 预览流 -> 绑定到 UI 的 surfaceId
    this.previewOutput = this.cameraManager.createPreviewOutput(previewProfile, surfaceId);
    // 拍照流 -> 这里的 surfaceId 暂时传 null,后续在回调里拿 buffer
    this.photoOutput = this.cameraManager.createPhotoOutput(photoProfile);

    // 6. 创建 Session 并开始配置
    this.session = this.cameraManager.createSession(camera.SceneMode.NORMAL_PHOTO) as camera.PhotoSession;
    this.session.beginConfig();

    // 7. 添加输入输出
    this.session.addInput(this.cameraInput);
    this.session.addOutput(this.previewOutput);
    this.session.addOutput(this.photoOutput);

    // 8. 提交配置并启动
    await this.session.commitConfig();
    await this.session.start();
  }

  // 拍照
  async takePhoto() {
    if (!this.photoOutput) return;

    // 配置拍照参数
    const photoCaptureSetting: camera.PhotoCaptureSetting = {
      quality: camera.QualityLevel.QUALITY_LEVEL_HIGH,
      rotation: camera.ImageRotation.ROTATION_0
    };

    // 触发拍照
    await this.photoOutput.capture(photoCaptureSetting);
  }

  // 释放资源(页面销毁时调用)
  async release() {
    await this.session?.stop();
    await this.session?.release();
    await this.cameraInput?.close();
  }
}

5. 处理拍照结果 (保存图片)

上面的 takePhoto 只是触发了动作,要拿到图片数据,需要监听 photoOutput 的回调。

修改 initCamera 方法,在创建 photoOutput 后添加监听:

// 在 initCamera 中
this.photoOutput.on('photoAvailable', (err, photo: camera.Photo) => {
   if (err) return;
   // photo.mainImage 就是图片数据
   let imageObj = photo.mainImage;
   
   // 获取 ByteBuffer
   imageObj.getComponent(image.ComponentType.JPEG).then((component) => {
      const buffer = component.byteBuffer;
      // TODO: 使用 fileIo 将 buffer 写入沙箱文件
      this.savePicture(buffer);
      // 记得释放 image
      imageObj.release(); 
   });
});

6. 常见坑点总结

  1. SurfaceId 为空:必须在 XComponent.onLoad 回调中获取 surfaceId,不能在 aboutToAppear 里拿,否则相机没地方渲染,会黑屏。
  2. 分辨率匹配previewProfilephotoProfile 必须从 camera.capability 中选取,不能随意捏造宽高,否则会报错。
  3. 生命周期:页面跳转或后台时,务必调用 session.stop()release(),否则下次回来相机可能被占用无法启动。
  4. 方向问题:拍照出来的照片可能会旋转 90 度,需要根据设备的 orientation 设置 capture 参数中的 rotation

通过以上步骤,你就能在鸿蒙上实现一个功能完备的自定义相机了。

Logo

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

更多推荐