ReactNative项目Openharmony三方库集成实战:@react-native-ohos/react-native-image-picker

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

📋 前言

在移动应用开发中,图片选择和拍照是最常见的功能之一。无论是用户头像上传、图片分享、还是证件照采集,都需要可靠的图片选择组件。@react-native-ohos/react-native-image-picker 是 React Native 社区广泛使用的图片选择库,支持从相册选择图片/视频,提供了丰富的配置选项和回调数据,是 RN 项目中图片处理的首选方案。

🎯 库简介

基本信息

  • 库名称 : @react-native-ohos/react-native-image-picker
  • 版本信息 :
    • 7.0.4: 支持 RN 0.72 版本
    • 8.2.2: 支持 RN 0.77 版本
  • 官方仓库: https://github.com/react-native-oh-library/react-native-image-picker
  • 主要功能 :
    • 🖼️ 从相册选择图片或视频
    • 📊 支持多选和单选模式
    • 🔧 丰富的配置选项
    • 📦 返回详细的媒体信息

为什么需要 Image Picker?

特性 手动实现 Image Picker
相册访问 ❌ 需原生代码 ✅ 统一API
多选支持 ⚠️ 复杂实现 ✅ 配置即可
媒体信息 ❌ 需额外处理 ✅ 自动返回
Base64转换 ❌ 需手动实现 ✅ 一键开启
HarmonyOS支持 ❌ 不支持 ✅ 完整支持

⚠️ 已知限制

功能 状态 说明
相机拍照 ❌ 暂不支持 HarmonyOS 相机功能暂未适配
相机录制视频 ❌ 暂不支持 HarmonyOS 相机功能暂未适配
相册选择 ✅ 支持 完整支持图片和视频选择

兼容性验证

在以下环境验证通过:

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

📦 安装步骤

1. 使用 npm 安装

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

bash 复制代码
# RN 0.72 版本推荐使用
npm install @react-native-ohos/react-native-image-picker@7.0.4-rc.1

# 或者使用 yarn
yarn add @react-native-ohos/react-native-image-picker@7.0.4-rc.1

2. 验证安装

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

json 复制代码
{
  "dependencies": {
    "@react-native-ohos/react-native-image-picker": "7.0.4-rc.1",
    // ... 其他依赖
  }
}

🔧 HarmonyOS 平台配置 ⭐

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

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

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

json5 复制代码
{
  // ... 其他配置
  "overrides": {
    "@rnoh/react-native-openharmony": "0.72.90"
  }
}

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

HAR 包引入方式简单快捷,适合大多数场景。我使用这种方式进行引入,比较方便

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

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

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

json5 复制代码
"dependencies": {
  "@rnoh/react-native-openharmony": "0.72.90",
  + "@react-native-ohos/react-native-image-picker": "file:../../node_modules/@react-native-ohos/react-native-image-picker/harmony/image_picker.har"
}

2.2 同步依赖

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

bash 复制代码
cd harmony/entry
ohpm install

2.3 配置 CMakeLists.txt

打开 harmony/entry/src/main/cpp/CMakeLists.txt,添加以下配置(只需要添加带+号的):

cmake 复制代码
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)

# 添加 ImagePicker 模块(HAR方式)
+ add_subdirectory("${OH_MODULES}/@react-native-ohos/react-native-image-picker/src/main/cpp" ./image_picker)

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)

# 链接 ImagePicker 库
+ target_link_libraries(rnoh_app PUBLIC rnoh_image_picker)

2.4 修改 PackageProvider.cpp

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

cpp 复制代码
#include "RNOH/PackageProvider.h"
#include "generated/RNOHGeneratedPackage.h"
+ #include "RNImagePickerPackage.h"

using namespace rnoh;

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

2.5 在 ArkTs 侧引入 ImagePickerViewPackage

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

typescript 复制代码
import type { RNPackageContext, RNPackage } from 'rnoh/ts';
+ import { ImagePickerViewPackage } from '@react-native-ohos/react-native-image-picker/ts';

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

方式二:源码引入 📁

