ReactNative项目OpenHarmony三方库集成实战:react-native-camera
在以下环境验证通过:在项目根目录执行以下命令,本文基于 RN 0.72.90 版本开发:2. 验证安装安装完成后,检查文件:🔧 HarmonyOS 平台配置 ⭐由于 HarmonyOS 不支持 AutoLink,需要手动配置原生端代码。本文提供 HAR 包引入 和 源码引入 两种方式,可根据实际需求选择。打开 ,添加以下配置:方式一:HAR 包引入(推荐)📦HAR 包引入方式简单快捷,适合大多
📷 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.ets 或 entry/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.ets 或 entry/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) ,请自由地享受和参与开源。
更多推荐


所有评论(0)