《HarmonyOS技术精讲-Camera Kit(相机服务)第7篇:闪光灯控制技术》

在这里插入图片描述

闪光灯控制,没那么简单

HarmonyOS NEXT 相机的闪光灯控制,官方文档写得很清楚:通过 setFlashMode 接口设置 FlashMode 枚举值。但实际开发中,很多人第一次实现这个功能时,会遇到几个很隐蔽的问题。

比如,在切换闪光灯模式后,照片的曝光效果并没有按照预期改变;又比如,相机启动时默认的闪光灯状态和UI显示不一致。这些问题不是API本身的问题,而是状态同步和生命周期管理的细节没有处理好。

这篇文章会从最基础的 FlashMode 枚举讲起,直接给出一个完整的闪光灯模式切换组件,然后重点分析开发中容易踩的几个坑。代码改完就能用,不在理论上纠结。

闪光灯模式到底控制什么

FlashMode 枚举定义了四种模式:

枚举值 说明 典型场景
FLASH_MODE_CLOSE 关闭闪光灯 光线充足环境、静物拍摄
FLASH_MODE_OPEN 强制开启闪光灯 暗光环境、补光需求
FLASH_MODE_AUTO 自动模式,相机根据环境光线判断 日常随手拍
FLASH_MODE_ALWAYS_OPEN 常亮模式,持续发光 视频补光、微距拍摄

重点注意: FLASH_MODE_ALWAYS_OPEN 是常亮模式,不是“一直闪”。它在拍照或录视频过程中会持续发光,适用于视频场景。FLASH_MODE_OPEN 则是在拍照瞬间闪光。

这个组件要解决的核心问题是:在相机启动后,用户能实时切换闪光灯模式,并且UI显示和实际闪光灯状态保持一致。

环境说明

DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:HarmonyOS NEXT 手机

核心实现:完整可运行的闪光灯控制组件

1. 相机初始化与闪光灯模式设置

这一段代码完成相机初始化和获取闪光灯能力。闪光灯控制依赖于 Flash 对象,而 Flash 对象又需要通过 CameraInputFlashMode 属性来获取。

// FlashController.ets
import { camera } from '@kit.CameraKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { common } from '@kit.AbilityKit';

export class FlashController {
  private cameraManager: camera.CameraManager | null = null;
  private cameraInput: camera.CameraInput | null = null;
  private flash: camera.Flash | null = null;
  private context: common.Context;

  constructor(context: common.Context) {
    this.context = context;
  }

  // 初始化相机资源,获取闪光灯控制对象
  async initCamera(): Promise<void> {
    try {
      // 获取CameraManager
      this.cameraManager = camera.getCameraManager(this.context);
      if (!this.cameraManager) {
        console.error('FlashController: 获取CameraManager失败');
        return;
      }

      // 获取相机列表,这里使用后置摄像头作为示例
      const cameras: Array<camera.CameraDevice> = this.cameraManager.getSupportedCameras();
      if (cameras.length === 0) {
        console.error('FlashController: 未找到相机设备');
        return;
      }

      // 选择一个后置摄像头
      const cameraDevice = cameras.find((device: camera.CameraDevice) =>
        device.cameraPosition === camera.CameraPosition.CAMERA_POSITION_BACK
      );
      if (!cameraDevice) {
        console.error('FlashController: 未找到后置摄像头');
        return;
      }

      // 创建相机输入流
      this.cameraInput = this.cameraManager.createCameraInput(cameraDevice);
      if (!this.cameraInput) {
        console.error('FlashController: 创建CameraInput失败');
        return;
      }

      // 打开相机
      await this.cameraInput.open();

      // 获取闪光灯控制对象
      this.flash = this.cameraInput.getFlash();
      if (!this.flash) {
        console.error('FlashController: 该相机不支持闪光灯');
        return;
      }

      console.info('FlashController: 相机初始化完成,闪光灯可用');
    } catch (error) {
      let err = error as BusinessError;
      console.error(`FlashController: 初始化相机失败, code: ${err.code}, message: ${err.message}`);
    }
  }

  // 设置闪光灯模式
  async setFlashMode(mode: camera.FlashMode): Promise<boolean> {
    if (!this.flash) {
      console.error('FlashController: 闪光灯对象未初始化');
      return false;
    }

    try {
      await this.flash.setFlashMode(mode);
      console.info(`FlashController: 闪光灯模式设置为 ${mode}`);
      return true;
    } catch (error) {
      let err = error as BusinessError;
      console.error(`FlashController: 设置闪光灯模式失败, code: ${err.code}, message: ${err.message}`);
      return false;
    }
  }