源码引入方式适合需要调试或修改原生代码的场景。

2.1 复制源码到 harmony 工程根目录

把 <RN工程>/node_modules/@react-native-ohos/react-native-image-picker/harmony 目录下的源码 image_picker 复制到 harmony(鸿蒙壳工程)工程根目录下。

2.2 在 build-profile.json5 添加模块

打开 harmony/build-profile.json5,添加以下模块:

json5 复制代码
modules: [
  // ... 其他模块
  + {
  +   name: 'image_picker',
  +   srcPath: './image_picker',
  + }
]

💡 提示 :如果存在 build-profile.template.json5 文件,也需要同步添加上述模块配置。

2.3 修改 image_picker/oh-package.json5

打开 harmony/image_picker/oh-package.json5,修改 react-native-openharmony 版本与项目版本一致:

json5 复制代码
{
  "dependencies": {
    "@rnoh/react-native-openharmony": "0.72.90"
  }
}

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

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

json5 复制代码
"dependencies": {
  "@rnoh/react-native-openharmony": "0.72.90",
  + "@react-native-ohos/react-native-image-picker": "file:../image_picker"
}

2.5 修改ts文件后缀

如果 harmony/image_picker/ts文件的后缀是ts,调整为ets

2.6 同步依赖

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

bash 复制代码
cd harmony/entry
ohpm install

2.6 配置 CMakeLists.txt

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

cmake 复制代码
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)

# 添加 ImagePicker 模块(源码方式)
+ add_subdirectory("${OH_MODULES}/@react-native-ohos/react-native-image-picker/src/main/cpp" ./image_picker)

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)

# 链接 ImagePicker 库
+ target_link_libraries(rnoh_app PUBLIC rnoh_image_picker)

2.7 修改 PackageProvider.cpp

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

cpp 复制代码
#include "RNOH/PackageProvider.h"
#include "generated/RNOHGeneratedPackage.h"
+ #include "RNImagePickerPackage.h"

using namespace rnoh;

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

2.8 在 ArkTs 侧引入 ImagePickerViewPackage

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

typescript 复制代码
import type { RNPackageContext, RNPackage } from 'rnoh/ts';
+ import { ImagePickerViewPackage } from '@react-native-ohos/react-native-image-picker/ts';

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

3. 同步并运行

点击 DevEco Studio 右上角的 sync 按钮,然后编译运行即可。

📖 API 详解

🔷 静态方法

Image Picker 提供两个核心静态方法:

方法 说明 HarmonyOS 支持
launchCamera() 调用相机拍照或录制视频 ❌ 暂不支持
launchImageLibrary() 从相册选择图片或视频 ✅
typescript 复制代码
import { launchCamera, launchImageLibrary } from 'react-native-image-picker';

// ⚠️ 调用相机 - HarmonyOS 暂不支持
// launchCamera(options, callback);

// ✅ 打开相册 - HarmonyOS 支持
launchImageLibrary(options, callback);

🔷 Options 配置属性

Options 对象用于配置图片选择器的行为,以下是各属性的详细应用说明。

1. mediaType - 媒体类型 ⭐

mediaType 属性用于指定选择器支持的媒体类型,是最常用的配置项。

typescript 复制代码
mediaType: 'photo' | 'video' | 'mixed';
值 说明 HarmonyOS 支持
photo 仅选择图片 ✅
video 仅选择视频 ✅
mixed 图片和视频均可 ✅

应用场景:

typescript 复制代码
import { launchImageLibrary } from 'react-native-image-picker';

// 场景1:选择头像(仅图片)
const selectAvatar = () => {
  launchImageLibrary(
    { mediaType: 'photo', selectionLimit: 1 },
    (response) => {
      if (response.assets?.[0]) {
        setAvatar(response.assets[0].uri);
      }
    }
  );
};

// 场景2:选择视频
const selectVideo = () => {
  launchImageLibrary(
    { mediaType: 'video', selectionLimit: 1 },
    (response) => {
      if (response.assets?.[0]) {
        setVideoUri(response.assets[0].uri);
      }
    }
  );
};

