在这里插入图片描述

📋 前言

react-native-camera-roll 是 React Native 社区最流行的相册访问库,提供了一套完整的相册管理解决方案。它支持保存图片/视频到相册、获取相册中的图片/视频、删除图片/视频等多种功能,并且完全兼容 Android、iOS 和 HarmonyOS 三端。

🎯 库简介

基本信息

  • 库名称: @react-native-ohos/camera-roll
  • 版本信息:
    • 7.8.4: 支持 RN 0.72 版本
    • 7.10.1: 支持 RN 0.77 版本
  • 官方仓库: https://github.com/react-native-oh-library/react-native-cameraroll
  • 主要功能:
    • 保存图片/视频到相册
    • 获取相册中的图片/视频
    • 获取相册列表
    • 删除图片/视频
    • 获取图片缩略图
    • 兼容 Android、iOS 和 HarmonyOS
  • 兼容性验证:
    • RNOH: 0.72.20; SDK: HarmonyOS NEXT Developer Beta1; IDE: DevEco Studio 5.0.3.200; ROM: 3.0.0.18;
    • RNOH: 0.77.18; SDK: HarmonyOS 6.0.0 Release SDK; IDE: DevEco Studio 6.0.0.868; ROM: 6.0.0.112;

为什么需要这个库?

  • 功能完整: 提供完整的相册管理解决方案
  • 跨平台: 在三端提供一致的体验
  • 易于使用: API 简单直观
  • 性能优异: 原生实现,高效稳定
  • 灵活配置: 支持多种配置选项

📦 安装步骤

1. 使用 npm 安装

根据您的 RN 版本选择对应的包名:

npm install @react-native-ohos/camera-roll@7.8.4-rc.1

2. 验证安装

安装完成后,检查 package.json 文件,应该能看到新增的依赖:

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

🔧 HarmonyOS 平台配置 ⭐

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

首先需要使用 DevEco Studio 打开项目里的 HarmonyOS 工程 harmony

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

{
  ...
  "overrides": {
    "@rnoh/react-native-openharmony": "^0.72.90"
  }
}

2. 引入原生端代码

方法一:通过 har 包引入(不推荐)

[!TIP] har 包位于三方库安装路径的 harmony 文件夹下。

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

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

点击右上角的 sync 按钮

或者在终端执行:

cd entry
ohpm install
方法二:直接链接源码

步骤 1: 把 <RN工程>/node_modules/@react-native-ohos/camera-roll/harmony 目录下的源码 camera_roll 复制到 harmony(鸿蒙壳工程)工程根目录下。

步骤 2: 在 harmony 工程根目录的 build-profile.template.json5(若存在)和 build-profile.json5 添加以下模块:

modules: [
  ...
  {
    name: 'camera_roll',
    srcPath: './camera_roll',
  }
]

步骤 3: 打开 camera_roll/oh-package.json5,修改 react-native-openharmony 和项目的版本一致。

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

"dependencies": {
  "@rnoh/react-native-openharmony": "0.72.90",
  "@react-native-ohos/camera-roll": "file:../camera_roll"
}

步骤 5: 点击 DevEco Studio 右上角的 sync 按钮

3. 配置 CMakeLists 和引入 CameraRollPackage

[!TIP] 仅 0.77 版本需要配置 CMakeLists 和引入 CameraRollPackage。

打开 entry/src/main/cpp/CMakeLists.txt,添加:

project(rnapp)
cmake_minimum_required(VERSION 3.4.1)
set(CMAKE_SKIP_BUILD_RPATH TRUE)
set(OH_MODULE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../oh_modules")
set(NODE_MODULES "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../node_modules")

set(RNOH_APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}")

set(RNOH_CPP_DIR "${OH_MODULE_DIR}/@rnoh/react-native-openharmony/src/main/cpp")
set(CMAKE_ASM_FLAGS "-Wno-error:unused-command-line-argument -Qunused-arguments")
set(CMAKE_CXX_FLAGS "-fstack-protector-strong -Wl,-z,relro,-z,noexecstack -s -fPIE -pie")

add_compile_definitions(WITH_HITRACE_SYSTRACE)
set(WITH_HITRACE_SYSTRACE 1) # for other CMakeLists.txt files to use

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

add_library(rnoh_app SHARED
    "${RNOH_CPP_DIR}/RNOHAppNapiBridge.cpp" "./PackageProvider.cpp")

target_link_libraries(rnoh_app PUBLIC rnoh)


set(OH_MODULES "${CMAKE_CURRENT_SOURCE_DIR}/../../../oh_modules")

+ add_subdirectory("${OH_MODULES}/@react-native-ohos/camera-roll/src/main/cpp" ./camera-roll)
+ target_link_libraries(rnoh_app PUBLIC rnoh_camera_roll)

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

#include "RNOH/PackageProvider.h"
#include "generated/RNOHGeneratedPackage.h"
#include "SamplePackage.h"
+ #include "CameraRollPackage.h"

using namespace rnoh;

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

4. 在 ArkTs 侧引入 CameraRollPackage

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

+ import { CameraRollPackage } from '@react-native-ohos/camera-roll/ts';

export function createRNPackages(ctx: RNPackageContext): RNPackage[] {
  return [
    new SamplePackage(ctx),
+   new CameraRollPackage(ctx)
  ];
}

5. 运行

点击右上角的 sync 按钮

或者在终端执行:

cd entry
ohpm install

然后编译、运行即可。

💻 完整代码示例

下面是一个完整的示例,展示了 react-native-camera-roll 的各种使用场景:

import React, { useState } from 'react';
import {
  View,
  Text,
  StyleSheet,
  TouchableOpacity,
  Image,
  ScrollView,
  Alert,
  TextInput,
} from 'react-native';
import {
  CameraRoll,
  type PhotoIdentifier,
  type GroupTypes,
} from '@react-native-camera-roll/camera-roll';

function CameraRollDemo() {
  const [savedImages, setSavedImages] = useState<string[]>([]);
  const [imageUrl, setImageUrl] = useState('');
  const [lastSavedAsset, setLastSavedAsset] = useState<PhotoIdentifier | null>(null);

  // 保存图片到相册
  const saveToCameraRoll = async () => {
    if (!imageUrl.trim()) {
      Alert.alert('提示', '请输入图片URL');
      return;
    }

    try {
      const asset = await CameraRoll.saveAsset(imageUrl);
      setLastSavedAsset(asset);
      setSavedImages([...savedImages, asset.node.image.uri]);
      Alert.alert('成功', '图片已保存到相册');
    } catch (error: any) {
      Alert.alert('错误', error.message || '保存失败');
    }
  };

  // 使用默认图片URL
  const useDefaultImage = () => {
    setImageUrl('https://res.vmallres.com/uomcdn/CN/cms/202408/5442d69d916d4bcf9ee740d595a164fb.jpg');
  };

  // 清空保存记录
  const clearHistory = () => {
    setSavedImages([]);
    setLastSavedAsset(null);
  };

  return (
    <ScrollView style={styles.container}>
      <View style={styles.content}>
        <Text style={styles.title}>相册管理器</Text>

        {/* 保存图片区域 */}
        <View style={styles.section}>
          <Text style={styles.sectionTitle}>保存图片到相册</Text>

          <TextInput
            style={styles.input}
            placeholder="输入图片URL"
            value={imageUrl}
            onChangeText={setImageUrl}
          />

          <TouchableOpacity
            style={styles.linkButton}
            onPress={useDefaultImage}
          >
            <Text style={styles.linkText}>使用示例图片</Text>
          </TouchableOpacity>

          <TouchableOpacity
            style={styles.primaryButton}
            onPress={saveToCameraRoll}
          >
            <Text style={styles.buttonText}>保存到相册</Text>
          </TouchableOpacity>
        </View>

        {/* 最后保存的图片信息 */}
        {lastSavedAsset && (
          <View style={styles.section}>
            <Text style={styles.sectionTitle}>最后保存的图片</Text>
            <Image
              source={{ uri: lastSavedAsset.node.image.uri }}
              style={styles.image}
            />
            <View style={styles.infoRow}>
              <Text style={styles.infoLabel}>文件名:</Text>
              <Text style={styles.infoText}>
                {lastSavedAsset.node.image.filename}
              </Text>
            </View>
            <View style={styles.infoRow}>
              <Text style={styles.infoLabel}>尺寸:</Text>
              <Text style={styles.infoText}>
                {lastSavedAsset.node.image.width} x {lastSavedAsset.node.image.height}
              </Text>
            </View>
            <View style={styles.infoRow}>
              <Text style={styles.infoLabel}>文件大小:</Text>
              <Text style={styles.infoText}>
                 {lastSavedAsset.node.image.fileSize ? `${(lastSavedAsset.node.image.fileSize / 1024).toFixed(2)} KB` : '未知'}
              </Text>
            </View>
            <View style={styles.infoRow}>
              <Text style={styles.infoLabel}>时间戳:</Text>
              <Text style={styles.infoText}>
                {new Date(lastSavedAsset.node.timestamp).toLocaleString()}
              </Text>
            </View>
            <View style={styles.infoRow}>
              <Text style={styles.infoLabel}>URI:</Text>
              <Text style={styles.infoText} numberOfLines={2}>
                {lastSavedAsset.node.image.uri}
              </Text>
            </View>
          </View>
        )}

        {/* 保存历史 */}
        {savedImages.length > 0 && (
          <View style={styles.section}>
            <View style={styles.sectionHeader}>
              <Text style={styles.sectionTitle}>保存历史 ({savedImages.length})</Text>
              <TouchableOpacity
                style={styles.smallButton}
                onPress={clearHistory}
              >
                <Text style={styles.smallButtonText}>清空</Text>
              </TouchableOpacity>
            </View>
            <View style={styles.imageGrid}>
              {savedImages.map((uri, index) => (
                <Image
                  key={index}
                  source={{ uri }}
                  style={styles.gridImage}
                />
              ))}
            </View>
          </View>
        )}

        {/* 说明 */}
        <View style={styles.noteSection}>
          <Text style={styles.noteTitle}>功能说明:</Text>
          <Text style={styles.noteText}>
            • 支持的图片格式:png/jpg/jpeg/heif/bmp/gif/webp/svg/heic
          </Text>
          <Text style={styles.noteText}>
            • 支持的视频格式:mp4/mov
          </Text>
          <Text style={styles.noteText}>
            • 需要在应用权限中申请相册读写权限
          </Text>
          <Text style={styles.noteText}>
            • HarmonyOS 部分接口受限,需要申请受限权限
          </Text>
        </View>

        {/* 使用限制 */}
        <View style={styles.warningSection}>
          <Text style={styles.warningTitle}>使用限制:</Text>
          <Text style={styles.warningText}>
            • getPhotos、getAlbums 等接口受限,需要申请受限权限
          </Text>
          <Text style={styles.warningText}>
            • deletePhotos 功能暂未适配 HarmonyOS
          </Text>
          <Text style={styles.warningText}>
            • 部分接口因 HarmonyOS 安全策略无法使用
          </Text>
          <Text style={styles.warningText}>
            • saveAsset 接口未完全支持所有格式
          </Text>
        </View>
      </View>
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f5f5f5',
  },
  content: {
    padding: 20,
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 20,
    textAlign: 'center',
    color: '#333',
  },
  section: {
    marginBottom: 20,
    padding: 15,
    backgroundColor: '#fff',
    borderRadius: 10,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
  },
  sectionTitle: {
    fontSize: 16,
    fontWeight: 'bold',
    marginBottom: 10,
    color: '#333',
  },
  sectionHeader: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: 10,
  },
  input: {
    height: 50,
    borderWidth: 1,
    borderColor: '#ddd',
    borderRadius: 8,
    paddingHorizontal: 15,
    marginBottom: 10,
    backgroundColor: '#f9f9f9',
    fontSize: 14,
  },
  linkButton: {
    marginBottom: 10,
  },
  linkText: {
    color: '#42a5f5',
    fontSize: 14,
    textDecorationLine: 'underline',
  },
  primaryButton: {
    backgroundColor: '#42a5f5',
    paddingVertical: 15,
    borderRadius: 8,
    alignItems: 'center',
  },
  buttonText: {
    color: '#fff',
    fontSize: 16,
    fontWeight: 'bold',
  },
  image: {
    width: '100%',
    height: 200,
    resizeMode: 'contain',
    marginBottom: 10,
    borderRadius: 8,
  },
  infoRow: {
    flexDirection: 'row',
    marginBottom: 5,
  },
  infoLabel: {
    fontSize: 14,
    fontWeight: 'bold',
    color: '#666',
    width: 80,
  },
  infoText: {
    fontSize: 14,
    color: '#333',
    flex: 1,
  },
  imageGrid: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    gap: 10,
  },
  gridImage: {
    width: 80,
    height: 80,
    borderRadius: 8,
  },
  smallButton: {
    backgroundColor: '#ef5350',
    paddingHorizontal: 12,
    paddingVertical: 6,
    borderRadius: 6,
  },
  smallButtonText: {
    color: '#fff',
    fontSize: 12,
    fontWeight: '500',
  },
  noteSection: {
    marginTop: 20,
    padding: 15,
    backgroundColor: '#e3f2fd',
    borderRadius: 8,
    borderLeftWidth: 4,
    borderLeftColor: '#42a5f5',
  },
  noteTitle: {
    fontSize: 14,
    fontWeight: 'bold',
    marginBottom: 8,
    color: '#1565c0',
  },
  noteText: {
    fontSize: 13,
    color: '#1976d2',
    marginBottom: 4,
  },
  warningSection: {
    marginTop: 20,
    padding: 15,
    backgroundColor: '#fff3cd',
    borderRadius: 8,
    borderLeftWidth: 4,
    borderLeftColor: '#ffc107',
  },
  warningTitle: {
    fontSize: 14,
    fontWeight: 'bold',
    marginBottom: 8,
    color: '#856404',
  },
  warningText: {
    fontSize: 13,
    color: '#856404',
    marginBottom: 4,
  },
});

