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 - 图片尺寸 📐

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

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,目前仅支持相册选择功能。

相关推荐
六元七角八分2 小时前
学习笔记二《JavaScript 流程控制》
javascript·笔记
En^_^Joy2 小时前
JavaScript Web API:DOM操作全解析
开发语言·前端·javascript
wuhen_n2 小时前
回溯算法入门 - LeetCode经典回溯算法题
前端·javascript·算法
xcs194052 小时前
前端 vue this.$nextTick(() => {
前端·javascript·vue.js
yuki_uix3 小时前
渲染优化三件套:React.memo、useMemo、useCallback 的使用边界
前端·react.js
滕青山3 小时前
基于 pdf-lib 的图片转PDF工具核心JS实现
前端·javascript·vue.js
yuki_uix3 小时前
前端异步编程三板斧:从面试题到底层思维
前端·javascript
外派叙利亚3 小时前
uniapp 颜色卡条拖动
前端·javascript·uni-app
兆子龙3 小时前
React Fiber 架构与 Vue 响应式原理深度对比
前端·javascript