// 场景3:图片或视频均可
const selectMedia = () => {
  launchImageLibrary(
    { mediaType: 'mixed', selectionLimit: 9 },
    (response) => {
      if (response.assets) {
        setMediaList(response.assets);
      }
    }
  );
};
2. selectionLimit - 选择数量限制 📊

selectionLimit 属性用于控制可选择的媒体数量。

typescript 复制代码
selectionLimit: number;  // 默认值为 1
值 说明 HarmonyOS 支持
1 单选模式(默认) ✅
n 最多选择 n 张 ✅
0 不限制数量(HarmonyOS 最多 50 张) ✅

应用场景:

typescript 复制代码
import { launchImageLibrary } from 'react-native-image-picker';

// 场景1:单选头像
const selectSingleImage = () => {
  launchImageLibrary(
    { mediaType: 'photo', selectionLimit: 1 },
    handleResponse
  );
};

// 场景2:最多选择 3 张图片
const selectUpToThree = () => {
  launchImageLibrary(
    { mediaType: 'photo', selectionLimit: 3 },
    handleResponse
  );
};

// 场景3:多选模式(不限制)
const selectMultiple = () => {
  launchImageLibrary(
    { mediaType: 'photo', selectionLimit: 0 },
    handleResponse
  );
};
3. cameraType - 相机类型 📷 ⚠️

⚠️ HarmonyOS 暂不支持: 由于相机功能未适配,此属性在 HarmonyOS 上无效

cameraType 属性用于指定使用前置或后置摄像头。

typescript 复制代码
cameraType: 'back' | 'front';
值 说明 HarmonyOS 支持
back 后置摄像头(默认) ❌ 暂不支持
front 前置摄像头 ❌ 暂不支持

💡 提示 :此属性仅在 iOS/Android 平台的 launchCamera 方法中有效,HarmonyOS 暂不支持相机功能。

4. includeBase64 - Base64 编码 🔤

includeBase64 属性用于是否返回图片的 Base64 编码字符串。

typescript 复制代码
includeBase64: boolean;  // 默认为 false

应用场景:

typescript 复制代码
import { launchImageLibrary } from 'react-native-image-picker';

// 场景1:获取 Base64 用于上传
const selectWithBase64 = () => {
  launchImageLibrary(
    { mediaType: 'photo', selectionLimit: 1, includeBase64: true },
    (response) => {
      if (response.assets?.[0]) {
        const asset = response.assets[0];
        // 直接使用 base64 上传
        uploadImage(asset.base64);
      }
    }
  );
};

// 场景2:显示预览图
const showPreview = () => {
  launchImageLibrary(
    { mediaType: 'photo', includeBase64: true },
    (response) => {
      if (response.assets?.[0]?.base64) {
        setImageSource(`data:image/jpeg;base64,${response.assets[0].base64}`);
      }
    }
  );
};

⚠️ 注意:大图片使用 Base64 会影响性能,建议仅在必要时开启。


🔷 Response 回调对象

Response 对象包含选择器的返回结果,以下是各属性的详细应用说明。

1. didCancel - 用户取消 🚫

didCancel 属性表示用户是否取消了选择操作。

typescript 复制代码
didCancel: boolean;

应用场景:

typescript 复制代码
import { launchImageLibrary } from 'react-native-image-picker';

const selectImage = () => {
  launchImageLibrary({ mediaType: 'photo' }, (response) => {
    if (response.didCancel) {
      console.log('用户取消了选择');
      return;
    }
    // 处理选中的图片
  });
};
2. assets - 媒体资源数组 🖼️

assets 属性是包含所有选中媒体的数组,每个元素是一个 Asset 对象。

typescript 复制代码
assets: Asset[];

应用场景:

typescript 复制代码
import { launchImageLibrary } from 'react-native-image-picker';

const selectImages = () => {
  launchImageLibrary(
    { mediaType: 'photo', selectionLimit: 0 },
    (response) => {
      if (response.assets && response.assets.length > 0) {
        // 获取所有选中图片的 URI
        const uris = response.assets.map(asset => asset.uri);
        setImageUris(uris);
  
        // 显示选中数量
        console.log(`已选择 ${response.assets.length} 张图片`);
      }
    }
  );
};