  // 获取当前闪光灯模式
  async getCurrentFlashMode(): Promise<camera.FlashMode | null> {
    if (!this.flash) {
      console.error('FlashController: 闪光灯对象未初始化');
      return null;
    }

    try {
      const mode: camera.FlashMode = await this.flash.getFlashMode();
      console.info(`FlashController: 当前闪光灯模式为 ${mode}`);
      return mode;
    } catch (error) {
      let err = error as BusinessError;
      console.error(`FlashController: 获取闪光灯模式失败, code: ${err.code}, message: ${err.message}`);
      return null;
    }
  }

  // 释放相机资源
  release(): void {
    try {
      this.cameraInput?.close();
      this.cameraManager = null;
      this.cameraInput = null;
      this.flash = null;
      console.info('FlashController: 相机资源已释放');
    } catch (error) {
      let err = error as BusinessError;
      console.error(`FlashController: 释放资源失败, code: ${err.code}, message: ${err.message}`);
    }
  }
}

关键点:

  • getFlash() 方法需要先打开 CameraInput 才能调用,否则会报错。
  • 闪光灯模式设置是异步操作,需要用 await 等待完成。
  • 如果相机不支持闪光灯,getFlash() 返回 null,需要做容错处理。

2. 闪光灯模式切换UI组件

这一部分提供完整的ArkUI界面,包含一个按钮组,点击后切换闪光灯模式,并显示当前模式。

// FlashModeUI.ets
import { camera } from '@kit.CameraKit';
import { FlashController } from './FlashController';

@Entry
@Component
struct FlashModePage {
  @State currentMode: number = camera.FlashMode.FLASH_MODE_CLOSE;
  private flashController: FlashController = new FlashController(getContext());

  aboutToAppear(): void {
    this.initCamera();
  }

  aboutToDisappear(): void {
    this.flashController.release();
  }

  async initCamera(): Promise<void> {
    await this.flashController.initCamera();
    // 初始化完成后,获取当前闪光灯模式,同步UI
    const mode: camera.FlashMode | null = await this.flashController.getCurrentFlashMode();
    if (mode !== null) {
      this.currentMode = mode;
    }
  }

