📷 RN项目鸿蒙化三方库集成实战:react-native-camera

欢迎加入开源鸿蒙跨平台社区https://openharmonycrossplatform.csdn.net

📋 前言

相机功能已经成为许多应用的核心功能之一。无论是社交应用拍照分享、电商应用扫码购物,还是企业应用文档扫描,都需要用到相机功能。react-native-camera 是 React Native 生态中最流行的相机组件库,提供了丰富的相机控制功能,包括拍照、录像、二维码扫描、人脸识别等,让开发者能够轻松实现各种相机相关功能。

🎯 库简介

基本信息

  • 库名称: react-native-camera
  • 版本信息:
    • v3.40.0 + @react-native-ohos/react-native-camera: 支持 RN 0.72/0.77 版本
  • 官方仓库: https://github.com/react-native-camera/react-native-camera
  • 鸿蒙仓库: https://github.com/react-native-oh-library/react-native-camera
  • 主要功能:
    • 📷 拍照功能,支持多种分辨率和质量设置
    • 🎥 视频录制,支持音频录制
    • 📱 前后摄像头切换
    • 🔦 闪光灯控制(开/关/自动)
    • 🔍 自动对焦和手动对焦
    • 📊 二维码/条形码扫描
    • 🎨 白平衡和曝光控制
    • 📐 缩放控制
    • 👤 人脸检测(部分支持)

为什么选择 react-native-camera?

功能需求 原生实现 react-native-camera
拍照功能 ⚠️ 需原生代码 ✅ 一行代码搞定
视频录制 ⚠️ 复杂原生实现 ✅ 简单 API 调用
二维码扫描 ⚠️ 需集成扫码库 ✅ 内置扫码功能
相机参数控制 ❌ 各平台差异大 ✅ 统一接口
闪光灯/对焦控制 ⚠️ 需原生权限 ✅ 自动处理
HarmonyOS 支持 ❌ 无 ✅ 完整适配

支持的核心功能

功能分类 功能名称 说明 HarmonyOS 支持
基础功能 拍照 takePictureAsync
录像 recordAsync
停止录像 stopRecording
相机控制 前后摄像头切换 type 属性
闪光灯控制 flashMode 属性
自动对焦 autoFocus 属性
缩放控制 zoom 属性
曝光控制 exposure 属性
高级功能 二维码扫描 onBarCodeRead
人脸检测 onFacesDetected ❌ 暂不支持
视频设置 视频质量 defaultVideoQuality
音频录制 captureAudio

兼容性验证

在以下环境验证通过:

  • RNOH: 0.72.90; SDK: HarmonyOS 6.0.0 Release SDK; IDE: DevEco Studio 6.0.2; ROM: 6.0.0

📦 安装步骤

1. 安装依赖

在项目根目录执行以下命令,本文基于 RN 0.72.90 版本开发:

# npm 安装
npm install @react-native-ohos/react-native-camera@3.40.1-rc.2

# 或者使用 yarn
yarn add @react-native-ohos/react-native-camera@3.40.1-rc.2

2. 验证安装

安装完成后,检查 package.json 文件:

{
  "dependencies": {
    "@react-native-ohos/react-native-camera": "^3.40.1-rc.2",
    // ... 其他依赖
  }
}

🔧 HarmonyOS 平台配置 ⭐

由于 HarmonyOS 不支持 AutoLink,需要手动配置原生端代码。本文提供 HAR 包引入源码引入 两种方式,可根据实际需求选择。

1. 在工程根目录的 oh-package.json5 添加 overrides 字段

在这里插入图片描述

打开 harmony/oh-package.json5,添加以下配置:

{
  // ... 其他配置
  "overrides": {
    "@rnoh/react-native-openharmony": "0.72.90"
  }
}

方式一:HAR 包引入(推荐)📦

HAR 包引入方式简单快捷,适合大多数场景。

💡 提示:HAR 包位于三方库安装路径的 harmony 文件夹下。

2.1 在 entry/oh-package.json5 添加依赖

打开 harmony/entry/oh-package.json5,添加以下依赖:

"dependencies": {
  "@rnoh/react-native-openharmony": "0.72.90",
  + "@react-native-ohos/react-native-camera": "file:../../node_modules/@react-native-ohos/react-native-camera/harmony/reactNativeCamera.har"
}

2.2 同步依赖

点击 DevEco Studio 右上角的 sync 按钮,或者在终端执行:

cd harmony/entry
ohpm install

2.3 配置 CMakeLists.txt