export default CameraRollDemo;

🎨 实际应用场景

react-native-camera-roll 可以应用于以下实际场景:

  1. 保存用户上传的图片: 保存用户从网络下载的图片到相册
  2. 保存应用生成的图片: 保存应用内生成的截图、海报等
  3. 图片管理应用: 开发相册管理、图片编辑等应用
  4. 社交媒体: 保存用户发布的动态图片
  5. 电商应用: 保存商品图片到本地

⚠️ 注意事项与最佳实践

1. 权限配置

在使用 camera-roll 之前,需要在 HarmonyOS 的 module.json5 中配置权限:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.READ_IMAGEVIDEO",
        "reason": "$string:read_imagevideo_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.WRITE_IMAGEVIDEO",
        "reason": "$string:write_imagevideo_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

[!TIP] 部分接口(如 getPhotos、getAlbums、deletePhotos 等)由于 HarmonyOS 安全策略要求,需要申请受限权限才能使用。详细说明:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/restricted-permissions-V5

2. 错误处理

try {
  const asset = await CameraRoll.saveAsset(imageUrl);
  // 处理成功
} catch (error: any) {
  // 处理错误
  Alert.alert('错误', error.message || '保存失败');
}

3. HarmonyOS 特殊处理

在 HarmonyOS 上,需要注意:

  • 受限权限: 部分接口需要申请受限权限才能使用
  • 功能限制: getPhotos、getAlbums、deletePhotos 等接口受限
  • 格式支持: saveAsset 接口未完全支持所有格式
  • 安全策略: HarmonyOS 对相册访问有严格的安全策略