  build() {
    Column() {
      // 模式选择按钮组
      Row() {
        this.createModeButton('关闭', camera.FlashMode.FLASH_MODE_CLOSE)
        this.createModeButton('开启', camera.FlashMode.FLASH_MODE_OPEN)
        this.createModeButton('自动', camera.FlashMode.FLASH_MODE_AUTO)
        this.createModeButton('常亮', camera.FlashMode.FLASH_MODE_ALWAYS_OPEN)
      }
      .width('100%')
      .height(80)
      .padding({ left: 10, right: 10 })

      // 当前模式提示
      Text(`当前闪光灯模式: ${this.getModeText(this.currentMode)}`)
        .fontSize(16)
        .margin({ top: 20 })
        .textAlign(TextAlign.Center)
        .width('100%')
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

  // 创建单个模式按钮
  @Builder
  createModeButton(label: string, mode: camera.FlashMode) {
    Button(label)
      .width(60)
      .height(60)
      .margin({ left: 5, right: 5 })
      .backgroundColor(this.currentMode === mode ? '#007AFF' : '#8E8E93')
      .fontColor(Color.White)
      .fontSize(14)
      .borderRadius(30)
      .onClick(() => {
        this.switchFlashMode(mode);
      })
  }

  // 切换闪光灯模式
  async switchFlashMode(mode: camera.FlashMode): Promise<void> {
    const success: boolean = await this.flashController.setFlashMode(mode);
    if (success) {
      this.currentMode = mode;
      console.info(`UI: 闪光灯模式已切换为 ${mode}`);
    } else {
      console.error('UI: 切换闪光灯模式失败');
      // 可以在这里弹出提示
    }
  }

  // 将模式枚举转换为可读文本
  getModeText(mode: number): string {
    switch (mode) {
      case camera.FlashMode.FLASH_MODE_CLOSE:
        return '关闭';
      case camera.FlashMode.FLASH_MODE_OPEN:
        return '开启';
      case camera.FlashMode.FLASH_MODE_AUTO:
        return '自动';
      case camera.FlashMode.FLASH_MODE_ALWAYS_OPEN:
        return '常亮';
      default:
        return '未知';
    }
  }
}

关键点:

  • aboutToAppear 中初始化相机,aboutToDisappear 中释放资源,这是标准的生命周期管理。
  • 点击按钮后,先调用 setFlashMode,成功后再更新UI状态 currentMode,避免UI与实际状态不一致。
  • 按钮的激活态通过 currentMode 比较来控制,不需要单独维护一个状态数组。

常见问题与解决方案

问题1:切换闪光灯模式后,相机预览画面无变化

现象:
调用 setFlashMode 返回成功,但相机预览画面中闪光灯没有亮起,拍照时也没有闪光效果。

原因:
这个问题与API的设计有关。setFlashMode 只是设置了闪光灯的“意图”,但实际是否生效取决于相机的当前会话配置。如果相机会话没有正确配置或者处于非活动状态,设置的模式不会立即生效。

在官方文档的实现路径中,闪光灯模式需要在创建 PhotoSessionVideoSession 后、开启会话前进行配置。如果不在正确的时机调用,设置会被忽略。

解决方案:
确保在 CameraInput 打开并且 Session 创建成功后,再调用 setFlashMode。推荐在相机初始化流程中同步完成闪光灯模式的配置。

// 在初始化相机后的配置阶段设置闪光灯模式
async configureSession(): Promise<void> {
  // 创建并配置Session...
  // 设置闪光灯模式
  await this.flash?.setFlashMode(camera.FlashMode.FLASH_MODE_AUTO);
  // 开始会话
  // ...
}

如果用户动态切换模式,需要确保相机会话处于“已配置”状态,调用 setFlashMode 才会立刻生效。在 PhotoSessionVideoSession 对象上,也有一个 setFlashMode 方法,两者必须保持一致。

问题2:页面退出后,再次进入时闪光灯状态未恢复

现象:
应用第一次打开时,闪光灯默认是关闭状态。用户切换到“开启”模式。退出页面再回来,UI显示的是“关闭”,但实际闪光灯可能还是开启状态,或者初始化后状态出现混乱。

原因:
这个问题由两部分组成:

  1. UI状态丢失: @State 在页面销毁后不会持久化,再次进入时 currentMode 会恢复为初始值。
  2. 相机状态不一致: 相机资源在 aboutToDisappear 中被释放,但设备硬件可能保留了上次的设置。重新初始化时,getCurrentFlashMode 返回的是硬件当前状态,可能与UI期望的不一致。

解决方案:
aboutToAppear 中强制从硬件读取状态,并更新UI。不要依赖上次设置的值。

async initCamera(): Promise<void> {
  await this.flashController.initCamera();
  // 强制从硬件读取当前闪光灯模式
  const mode: camera.FlashMode | null = await this.flashController.getCurrentFlashMode();
  if (mode !== null) {
    this.currentMode = mode; // 以硬件状态为准
  } else {
    // 如果获取失败,使用默认值
    this.currentMode = camera.FlashMode.FLASH_MODE_CLOSE;
  }
}

这种做法确保了每次页面显示时,UI状态和相机状态完全同步。用户在任何地方修改了闪光灯模式,回到页面都能正确显示。

最佳实践

  1. 不要依赖默认状态。 相机启动后,getCurrentFlashMode 返回的值不一定是你期望的。在UI初始化时,强制读取一次硬件状态。

  2. UI更新必须在API调用成功之后。 很多同学习惯先改UI再调用API,这样做在API调用失败时会导致UI与状态不一致。始终遵循“调用API -> 检查结果 -> 更新UI”的顺序。

  3. 生命周期管理要彻底。 CameraInputopenclose 必须成对出现。如果在 aboutToAppearopen,一定要在对应的 aboutToDisappearclose。漏掉 close 会导致相机资源一直被占用,影响其他应用使用。

  4. 异步边界要清晰。 setFlashMode 是异步的,不要在循环中连续调用多次。如果需要连续切换,应该等到上一次操作完成后再进行下一次。

完整Demo入口

// Index.ets
import { FlashModePage } from './FlashModeUI';

@Entry
@Component
struct Index {
  build() {
    Column() {
      FlashModePage()
    }
    .width('100%')
    .height('100%')
  }
}

FAQ

Q:为什么在模拟器上切换闪光灯模式没有任何效果?

A:模拟器不支持真实的相机硬件,闪光灯控制依赖于设备硬件。setFlashMode 调用在模拟器上会成功返回,但不会产生物理效果。所有与闪光灯相关的功能必须在真机上测试。

Q:为什么页面返回后,闪光灯模式没有恢复成关闭状态?

A:相机硬件状态在页面销毁后不会自动重置。如果你希望用户离开页面时关闭闪光灯,需要在 aboutToDisappear 中显式调用 setFlashMode(FLASH_MODE_CLOSE),然后再释放相机资源。但这取决于业务需求,不是必须的行为。

Q:为什么第一次授权相机权限成功,第二次打开应用时授权弹窗又弹出来了?

A:HarmonyOS的权限授权是持久化的,正常情况下不会重复弹窗。如果每次都弹窗,说明你的应用没有正确保存或检查权限状态。检查 requestPermissionsFromUser 的调用时机,确保只在权限未授权时调用。另外,某些开发者选项可能会重置权限状态,这种属于特殊调试环境。

Logo

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

更多推荐