打开 harmony/entry/src/main/cpp/CMakeLists.txt,添加以下配置:

project(rnapp)
cmake_minimum_required(VERSION 3.4.1)
set(CMAKE_SKIP_BUILD_RPATH TRUE)
set(RNOH_APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
set(NODE_MODULES "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../node_modules")
+ set(OH_MODULES "${CMAKE_CURRENT_SOURCE_DIR}/../../../oh_modules")
set(RNOH_CPP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../../react-native-harmony/harmony/cpp")
set(LOG_VERBOSITY_LEVEL 1)
set(CMAKE_ASM_FLAGS "-Wno-error- unused-command-line-argument -Qunused-arguments")
set(CMAKE_CXX_FLAGS "-fstack-protector-strong -Wl,-z,relro,-z,now,-z,noexecstack -s -fPIE -pie")
set(WITH_HITRACE_SYSTRACE 1)
add_compile_definitions(WITH_HITRACE_SYSTRACE)

add_subdirectory("${RNOH_CPP_DIR}" ./rn)

# 添加 Camera 模块
+ add_subdirectory("${OH_MODULES}/@react-native-ohos/react-native-camera/src/main/cpp" ./reactNativeCamera)

file(GLOB GENERATED_CPP_FILES "./generated/*.cpp")

add_library(rnoh_app SHARED
    ${GENERATED_CPP_FILES}
    "./PackageProvider.cpp"
    "${RNOH_CPP_DIR}/RNOHAppNapiBridge.cpp"
)
target_link_libraries(rnoh_app PUBLIC rnoh)

# 链接 Camera 库
+ target_link_libraries(rnoh_app PUBLIC rnoh_native_camera)

2.4 修改 PackageProvider.cpp

打开 harmony/entry/src/main/cpp/PackageProvider.cpp,添加:

#include "RNOH/PackageProvider.h"
#include "generated/RNOHGeneratedPackage.h"
+ #include "NativeCameraPackage.h"

using namespace rnoh;

std::vector<std::shared_ptr<Package>> PackageProvider::getPackages(Package::Context ctx) {
    return {
        std::make_shared<RNOHGeneratedPackage>(ctx),
        + std::make_shared<NativeCameraPackage>(ctx),
    };
}

2.5 在 ArkTs 侧引入相机组件

⚠️ 重要:本库使用了混合方案,需要添加组件名映射。

找到 buildCustomRNComponent() 函数,一般位于 entry/src/main/ets/pages/index.etsentry/src/main/ets/rn/LoadBundle.ets,添加:

import { ReactCameraView } from '@react-native-ohos/react-native-camera';

@Builder
export function buildCustomRNComponent(ctx: ComponentBuilderContext) {
  // ... 其他组件
  + if (ctx.componentName === ReactCameraView.NAME) {
  +   ReactCameraView({
  +     ctx: ctx.rnComponentContext,
  +     tag: ctx.tag,
  +   })
  + }
}

2.6 添加组件名到常量数组

entry/src/main/ets/pages/index.etsentry/src/main/ets/rn/LoadBundle.ets 找到常量 arkTsComponentNames,在其数组里添加组件名:

const arkTsComponentNames: Array<string> = [
  SampleView.NAME,
  GeneratedSampleView.NAME,
  PropsDisplayer.NAME,
  + ReactCameraView.NAME
];

2.7 在 ArkTs 侧引入 ReactNativeCameraPackage

打开 harmony/entry/src/main/ets/RNPackagesFactory.ts,添加:

import type { RNPackageContext, RNPackage } from 'rnoh/ts';
+ import { ReactNativeCameraPackage, FaceDectorPackage } from '@react-native-ohos/react-native-camera/ts';

export function createRNPackages(ctx: RNPackageContext): RNPackage[] {
  return [
    // ... 其他包
    + new ReactNativeCameraPackage(ctx),
    + new FaceDectorPackage(ctx),
  ];
}

🔐 权限配置

在 entry 目录下的 module.json5 中添加权限

打开 harmony/entry/src/main/module.json5,添加:

"requestPermissions": [
  {
    "name": "ohos.permission.CAMERA",
    "reason": "$string:camera_reason",
    "usedScene": {
      "abilities": [
        "EntryAbility"
      ],
      "when": "inuse"
    }
  },
  {
    "name": "ohos.permission.MICROPHONE",
    "reason": "$string:microphone_reason",
    "usedScene": {
      "abilities": [
        "EntryAbility"
      ],
      "when": "inuse"
    }
  }
]

在 string.json 中添加权限说明

打开 harmony/entry/src/main/resources/base/element/string.json,添加:

{
  "string": [
    {
      "name": "camera_reason",
      "value": "使用相机进行拍照和录像"
    },
    {
      "name": "microphone_reason",
      "value": "使用麦克风录制视频声音"
    }
  ]
}

同步并运行 🚀

3. 同步依赖

点击 DevEco Studio 右上角的 sync 按钮,或者在终端执行:

cd harmony/entry
ohpm install

然后编译、运行即可。

📖 API 详解

🔷 RNCamera 组件属性

1. type - 摄像头类型 📱

控制使用前置或后置摄像头。

import { RNCamera } from '@react-native-ohos/react-native-camera';

<RNCamera
  type="back"  // 后置摄像头
  // type="front"  // 前置摄像头
  style={styles.camera}
/>
说明
'back' 后置摄像头(默认)
'front' 前置摄像头

2. flashMode - 闪光灯模式 🔦

控制闪光灯的开关状态。

<RNCamera
  flashMode="on"
  style={styles.camera}
/>
说明
'off' 关闭闪光灯
'on' 开启闪光灯
'auto' 自动闪光灯
'torch' 手电筒模式(常亮)

3. autoFocus - 自动对焦 🔍

控制相机的自动对焦行为。

<RNCamera
  autoFocus="on"
  style={styles.camera}
/>
说明
'on' 开启自动对焦(默认)
'off' 关闭自动对焦

4. zoom - 缩放控制 🔎

控制相机的缩放比例。

<RNCamera
  zoom={0.5}  // 0.0 - 1.0
  style={styles.camera}
/>
参数 类型 说明
zoom number 缩放比例,范围 0.0 - 1.0

5. whiteBalance - 白平衡 ⚪

控制相机的白平衡设置。

<RNCamera
  whiteBalance="auto"
  style={styles.camera}
/>
说明
'auto' 自动白平衡
'sunny' 晴天
'cloudy' 阴天
'shadow' 阴影
'incandescent' 白炽灯
'fluorescent' 荧光灯

6. exposure - 曝光控制 ☀️

控制相机的曝光值。

<RNCamera
  exposure={0.5}  // -1.0 到 1.0
  style={styles.camera}
/>

🔷 拍照和录像

7. takePictureAsync - 拍照 📸

拍摄一张照片。

const takePicture = async () => {
  if (this.camera) {
    const options = {
      quality: 80,            // 图片质量 0-100
      base64: false,          // 是否返回 base64
      width: 1920,            // 图片宽度
      exif: false,            // 是否包含 EXIF 信息
      doNotSave: false,       // 是否不保存到相册
    };
  
    const data = await this.camera.takePictureAsync(options);
    console.log('照片路径:', data.uri);
    console.log('图片宽度:', data.width);
    console.log('图片高度:', data.height);
  }
};
参数 类型 必填 说明
quality number 图片质量,0-1 之间
base64 boolean 是否返回 base64 编码
width number 图片宽度
height number 图片高度
exif boolean 是否包含 EXIF 信息
doNotSave boolean 是否不保存到相册

返回值

interface TakePictureResponse {
  uri: string;          // 图片本地路径
  width: number;        // 图片宽度
  height: number;       // 图片高度
  base64?: string;      // base64 编码(如果启用)
  exif?: object;        // EXIF 信息(如果启用)
}

⚠️ 鸿蒙版本返回值差异

  • 鸿蒙版本返回 path 属性而非 uri
  • 建议使用 data.path || data.uri 兼容两种情况
  • 返回值示例:{ path: "file://...", width: 1920, height: 1080, ... }
  • 重要:如果路径没有 file:// 前缀,需要手动添加,否则 Image 组件可能无法加载

⚠️ 照片保存位置

  • takePictureAsync 只将照片保存到应用缓存目录,不会自动保存到系统相册
  • 如需保存到相册,需要使用 @react-native-ohos/react-native-cameraroll 或鸿蒙媒体库 API
  • 照片路径示例:file:///data/storage/el2/base/cache/camera/xxx.jpeg

8. recordAsync - 开始录像 🎥

开始录制视频。

const startRecording = async () => {
  if (this.camera) {
    const options = {
      quality: 2,  // 鸿蒙版本必须使用数值,不能使用字符串!
      maxDuration: 60,        // 最大录制时长(秒)
      maxFileSize: 100 * 1024 * 1024,  // 最大文件大小(字节)
      mute: false,            // 是否静音
      path: '',               // 自定义保存路径
    };
  
    const data = await this.camera.recordAsync(options);
    console.log('视频路径:', data.uri);
  }
};

⚠️ 鸿蒙版本 quality 参数必须使用数值

  • 鸿蒙版本 Camera.Constants.VideoQuality 未定义
  • 使用字符串形式(如 '720p')会报错 “cannot convert undefined value to object”
  • 必须使用数值形式
// 鸿蒙版本正确的用法
const VIDEO_QUALITY = {
  '2160p': 0,
  '1080p': 1,
  '720p': 2,
  '480p': 3,
  '4:3': 4,
};

const options = {
  quality: VIDEO_QUALITY['720p'],  // 使用数值 2
  maxDuration: 30,
  mute: true,
};
参数 类型 必填 说明
quality number 视频质量(鸿蒙版本必须使用数值)
maxDuration number 最大录制时长(秒)
maxFileSize number 最大文件大小(字节)
mute boolean 是否静音录制

视频质量数值

数值 说明
0 4K (2160p)
1 全高清 (1080p)
2 高清 (720p)
3 标清 (480p)
4 4:3 比例

返回值

interface RecordResponse {
  uri: string;          // 视频本地路径
  videoOrientation: number;  // 视频方向
  isRecordingInterrupted: boolean;  // 是否被中断
}

⚠️ 鸿蒙版本注意事项

  • 录像返回值可能为 undefined,需要做空值检查
  • 建议使用 try-catch 捕获录像过程中的错误
  • 录像完成后需要检查 data && data.uri 是否存在

9. stopRecording - 停止录像 ⏹️

停止当前的视频录制。

const stopRecording = () => {
  if (this.camera) {
    this.camera.stopRecording();
  }
};

🔷 二维码扫描

10. onBarCodeRead - 二维码扫描 📱

监听并读取二维码/条形码。

⚠️ 重要:鸿蒙版本必须设置 barCodeScannerEnabled={true} 才能启用扫码功能!

⚠️ 鸿蒙版本特殊限制

  • 扫码模式和拍照/录像模式是互斥的,不能同时使用
  • barCodeScannerEnabled 只在组件初始化时生效,无法动态切换
const [cameraKey, setCameraKey] = useState(0);

const handleModeChange = (newMode: 'photo' | 'video' | 'qr') => {
  if (newMode !== mode) {
    setLastScannedData(null);
    setIsScanning(true);
    setMode(newMode);
    setCameraKey(prev => prev + 1);  // 强制重新挂载组件
  }
};

// RNCamera 使用动态 key
<RNCamera
  key={`camera-${cameraKey}-${isQRMode ? 'qr' : 'media'}`}
  ...
/>

⚠️ 防抖处理onBarCodeRead 回调会持续触发,必须添加防抖逻辑防止重复弹窗!

// 添加防抖状态
const [isScanning, setIsScanning] = useState(true);
const [lastScannedData, setLastScannedData] = useState<string | null>(null);

const handleBarCodeRead = (event: { type: string; data: string }) => {
  if (!isScanning) return;
  if (!event || !event.data) return;
  if (lastScannedData === event.data) return;
  
  setIsScanning(false);
  setLastScannedData(event.data);
  
  Alert.alert(
    '扫描结果',
    `类型: ${event.type}\n内容: ${event.data}`,
    [
      { text: '继续扫描', onPress: () => setIsScanning(true) },
      { text: '关闭', style: 'cancel' }
    ]
  );
};

事件对象

interface BarCodeReadEvent {
  data: string;        // 扫描结果内容
  type: string;        // 条码类型
  bounds: {
    origin: { x: number; y: number };  // 左上角坐标
    size: { width: number; height: number };  // 尺寸
  };
  target: number;      // 目标 ID
  rawData?: string;    // 原始数据
}

支持的条码类型

类型 说明
ean13 EAN-13 条码
ean8 EAN-8 条码
qr QR 码
code128 Code 128 条码
code39 Code 39 条码
code93 Code 93 条码
upc_e UPC-E 条码
upc_a UPC-A 条码
pdf417 PDF417 条码
aztec Aztec 码
datamatrix Data Matrix 码

🔷 其他 API

11. getCameraIdAsync - 获取可用摄像头列表 📋

获取设备上所有可用的摄像头 ID。

const cameraIds = await RNCamera.getCameraIdAsync();
// 返回值示例: ["0", "1"] (后置和前置摄像头)
12. refreshAuthorization - 刷新权限 🔄

重新请求相机权限。

await RNCamera.refreshAuthorization();

在这里插入图片描述

📊 完整示例:多功能相机应用

使用了react-native-video库适配可以看这篇文章:https://blog.csdn.net/2402_83107102/article/details/159243859

import React, { useRef, useState, useEffect, useCallback } from 'react';
import {
  View,
  Text,
  StyleSheet,
  TouchableOpacity,
  Image,
  Alert,
  Vibration,
  Dimensions,
  Platform,
  DeviceEventEmitter,
} from 'react-native';
import { RNCamera } from '@react-native-ohos/react-native-camera';
import Video, { VideoRef } from 'react-native-video';

const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');

const VIDEO_QUALITY = {
  '2160p': 0,
  '1080p': 1,
  '720p': 2,
  '480p': 3,
  '4:3': 4,
};

function MultiFunctionCamera() {
  const cameraRef = useRef<RNCamera>(null);
  const videoPlayerRef = useRef<VideoRef>(null);
  const [cameraType, setCameraType] = useState<'back' | 'front'>('back');
  const [flashMode, setFlashMode] = useState<'off' | 'on' | 'auto' | 'torch'>('off');
  const [zoom, setZoom] = useState<number>(0.1);
  const [isRecording, setIsRecording] = useState(false);
  const [photoUri, setPhotoUri] = useState<string | null>(null);
  const [videoUri, setVideoUri] = useState<string | null>(null);
  const [mode, setMode] = useState<'photo' | 'video' | 'qr'>('photo');
  const [lastScannedData, setLastScannedData] = useState<string | null>(null);
  const [isScanning, setIsScanning] = useState(true);
  const [cameraKey, setCameraKey] = useState(0);
  const recordingResolveRef = useRef<Function | null>(null);
  const recordingRejectRef = useRef<Function | null>(null);

  useEffect(() => {
    const recordingErrorListener = DeviceEventEmitter.addListener('onRecordingError', (error: any) => {
      console.log('录像错误事件:', error);
      setIsRecording(false);
      if (recordingRejectRef.current) {
        recordingRejectRef.current(new Error(error?.message || '录像过程中发生错误'));
        recordingResolveRef.current = null;
        recordingRejectRef.current = null;
      }
    });

    const recordingFinishedListener = DeviceEventEmitter.addListener('onRecordingFinished', (video: any) => {
      console.log('录像完成事件:', video);
      setIsRecording(false);
      if (recordingResolveRef.current) {
        recordingResolveRef.current(video);
        recordingResolveRef.current = null;
        recordingRejectRef.current = null;
      }
    });

    return () => {
      recordingErrorListener.remove();
      recordingFinishedListener.remove();
    };
  }, []);

  const flashModes: { mode: 'off' | 'on' | 'auto' | 'torch'; icon: string; label: string }[] = [
    { mode: 'off', icon: '⚡', label: '关闭' },
    { mode: 'on', icon: '💡', label: '开启' },
    { mode: 'auto', icon: '🔄', label: '自动' },
    { mode: 'torch', icon: '🔦', label: '手电筒' },
  ];

  const toggleFlash = () => {
    const currentIndex = flashModes.findIndex((item) => item.mode === flashMode);
    const nextIndex = (currentIndex + 1) % flashModes.length;
    setFlashMode(flashModes[nextIndex].mode);
  };

  const toggleCamera = () => {
    setCameraType(
      cameraType === 'back'
        ? 'front'
        : 'back'
    );
  };

  const takePicture = async () => {
    if (cameraRef.current) {
      try {
        const options = {
          quality: 80,
          base64: false,
          width: 1920,
        };
        const data = await cameraRef.current.takePictureAsync(options);
        console.log('拍照返回数据:', JSON.stringify(data));
        if (data) {
          let photoPath = (data as any).path || (data as any).uri;
          if (photoPath) {
            if (!photoPath.startsWith('file://') && !photoPath.startsWith('content://')) {
              photoPath = 'file://' + photoPath;
            }
            console.log('最终照片路径:', photoPath);
            setPhotoUri(photoPath);
            Vibration.vibrate(50);
          } else {
            Alert.alert('错误', '拍照失败:未获取到图片路径');
          }
        } else {
          Alert.alert('错误', '拍照失败:返回数据为空');
        }
      } catch (error: any) {
        console.log('拍照错误:', error);
        Alert.alert('错误', `拍照失败: ${error?.message || error || '未知错误'}`);
      }
    }
  };

  const toggleRecording = async () => {
    if (cameraRef.current) {
      if (isRecording) {
        console.log('停止录像...');
        cameraRef.current.stopRecording();
        setIsRecording(false);
      } else {
        setIsRecording(true);
        console.log('开始录像...');
        
        try {
          const options = {
            quality: VIDEO_QUALITY['480p'] as any,
            maxDuration: 30,
            mute: true,
          };
          
          console.log('录像选项:', JSON.stringify(options));
          
          const data = await new Promise((resolve, reject) => {
            recordingResolveRef.current = resolve;
            recordingRejectRef.current = reject;
            
            cameraRef.current?.recordAsync(options)
              .then((result) => {
                console.log('recordAsync 返回:', result);
                resolve(result);
              })
              .catch((err) => {
                console.log('recordAsync 错误:', err);
                reject(err);
              });
            
            setTimeout(() => {
              if (recordingRejectRef.current) {
                recordingRejectRef.current(new Error('录像超时'));
                recordingResolveRef.current = null;
                recordingRejectRef.current = null;
              }
            }, 35000);
          });
          
          console.log('录像返回数据:', JSON.stringify(data));
          if (data && (data as any).uri) {
            let recordedUri = (data as any).uri;
            if (!recordedUri.startsWith('file://') && !recordedUri.startsWith('content://')) {
              recordedUri = 'file://' + recordedUri;
            }
            console.log('视频路径:', recordedUri);
            setVideoUri(recordedUri);
          } else if (data) {
            let recordedUri = (data as any).path || (data as any).uri;
            if (recordedUri) {
              if (!recordedUri.startsWith('file://') && !recordedUri.startsWith('content://')) {
                recordedUri = 'file://' + recordedUri;
              }
              console.log('视频路径:', recordedUri);
              setVideoUri(recordedUri);
            } else {
              Alert.alert('提示', '录像已完成,但未获取到视频路径');
            }
          }
        } catch (error: any) {
          console.log('录像错误:', error);
          Alert.alert('错误', `录像失败: ${error?.message || error || '未知错误'}`);
        } finally {
          setIsRecording(false);
          recordingResolveRef.current = null;
          recordingRejectRef.current = null;
        }
      }
    }
  };

  const handleBarCodeRead = useCallback((event: { type: string; data: string }) => {
    if (!isScanning) return;
    if (!event || !event.data) return;
    
    if (lastScannedData === event.data) return;
    
    setIsScanning(false);
    setLastScannedData(event.data);
    Vibration.vibrate(100);
    
    Alert.alert(
      '扫描结果',
      `类型: ${event.type}\n内容: ${event.data}`,
      [
        {
          text: '继续扫描',
          onPress: () => {
            setIsScanning(true);
          }
        },
        {
          text: '关闭',
          style: 'cancel'
        }
      ]
    );
  }, [isScanning, lastScannedData]);

  const getCurrentFlashIcon = () => {
    const current = flashModes.find((item) => item.mode === flashMode);
    return current ? current.icon : '⚡';
  };

  const handleModeChange = (newMode: 'photo' | 'video' | 'qr') => {
    if (newMode !== mode) {
      setIsRecording(false);
      setLastScannedData(null);
      setIsScanning(true);
      setMode(newMode);
      setCameraKey(prev => prev + 1);
    }
  };

  const handleRetake = () => {
    setPhotoUri(null);
    setVideoUri(null);
  };

  if (photoUri) {
    return (
      <View style={styles.container}>
        <Image 
          source={{ uri: photoUri }} 
          style={styles.preview}
          resizeMode="contain"
          onLoad={() => console.log('图片加载成功, URI:', photoUri)}
          onError={(e) => console.log('图片加载错误:', e.nativeEvent.error)}
        />
        <View style={styles.previewControls}>
          <TouchableOpacity
            style={styles.previewButton}
            onPress={handleRetake}
          >
            <Text style={styles.previewButtonText}>重新拍摄</Text>
          </TouchableOpacity>
        </View>
      </View>
    );
  }

  if (videoUri) {
    return (
      <View style={styles.container}>
        <Video
          source={{ uri: videoUri }}
          ref={videoPlayerRef}
          style={styles.videoPlayer}
          controls={true}
          resizeMode="contain"
          repeat={false}
          paused={false}
          onLoad={(data: any) => {
            console.log('视频加载成功:', data);
          }}
          onError={(error: any) => {
            console.log('视频加载错误:', error);
          }}
          onEnd={() => {
            console.log('视频播放结束');
          }}
        />
        <View style={styles.previewControls}>
          <TouchableOpacity
            style={styles.previewButton}
            onPress={handleRetake}
          >
            <Text style={styles.previewButtonText}>重新录制</Text>
          </TouchableOpacity>
        </View>
      </View>
    );
  }

  const isQRMode = mode === 'qr';

  return (
    <View style={styles.container}>
      <RNCamera
        key={`camera-${cameraKey}-${isQRMode ? 'qr' : 'media'}`}
        ref={cameraRef}
        style={styles.camera}
        type={cameraType}
        flashMode={flashMode}
        zoom={isQRMode ? 0.1 : zoom}
        autoFocus="on"
        captureAudio={!isQRMode}
        barCodeScannerEnabled={isQRMode}
        onBarCodeRead={isQRMode ? handleBarCodeRead : undefined}
      >
        <View style={styles.topBar}>
          <TouchableOpacity style={styles.topButton} onPress={toggleFlash}>
            <Text style={styles.topButtonIcon}>{getCurrentFlashIcon()}</Text>
          </TouchableOpacity>
          
          <View style={styles.modeTabs}>
            {['photo', 'video', 'qr'].map((m) => (
              <TouchableOpacity
                key={m}
                style={[styles.modeTab, mode === m && styles.modeTabActive]}
                onPress={() => handleModeChange(m as any)}
              >
                <Text
                  style={[
                    styles.modeTabText,
                    mode === m && styles.modeTabTextActive,
                  ]}
                >
                  {m === 'photo' ? '拍照' : m === 'video' ? '录像' : '扫码'}
                </Text>
              </TouchableOpacity>
            ))}
          </View>
          
          <TouchableOpacity style={styles.topButton} onPress={toggleCamera}>
            <Text style={styles.topButtonIcon}>🔄</Text>
          </TouchableOpacity>
        </View>

        {isQRMode && (
          <View style={styles.qrOverlay}>
            <View style={styles.qrFrame}>
              <View style={[styles.qrCorner, styles.qrTopLeft]} />
              <View style={[styles.qrCorner, styles.qrTopRight]} />
              <View style={[styles.qrCorner, styles.qrBottomLeft]} />
              <View style={[styles.qrCorner, styles.qrBottomRight]} />
            </View>
            <Text style={styles.qrHint}>将二维码放入框内</Text>
          </View>
        )}

        {!isQRMode && (
          <View style={styles.zoomContainer}>
            <Text style={styles.zoomLabel}>缩放: {Math.abs(zoom - 0.1) < 0.01 ? '1x' : Math.abs(zoom - 0.5) < 0.01 ? '5x' : '10x'}</Text>
            <View style={styles.zoomBar}>
              <TouchableOpacity
                style={[styles.zoomLevel, Math.abs(zoom - 0.1) < 0.01 && styles.zoomLevelActive]}
                onPress={() => setZoom(0.1)}
              >
                <Text style={styles.zoomText}>1x</Text>
              </TouchableOpacity>
              <TouchableOpacity
                style={[styles.zoomLevel, Math.abs(zoom - 0.5) < 0.01 && styles.zoomLevelActive]}
                onPress={() => setZoom(0.5)}
              >
                <Text style={styles.zoomText}>5x</Text>
              </TouchableOpacity>
              <TouchableOpacity
                style={[styles.zoomLevel, Math.abs(zoom - 1) < 0.01 && styles.zoomLevelActive]}
                onPress={() => setZoom(1)}
              >
                <Text style={styles.zoomText}>10x</Text>
              </TouchableOpacity>
            </View>
          </View>
        )}
      </RNCamera>

      <View style={styles.bottomBar}>
        {mode === 'photo' && (
          <TouchableOpacity style={styles.captureButton} onPress={takePicture}>
            <View style={styles.captureInner} />
          </TouchableOpacity>
        )}
        
        {mode === 'video' && (
          <TouchableOpacity
            style={[
              styles.recordButton,
              isRecording && styles.recordButtonActive,
            ]}
            onPress={toggleRecording}
          >
            <Text style={styles.recordIcon}>{isRecording ? '⏹️' : '⏺️'}</Text>
          </TouchableOpacity>
        )}
        
        {mode === 'qr' && (
          <View style={styles.qrPlaceholder}>
            <Text style={styles.qrPlaceholderText}>扫描中...</Text>
          </View>
        )}
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#000',
  },
  camera: {
    flex: 1,
  },
  topBar: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingTop: 50,
    paddingHorizontal: 20,
  },
  topButton: {
    width: 44,
    height: 44,
    borderRadius: 22,
    backgroundColor: 'rgba(0,0,0,0.5)',
    justifyContent: 'center',
    alignItems: 'center',
  },
  topButtonIcon: {
    fontSize: 20,
  },
  modeTabs: {
    flexDirection: 'row',
    backgroundColor: 'rgba(0,0,0,0.5)',
    borderRadius: 20,
    padding: 4,
  },
  modeTab: {
    paddingHorizontal: 16,
    paddingVertical: 8,
    borderRadius: 16,
  },
  modeTabActive: {
    backgroundColor: '#667eea',
  },
  modeTabText: {
    color: '#fff',
    fontSize: 14,
  },
  modeTabTextActive: {
    fontWeight: 'bold',
  },
  qrOverlay: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  qrFrame: {
    width: 250,
    height: 250,
    position: 'relative',
  },
  qrCorner: {
    position: 'absolute',
    width: 30,
    height: 30,
    borderColor: '#667eea',
  },
  qrTopLeft: {
    top: 0,
    left: 0,
    borderTopWidth: 3,
    borderLeftWidth: 3,
  },
  qrTopRight: {
    top: 0,
    right: 0,
    borderTopWidth: 3,
    borderRightWidth: 3,
  },
  qrBottomLeft: {
    bottom: 0,
    left: 0,
    borderBottomWidth: 3,
    borderLeftWidth: 3,
  },
  qrBottomRight: {
    bottom: 0,
    right: 0,
    borderBottomWidth: 3,
    borderRightWidth: 3,
  },
  qrHint: {
    color: '#fff',
    fontSize: 14,
    marginTop: 20,
  },
  zoomContainer: {
    position: 'absolute',
    bottom: 150,
    left: 0,
    right: 0,
    alignItems: 'center',
  },
  zoomLabel: {
    color: '#fff',
    fontSize: 14,
    marginBottom: 10,
  },
  zoomBar: {
    flexDirection: 'row',
    backgroundColor: 'rgba(0,0,0,0.5)',
    borderRadius: 20,
    padding: 4,
  },
  zoomLevel: {
    paddingHorizontal: 20,
    paddingVertical: 8,
    borderRadius: 16,
  },
  zoomLevelActive: {
    backgroundColor: '#667eea',
  },
  zoomText: {
    color: '#fff',
    fontSize: 14,
  },
  bottomBar: {
    height: 120,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#000',
  },
  captureButton: {
    width: 70,
    height: 70,
    borderRadius: 35,
    backgroundColor: '#fff',
    padding: 4,
    justifyContent: 'center',
    alignItems: 'center',
  },
  captureInner: {
    width: 60,
    height: 60,
    borderRadius: 30,
    backgroundColor: '#fff',
    borderWidth: 3,
    borderColor: '#667eea',
  },
  recordButton: {
    width: 80,
    height: 80,
    borderRadius: 40,
    backgroundColor: 'rgba(255,255,255,0.3)',
    justifyContent: 'center',
    alignItems: 'center',
    borderWidth: 4,
    borderColor: '#fff',
  },
  recordButtonActive: {
    backgroundColor: 'rgba(255,0,0,0.5)',
    borderColor: '#ff0000',
  },
  recordIcon: {
    fontSize: 30,
  },
  qrPlaceholder: {
    padding: 20,
  },
  qrPlaceholderText: {
    color: '#fff',
    fontSize: 16,
  },
  preview: {
    flex: 1,
    backgroundColor: '#111',
  },
  videoPlayer: {
    flex: 1,
    backgroundColor: '#000',
  },
  previewControls: {
    flexDirection: 'row',
    justifyContent: 'center',
    padding: 20,
    backgroundColor: '#000',
  },
  previewButton: {
    paddingHorizontal: 40,
    paddingVertical: 15,
    borderRadius: 25,
    backgroundColor: '#667eea',
  },
  previewButtonText: {
    color: '#fff',
    fontSize: 16,
    fontWeight: '600',
  },
});

export default MultiFunctionCamera;


⚠️ 遗留问题

问题 描述 状态
onFacesDetected 人脸检测功能暂不支持 ❌ 不支持
notAuthorizedView 未授权视图(Android 独有) ❌ HarmonyOS 使用系统弹窗
pendingAuthorizationView 授权中视图(Android 独有) ❌ HarmonyOS 使用系统弹窗
ratio 相机比例(Android 独有) ❌ HarmonyOS 无效果

📝 开源协议

本项目基于 MIT License (MIT) ,请自由地享受和参与开源。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