🔷 Asset 资源对象

Asset 对象包含单个媒体资源的详细信息,以下是各属性的详细应用说明。

1. uri - 文件路径 📁

uri 属性是选中媒体的文件路径,是最常用的属性。

typescript 复制代码
uri: string;

应用场景:

typescript 复制代码
import { launchImageLibrary } from 'react-native-image-picker';
import { Image } from 'react-native';

// 场景1:显示选中的图片
const ImagePickerDemo = () => {
  const [imageUri, setImageUri] = useState<string | null>(null);

  const selectImage = () => {
    launchImageLibrary({ mediaType: 'photo' }, (response) => {
      if (response.assets?.[0]?.uri) {
        setImageUri(response.assets[0].uri);
      }
    });
  };

  return (
    <View>
      {imageUri && (
        <Image source={{ uri: imageUri }} style={{ width: 200, height: 200 }} />
      )}
      <Button title="选择图片" onPress={selectImage} />
    </View>
  );
};

// 场景2:上传图片
const uploadSelectedImage = async () => {
  const response = await launchImageLibrary({ mediaType: 'photo' });
  if (response.assets?.[0]?.uri) {
    const formData = new FormData();
    formData.append('image', {
      uri: response.assets[0].uri,
      type: response.assets[0].type,
      name: response.assets[0].fileName,
    });
    await fetch('https://api.example.com/upload', {
      method: 'POST',
      body: formData,
    });
  }
};
2. width 和 height - 图片尺寸 📐

width 和 height 属性表示图片的宽度和高度(像素)。

typescript 复制代码
width: number;
height: number;

应用场景:

typescript 复制代码
import { launchImageLibrary } from 'react-native-image-picker';

// 场景1:检查图片尺寸是否符合要求
const selectImageWithSizeCheck = () => {
  launchImageLibrary({ mediaType: 'photo' }, (response) => {
    if (response.assets?.[0]) {
      const { width, height } = response.assets[0];
  
      // 检查最小尺寸
      if (width < 800 || height < 600) {
        Alert.alert('提示', '图片尺寸太小,请选择更高分辨率的图片');
        return;
      }
  
      // 检查宽高比
      const ratio = width / height;
      if (ratio < 0.9 || ratio > 1.1) {
        Alert.alert('提示', '请选择接近正方形的图片');
        return;
      }
  
      setImage(response.assets[0].uri);
    }
  });
};

// 场景2:根据尺寸计算显示大小
const calculateDisplaySize = (asset: Asset) => {
  const maxWidth = 300;
  const maxHeight = 300;
  
  let displayWidth = asset.width;
  let displayHeight = asset.height;
  
  if (displayWidth > maxWidth) {
    displayHeight = (maxWidth / displayWidth) * displayHeight;
    displayWidth = maxWidth;
  }
  
  if (displayHeight > maxHeight) {
    displayWidth = (maxHeight / displayHeight) * displayWidth;
    displayHeight = maxHeight;
  }
  
  return { width: displayWidth, height: displayHeight };
};
3. fileSize - 文件大小 📊

fileSize 属性表示文件大小(字节)。

typescript 复制代码
fileSize: number;

应用场景:

typescript 复制代码
import { launchImageLibrary } from 'react-native-image-picker';

// 场景1:检查文件大小限制
const selectImageWithSizeLimit = () => {
  const MAX_SIZE = 5 * 1024 * 1024; // 5MB

  launchImageLibrary({ mediaType: 'photo' }, (response) => {
    if (response.assets?.[0]) {
      const fileSize = response.assets[0].fileSize;
  
      if (fileSize && fileSize > MAX_SIZE) {
        Alert.alert('提示', `文件大小 ${(fileSize / 1024 / 1024).toFixed(2)}MB 超过限制 5MB`);
        return;
      }
  
      uploadImage(response.assets[0]);
    }
  });
};