4. 最佳实践

// ✅ 推荐:使用 try-catch 处理错误
try {
  const asset = await CameraRoll.saveAsset(imageUrl);
  console.log('保存成功:', asset);
} catch (error) {
  console.error('保存失败:', error);
}

// ✅ 推荐:验证 URL 有效性
if (imageUrl && imageUrl.startsWith('http')) {
  await CameraRoll.saveAsset(imageUrl);
}

// ✅ 推荐:使用 saveAsset 代替已弃用的方法
// ❌ 不推荐
CameraRoll.save(imageUrl);
CameraRoll.saveToCameraRoll(imageUrl);

// ✅ 推荐
CameraRoll.saveAsset(imageUrl);

5. 性能优化

  • 避免频繁保存大量图片
  • 使用缩略图进行预览
  • 及时清理不需要的图片资源

🧪 测试验证

1. Android 平台测试

npm run android

测试要点:

  • 测试保存图片功能
  • 测试保存视频功能
  • 验证权限申请
  • 检查图片质量

2. iOS 平台测试

npm run ios

测试要点:

  • 测试保存图片功能
  • 测试相册访问
  • 验证权限流程
  • 检查图片元数据

3. HarmonyOS 平台测试

npm run harmony

测试要点:

  • 验证保存图片功能
  • 测试权限配置
  • 检查图片显示
  • 验证受限接口