// 场景2:显示文件大小
const formatFileSize = (bytes?: number) => {
  if (!bytes) return '未知';
  if (bytes < 1024) return `${bytes} B`;
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
  return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
};
4. type - 文件类型 📄

type 属性表示文件的 MIME 类型。

typescript 复制代码
type: string;

应用场景:

typescript 复制代码
import { launchImageLibrary } from 'react-native-image-picker';

// 场景1:根据类型处理不同媒体
const selectAndProcessMedia = () => {
  launchImageLibrary({ mediaType: 'mixed' }, (response) => {
    if (response.assets?.[0]) {
      const asset = response.assets[0];
  
      if (asset.type?.startsWith('image/')) {
        console.log('这是图片:', asset.type);
        processImage(asset);
      } else if (asset.type?.startsWith('video/')) {
        console.log('这是视频:', asset.type);
        processVideo(asset);
      }
    }
  });
};

// 场景2:上传时设置正确的 Content-Type
const uploadWithCorrectType = (asset: Asset) => {
  const formData = new FormData();
  formData.append('file', {
    uri: asset.uri,
    type: asset.type || 'image/jpeg',
    name: asset.fileName || 'upload.jpg',
  });
};
5. fileName - 文件名称 📝

fileName 属性表示文件的原始名称。

typescript 复制代码
fileName: string;

应用场景:

typescript 复制代码
import { launchImageLibrary } from 'react-native-image-picker';

// 场景1:显示文件名
const ImageInfo = () => {
  const [selectedFile, setSelectedFile] = useState<string>('');

  const selectImage = () => {
    launchImageLibrary({ mediaType: 'photo' }, (response) => {
      if (response.assets?.[0]) {
        setSelectedFile(response.assets[0].fileName || '未知文件');
      }
    });
  };

  return (
    <View>
      <Text>已选择: {selectedFile}</Text>
      <Button title="选择图片" onPress={selectImage} />
    </View>
  );
};

// 场景2:根据文件名判断类型
const getFileExtension = (fileName: string) => {
  return fileName.split('.').pop()?.toLowerCase();
};
6. originalPath - 原始路径 📂

originalPath 属性表示文件的原始完整路径。

typescript 复制代码
originalPath: string;

应用场景:

typescript 复制代码
import { launchImageLibrary } from 'react-native-image-picker';

// 场景:获取原始路径用于文件操作
const selectAndGetPath = () => {
  launchImageLibrary({ mediaType: 'photo' }, (response) => {
    if (response.assets?.[0]) {
      const originalPath = response.assets[0].originalPath;
      console.log('原始路径:', originalPath);
      // 可以用于文件复制、移动等操作
    }
  });
};
7. id - 本地标识符 🆔

id 属性是媒体在设备上的本地标识符。

typescript 复制代码
id: string;

应用场景:

typescript 复制代码
import { launchImageLibrary } from 'react-native-image-picker';

// 场景:用于去重或缓存
const selectedIds = new Set<string>();

const selectUniqueImages = () => {
  launchImageLibrary({ mediaType: 'photo', selectionLimit: 0 }, (response) => {
    if (response.assets) {
      const newAssets = response.assets.filter(asset => 
        asset.id && !selectedIds.has(asset.id)
      );
  
      newAssets.forEach(asset => {
        if (asset.id) selectedIds.add(asset.id);
      });
  
      console.log(`新增 ${newAssets.length} 张图片`);
    }
  });
};

🔷 属性支持情况总览

Options 配置属性
属性 说明 HarmonyOS 支持
mediaType 媒体类型 ✅
cameraType 相机类型 ❌ 暂不支持
includeBase64 Base64 编码 ✅
selectionLimit 选择数量限制 ✅
maxWidth 最大宽度 ❌
maxHeight 最大高度 ❌
quality 图片质量 ❌
videoQuality 视频质量 ❌
durationLimit 视频时长限制 ❌
includeExtra 额外信息 ❌
saveToPhotos 保存到相册 ❌
Response 对象属性
属性 说明 HarmonyOS 支持
didCancel 用户取消 ✅
errorCode 错误码 ❌
errorMessage 错误信息 ❌
assets 媒体数组 ✅
Asset 对象属性
属性 说明 HarmonyOS 支持
uri 文件路径 ✅
originalPath 原始路径 ✅
width 宽度 ✅
height 高度 ✅
fileSize 文件大小 ✅
type 文件类型 ✅
fileName 文件名 ✅
base64 Base64 编码 ✅
id 本地标识符 ✅
duration 视频时长 ❌
bitrate 视频比特率 ❌
timestamp 时间戳 ❌

💻 完整代码示例

下面是一个完整的示例,展示了 Image Picker 的所有 API 应用场景:

typescript 复制代码
import React, { useState } from 'react';
import {
  View,
  Text,
  StyleSheet,
  ScrollView,
  TouchableOpacity,
  Image,
  Alert,
  SafeAreaView,
} from 'react-native';
import {
  launchImageLibrary,
  ImagePickerResponse,
  Asset,
} from 'react-native-image-picker';

function ImagePickerDemo() {
  const [selectedImages, setSelectedImages] = useState<Asset[]>([]);
  const [lastResponse, setLastResponse] = useState<string>('');

  // 格式化文件大小
  const formatFileSize = (bytes?: number) => {
    if (!bytes) return '未知';
    if (bytes < 1024) return `${bytes} B`;
    if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
    return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
  };

  // 处理响应
  const handleResponse = (response: ImagePickerResponse) => {
    setLastResponse(JSON.stringify(response, null, 2));

    if (response.didCancel) {
      console.log('用户取消了选择');
      return;
    }

    if (response.errorCode) {
      Alert.alert('错误', response.errorMessage || '选择图片失败');
      return;
    }

    if (response.assets && response.assets.length > 0) {
      setSelectedImages(response.assets);
    }
  };

  // 1. 单选图片
  const selectSingleImage = () => {
    launchImageLibrary(
      {
        mediaType: 'photo',
        selectionLimit: 1,
      },
      handleResponse
    );
  };

  // 2. 多选图片(最多9张)
  const selectMultipleImages = () => {
    launchImageLibrary(
      {
        mediaType: 'photo',
        selectionLimit: 9,
      },
      handleResponse
    );
  };

  // 3. 选择图片并获取Base64
  const selectWithBase64 = () => {
    launchImageLibrary(
      {
        mediaType: 'photo',
        selectionLimit: 1,
        includeBase64: true,
      },
      (response) => {
        if (response.assets?.[0]) {
          const asset = response.assets[0];
          console.log('Base64长度:', asset.base64?.length);
          handleResponse(response);
        }
      }
    );
  };

  // 4. 选择视频
  const selectVideo = () => {
    launchImageLibrary(
      {
        mediaType: 'video',
        selectionLimit: 1,
      },
      handleResponse
    );
  };

  // 5. 选择图片或视频
  const selectMixed = () => {
    launchImageLibrary(
      {
        mediaType: 'mixed',
        selectionLimit: 5,
      },
      handleResponse
    );
  };

  // 6. 无限制选择
  const selectUnlimited = () => {
    launchImageLibrary(
      {
        mediaType: 'photo',
        selectionLimit: 0,
      },
      handleResponse
    );
  };

  // 清空选择
  const clearSelection = () => {
    setSelectedImages([]);
    setLastResponse('');
  };

  return (
    <SafeAreaView style={styles.container}>
      <ScrollView style={styles.scrollView}>
        <Text style={styles.title}>📷 Image Picker 演示</Text>

        {/* 功能按钮区 */}
        <View style={styles.section}>
          <Text style={styles.sectionTitle}>相册选择</Text>
          <View style={styles.buttonRow}>
            <TouchableOpacity style={styles.button} onPress={selectSingleImage}>
              <Text style={styles.buttonText}>单选图片</Text>
            </TouchableOpacity>
            <TouchableOpacity style={styles.button} onPress={selectMultipleImages}>
              <Text style={styles.buttonText}>多选(最多9张)</Text>
            </TouchableOpacity>
          </View>
          <View style={styles.buttonRow}>
            <TouchableOpacity style={styles.button} onPress={selectWithBase64}>
              <Text style={styles.buttonText}>获取Base64</Text>
            </TouchableOpacity>
            <TouchableOpacity style={styles.button} onPress={selectUnlimited}>
              <Text style={styles.buttonText}>无限制选择</Text>
            </TouchableOpacity>
          </View>
        </View>

        <View style={styles.section}>
          <Text style={styles.sectionTitle}>媒体类型</Text>
          <View style={styles.buttonRow}>
            <TouchableOpacity style={[styles.button, styles.buttonVideo]} onPress={selectVideo}>
              <Text style={styles.buttonText}>选择视频</Text>
            </TouchableOpacity>
            <TouchableOpacity style={[styles.button, styles.buttonMixed]} onPress={selectMixed}>
              <Text style={styles.buttonText}>图片+视频</Text>
            </TouchableOpacity>
          </View>
        </View>

        {/* 已选图片展示 */}
        {selectedImages.length > 0 && (
          <View style={styles.section}>
            <View style={styles.sectionHeader}>
              <Text style={styles.sectionTitle}>已选择 {selectedImages.length} 张</Text>
              <TouchableOpacity onPress={clearSelection}>
                <Text style={styles.clearText}>清空</Text>
              </TouchableOpacity>
            </View>
      
            <ScrollView horizontal showsHorizontalScrollIndicator={false}>
              {selectedImages.map((asset, index) => (
                <View key={`image-${index}-${asset.uri}`} style={styles.imageCard}>
                  <Image
                    source={{ uri: asset.uri }}
                    style={styles.thumbnail}
                    resizeMode="cover"
                  />
                  <View style={styles.imageInfo}>
                    <Text style={styles.imageName} numberOfLines={1}>
                      {asset.fileName}
                    </Text>
                    <Text style={styles.imageSize}>
                      {asset.width} × {asset.height}
                    </Text>
                    <Text style={styles.imageSize}>
                      {formatFileSize(asset.fileSize)}
                    </Text>
                    <Text style={styles.imageType}>
                      {asset.type}
                    </Text>
                  </View>
                </View>
              ))}
            </ScrollView>
          </View>
        )}

        {/* 响应数据 */}
        {lastResponse ? (
          <View style={styles.section}>
            <Text style={styles.sectionTitle}>响应数据</Text>
            <ScrollView style={styles.responseBox}>
              <Text style={styles.responseText}>{lastResponse}</Text>
            </ScrollView>
          </View>
        ) : null}
      </ScrollView>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f5f5f5',
  },
  scrollView: {
    flex: 1,
    padding: 16,
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    textAlign: 'center',
    marginBottom: 20,
    color: '#333',
  },
  section: {
    backgroundColor: '#fff',
    borderRadius: 12,
    padding: 16,
    marginBottom: 16,
  },
  sectionHeader: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: 12,
  },
  sectionTitle: {
    fontSize: 16,
    fontWeight: '600',
    color: '#333',
    marginBottom: 12,
  },
  clearText: {
    color: '#FF3B30',
    fontSize: 14,
  },
  buttonRow: {
    flexDirection: 'row',
    gap: 10,
    marginBottom: 10,
  },
  button: {
    flex: 1,
    backgroundColor: '#007AFF',
    paddingVertical: 12,
    borderRadius: 8,
    alignItems: 'center',
  },
  buttonVideo: {
    backgroundColor: '#34C759',
  },
  buttonMixed: {
    backgroundColor: '#FF9500',
  },
  buttonText: {
    color: '#fff',
    fontSize: 14,
    fontWeight: '600',
  },
  imageCard: {
    width: 160,
    marginRight: 12,
    backgroundColor: '#f8f9fa',
    borderRadius: 8,
    overflow: 'hidden',
  },
  thumbnail: {
    width: '100%',
    height: 120,
  },
  imageInfo: {
    padding: 8,
  },
  imageName: {
    fontSize: 12,
    fontWeight: '500',
    color: '#333',
  },
  imageSize: {
    fontSize: 11,
    color: '#666',
    marginTop: 2,
  },
  imageType: {
    fontSize: 10,
    color: '#999',
    marginTop: 2,
  },
  responseBox: {
    backgroundColor: '#f0f0f0',
    borderRadius: 8,
    padding: 12,
    maxHeight: 200,
  },
  responseText: {
    fontSize: 11,
    fontFamily: 'monospace',
    color: '#333',
  },
});