4. 常见问题排查

问题 1: 保存失败

  • 检查权限配置
  • 确认 URL 有效性
  • 验证文件格式支持

问题 2: 权限拒绝

  • 检查权限配置是否正确
  • 确认用户已授权
  • 验证受限权限申请

问题 3: 图片格式不支持

  • 确认文件格式是否在支持列表中
  • 检查文件是否损坏
  • 尝试使用其他格式

📊 对比:原生相册 vs react-native-camera-roll

特性 原生相册 react-native-camera-roll
跨平台一致性 ✅ 完全一致
API 简洁性 ⚠️ 复杂 ✅ 简洁
保存功能 ✅ 完整 ✅ 完整
读取功能 ✅ 完整 ⚠️ 受限
删除功能 ✅ 完整 ❌ 未适配
格式支持 ✅ 广泛 ⚠️ 部分

📝 总结

通过集成 react-native-camera-roll,我们为项目添加了强大的相册管理能力。这个库提供了保存图片/视频到相册的核心功能,支持多种格式,并且完全跨平台兼容。

关键要点回顾

  • 安装依赖: npm install @react-native-ohos/camera-roll
  • 配置平台: 通过 har 包或直接链接源码,配置 CMakeLists.txt、PackageProvider.cpp、RNPackagesFactory.ts(仅 0.77 需要)
  • 集成代码: 使用 CameraRoll.saveAsset 保存图片/视频
  • 支持功能: 保存图片/视频、获取元数据等
  • 重要: HarmonyOS 部分接口受限,需要申请受限权限

实际效果

  • Android: 原生相册体验
  • iOS: 高质量的相册管理
  • HarmonyOS: 一致的保存体验

已知限制

  • ❌ getPhotos、getAlbums 接口受限(需要受限权限)
  • ❌ deletePhotos 功能暂未适配
  • ⚠️ 部分接口因安全策略无法使用
  • ⚠️ saveAsset 接口未完全支持所有格式

遗留问题

  • deletePhotos 删除图片/视频未实现 HarmonyOS 化
  • harmonyRefreshGallerySelection 无对应接口
  • 部分接口因安全策略无法使用
  • saveAsset 接口未完全支持

希望这篇教程能帮助你顺利集成 react-native-camera-roll,构建出色的相册管理体验!


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

Logo

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

更多推荐