export default ImagePickerDemo;

⚠️ 注意事项与最佳实践

1. 最佳实践

typescript 复制代码
// ✅ 推荐:处理所有可能的响应情况
const selectImageSafely = () => {
  launchImageLibrary({ mediaType: 'photo' }, (response) => {
    if (response.didCancel) {
      return;
    }
    if (response.errorCode) {
      Alert.alert('错误', response.errorMessage || '未知错误');
      return;
    }
    if (response.assets?.[0]) {
      processImage(response.assets[0]);
    }
  });
};

// ✅ 推荐:检查文件大小后再上传
const checkAndUpload = (asset: Asset) => {
  const MAX_SIZE = 10 * 1024 * 1024; // 10MB
  if (asset.fileSize && asset.fileSize > MAX_SIZE) {
    Alert.alert('提示', '文件过大,请选择较小的图片');
    return;
  }
  uploadImage(asset);
};

// ⚠️ 注意:大图片使用 Base64 会影响性能
// 仅在必要时开启 includeBase64

2. 遗留问题

问题 状态 Issue
部分属性未实现 HarmonyOS 化 ⚠️ 待修复 issue#14
相机功能暂未适配 ⚠️ 待修复 issue#13

🧪 测试验证

1. 测试要点

  • 单选模式: 确认只能选择一张图片

  • 多选模式 : 确认可以选择多张图片

  • 媒体类型: 测试 photo、video、mixed 三种类型

  • Base64 : 确认 Base64 编码正确返回

  • 文件信息: 验证 width、height、fileSize 等属性

⚠️ 注意: 相机功能暂未适配,无法测试拍照功能。

2. 常见问题排查

问题 1: 无法打开相册

  • 检查是否配置了 READ_MEDIA 权限
  • 确认原生代码是否正确链接
  • 查看控制台错误日志

问题 2: 图片无法显示

  • 检查 uri 是否正确获取
  • 确认 Image 组件的 source 格式正确
  • 尝试使用 originalPath 替代 uri

问题 3: Base64 返回为空

  • 确认 includeBase64 设置为 true
  • 检查图片是否过大导致内存问题

⚠️ 重要提示: 相机功能(launchCamera)暂未适配 HarmonyOS,目前仅支持相册选择功能。

相关推荐
用户6602676118793 分钟前
HTTP 200,页面仍是旧状态:定位一次前后端字段错配
javascript
用户725264966296914 分钟前
浏览器为什么加载不了超大图片
前端·javascript
donk131422 分钟前
React Native for OpenHarmony 实战:三方库 react-native-step-indicator 的鸿蒙化适配指南
分布式·react native·react.js·wpf·harmonyos
OpsEye33 分钟前
企业 AI 治理到底管什么:预算、权限、会话、效能四个维度
javascript·ai编程
keyipatience2 小时前
JS学习笔记(2)数组,函数,作用域,对象
前端·javascript·笔记·学习·html
donk13142 小时前
React Native for OpenHarmony 实战:三方库 react-native-segmented-control-tab 的鸿蒙化适配指南
javascript·react native·react.js·华为·harmonyos
Geoffwo2 小时前
robotjs0.6.0鼠标中轴滚动失效修复方案
前端·javascript
溪语流沙3 小时前
【Web全栈进阶】Vue Router + Pinia:SPA路由与全局状态
前端·javascript·vue.js
兰亭妙微UI设计公司3 小时前
兰亭妙微软件界面设计公司的解锁按钮设计10大密码
开发语言·前端·javascript
u0111026754 小时前
WebP 转成 PNG 后为什么更大?图片格式转换不等于压缩
前端·javascript·vue.js