在这里插入图片描述

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

📋 前言

react-native-image-crop-picker 是 React Native 生态中最流行的图片选择与裁剪库,提供了强大的图片选择、相机拍照、裁剪编辑等功能。该库支持从相册选择图片、调用相机拍照、图片裁剪、压缩等多种操作,广泛应用于头像上传、图片编辑、相册管理等场景,是实现图片处理功能的首选库。

🎯 库简介

基本信息

  • 库名称: react-native-image-crop-picker
  • 版本信息:
    • RN 0.72: @react-native-ohos/react-native-image-crop-picker (0.40.5)
    • RN 0.77: @react-native-ohos/react-native-image-crop-picker (0.50.2)
    • RN 0.82: @react-native-ohos/react-native-image-crop-picker (0.51.2)
  • 官方仓库: https://gitcode.com/openharmony-sig/rntpc_react-native-image-crop-picker
  • 主要功能:
    • 从相册选择单张或多张图片
    • 调用相机拍照
    • 图片裁剪编辑
    • 图片压缩与质量调整
    • 支持 Base64 格式返回
    • 支持多种媒体类型(照片、视频)
    • 支持自由裁剪、圆形裁剪等
    • 自定义裁剪界面文本和颜色
    • 支持多选和最大数量限制
  • 兼容性验证:
    • 支持 HarmonyOS NEXT
    • 支持相机和相册权限管理
    • 完整的图片处理功能

为什么需要这个库?

  • 功能完整: 集成图片选择、拍照、裁剪于一体
  • 易于使用: API 简洁,集成方便
  • 高度可定制: 支持丰富的配置选项
  • 性能优异: 图片处理效率高
  • 跨平台: Android、iOS、HarmonyOS 完全兼容
  • 灵活裁剪: 支持多种裁剪模式和自定义样式

📦 安装步骤

1. 使用 npm 安装

npm install @react-native-ohos/react-native-image-crop-picker@0.40.5-rc.1

2. 使用 yarn 安装

yarn add @react-native-ohos/react-native-image-crop-picker

3. 验证安装

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

{
  "dependencies": {
    "@react-native-ohos/react-native-image-crop-picker": "0.40.5-rc.1",
    // ... 其他依赖
  }
}

🔧 HarmonyOS 平台配置 ⭐

重要说明

  • Autolink 支持: 版本 0.40.5 支持 Autolink(RN 0.72),版本 0.50.2 和 0.51.2 不支持 Autolink(RN 0.77 和 RN 0.82)
  • 导入库名: 使用 react-native-image-crop-picker 导入,编辑器不会报错
  • 权限要求: 需要在 module.json5 中配置相机和相册权限

手动配置步骤

1. 配置 overrides 字段

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

{
  "overrides": {
    "@rnoh/react-native-openharmony": "./react_native_openharmony"
  }
}
2. 引入原生端代码

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

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

"dependencies": {
  "@rnoh/react-native-openharmony": "file:../react_native_openharmony",
  "@react-native-ohos/react-native-image-crop-picker": "file:../../node_modules/@react-native-ohos/react-native-image-crop-picker/harmony/react_native_image_crop_picker.har"
}

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

cd entry
ohpm install

方法二:直接链接源码

<RN工程>/node_modules/@react-native-ohos/react-native-image-crop-picker/harmony 目录下的源码 image_crop_picker 复制到 harmony 工程根目录下。

harmony 工程根目录的 build-profile.json5 添加模块:

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

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

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

"dependencies": {
  "@rnoh/react-native-openharmony": "0.72.90",
  "@react-native-ohos/react-native-image-crop-picker": "file:../image_crop_picker"
}

点击 DevEco Studio 右上角的 sync 按钮。

3. 配置 CMakeLists 和引入 ImageCropPickerPackage

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

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

# RNOH_BEGIN: manual_package_linking_1

+ add_subdirectory("${OH_MODULES}/@react-native-ohos/react-native-image-crop-picker/src/main/cpp" ./image_crop_picker)
# RNOH_END: manual_package_linking_1

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

# RNOH_BEGIN: manual_package_linking_2
+ target_link_libraries(rnoh_app PUBLIC rnoh_image_crop_picker)
# RNOH_END: manual_package_linking_2

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

+ #include "ImageCropPickerPackage.h"

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<ImageCropPickerPackage>(ctx),
    };
}
4. 在 ArkTs 侧引入 ImageCropPickerPackage

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

+ import {ImageCropPickerPackage} from '@react-native-ohos/react-native-image-crop-picker/ts';

export function createRNPackages(ctx: RNPackageContext): RNPackage[] {
  return [
    new SamplePackage(ctx),
+   new ImageCropPickerPackage(ctx)
  ];
}
5. 配置权限

打开 entry/src/main/module.json5,添加相机和相册权限:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.CAMERA",
        "reason": "$string:camera_permission_reason",
        "usedScene": {
          "abilities": [
            "EntryAbility"
          ],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.READ_IMAGEVIDEO",
        "reason": "$string:read_media_permission_reason",
        "usedScene": {
          "abilities": [
            "EntryAbility"
          ],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.WRITE_IMAGEVIDEO",
        "reason": "$string:write_media_permission_reason",
        "usedScene": {
          "abilities": [
            "EntryAbility"
          ],
          "when": "inuse"
        }
      }
    ]
  }
}
6. 配置必要的 ImageEditAbility ⭐

[!TIP] 该模块的内容无法通过 autolink 自动生成,始终需要手动配置

(1) 创建 ImageEditAbility.ets

entry/src/main/ets/entryability 目录下创建 ImageEditAbility.ets 文件:

import UIAbility from '@ohos.app.ability.UIAbility';
import window from '@ohos.window';
import { BusinessError } from "@ohos.base";

const TAG = 'ImageEditAbility';

export default class ImageEditAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage) {
    this.setWindowOrientation(windowStage, window.Orientation.PORTRAIT);
    windowStage.loadContent('pages/ImageEdit', (err, data) => {
      let windowClass: window.Window = windowStage.getMainWindowSync();
      let isLayoutFullScreen = true;
      windowClass.setWindowLayoutFullScreen(isLayoutFullScreen).then(() => {
        console.info('Succeeded in setting the window layout to full-screen mode.');
      }).catch((err: BusinessError) => {
        console.error(`Failed to set the window layout to full-screen mode. Code is ${err.code}, message is ${err.message}`);
      });

      let type = window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR;
      let avoidArea = windowClass.getWindowAvoidArea(type);
      let bottomRectHeight = avoidArea.bottomRect.height; // 获取到导航区域的高度
      AppStorage.setOrCreate('bottomRectHeight', bottomRectHeight);

      type = window.AvoidAreaType.TYPE_SYSTEM;
      avoidArea = windowClass.getWindowAvoidArea(type);
      let topRectHeight = avoidArea.topRect.height; // 获取状态栏区域高度
      AppStorage.setOrCreate('topRectHeight', topRectHeight);

      windowClass.on('avoidAreaChange', (data) => {
        if (data.type === window.AvoidAreaType.TYPE_SYSTEM) {
          let topRectHeight = data.area.topRect.height;
          AppStorage.setOrCreate('topRectHeight', topRectHeight);
        } else if (data.type == window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR) {
          let bottomRectHeight = data.area.bottomRect.height;
          AppStorage.setOrCreate('bottomRectHeight', bottomRectHeight);
        }
      });

      if (err.code) {
        console.info(TAG, 'Failed to load the content. Cause: %{public}s',
          JSON.stringify(err) ?? '');
        return;
      }
      console.info(TAG, 'Succeeded in loading the content');
    });
    try {
      windowStage.getMainWindowSync().setWindowLayoutFullScreen(true, (err) => {
        if (err.code) {
          console.error('Failed to enable the full-screen mode. Cause: ' + JSON.stringify(err));
          return;
        }
        console.info('Succeeded in enabling the full-screen mode.');
      });
    } catch (exception) {
      console.error('Failed to set the system bar to be invisible. Cause: ' + JSON.stringify(exception));
    }
  }

  setWindowOrientation(stage: window.WindowStage, orientation: window.Orientation): void {
    console.info(TAG, "into setWindowOrientation :");
    if (!stage || !orientation) {
      return;
    }
    stage.getMainWindow().then(windowInstance => {
      windowInstance.setPreferredOrientation(orientation);
    });
  }

  onBackground() {
    this.context.terminateSelf();
  }
}

(2) 在 module.json5 中注册 ImageEditAbility

打开 entry/src/main/module.json5,在 abilities 数组中添加:

{
  "module": {
    "abilities": [
      // ... 其他 abilities
      {
        "name": "ImageEditAbility",
        "srcEntry": "./ets/entryability/ImageEditAbility.ets",
        "description": "$string:EntryAbility_desc",
        "icon": "$media:icon",
        "startWindowIcon": "$media:startIcon",
        "startWindowBackground": "$color:start_window_background",
        "removeMissionAfterTerminate": true
      }
    ]
  }
}

(3) 创建 ImageEdit.ets 页面

entry/src/main/ets/pages 目录下创建 ImageEdit.ets 文件:

import { ImageEditInfo } from '@react-native-ohos/react-native-image-crop-picker';

@Entry
@Component
struct ImageEdit {
  @State cropperCircleOverlay: boolean = false;

  aboutToAppear(): void {
    this.cropperCircleOverlay = AppStorage.Get('cropperCircleOverlay') || false;
  }

  build() {
    Row() {
      Column() {
        if (!this.cropperCircleOverlay) {
          ImageEditInfo();
        }
      }
      .width('100%');
    }
    .height('100%');
  }
}

(4) 在 main_pages.json 中添加页面配置

打开 entry/src/main/resources/base/profile/main_pages.json,在 src 数组中添加:

{
  "src": [
    "pages/Index",
    "pages/ImageEdit"
  ]
}
7. 配置字符串资源

由于权限配置中使用了 $string 引用,需要在字符串资源文件中添加对应的字符串定义。

打开 entry/src/main/resources/base/element/string.json,添加以下内容:

{
  "string": [
    {
      "name": "camera_permission_reason",
      "value": "需要使用相机拍照功能,用于选择和编辑图片"
    },
    {
      "name": "read_media_permission_reason",
      "value": "需要读取相册中的图片,用于图片选择和编辑"
    },
    {
      "name": "write_media_permission_reason",
      "value": "需要保存编辑后的图片到相册"
    }
  ]
}

如果项目还支持其他语言(如英文),还需要在对应的语言资源文件中添加相同的字符串定义。例如,打开 entry/src/main/resources/en/element/string.json,添加:

{
  "string": [
    {
      "name": "camera_permission_reason",
      "value": "Need to use camera to take photos for image selection and editing"
    },
    {
      "name": "read_media_permission_reason",
      "value": "Need to read images from gallery for image selection and editing"
    },
    {
      "name": "write_media_permission_reason",
      "value": "Need to save edited images to gallery"
    }
  ]
}

💻 完整代码示例

下面展示了 react-native-image-crop-picker 的完整使用场景,包括从相册选择、拍照、裁剪等多种功能:

import React, { useState } from 'react';
import {
  StyleSheet,
  ScrollView,
  View,
  Text,
  TouchableOpacity,
  Image,
  Alert,
  SafeAreaView,
} from 'react-native';
import ImagePicker, {
  Image as ImageType,
  Options,
} from 'react-native-image-crop-picker';

/**
 * react-native-image-crop-picker 图片选择与裁剪示例
 * 功能演示:
 * 1. 从相册选择单张图片
 * 2. 从相册选择多张图片
 * 3. 调用相机拍照
 * 4. 裁剪指定图片
 * 5. 清理缓存图片
 */
const ImageCropPickerExample = () => {
  const [selectedImages, setSelectedImages] = useState<ImageType[]>([]);
  const [cropping, setCropping] = useState<boolean>(true);
  const [multiple, setMultiple] = useState<boolean>(false);
  const [maxFiles, setMaxFiles] = useState<number>(9);
  const [includeBase64, setIncludeBase64] = useState<boolean>(false);

  /**
   * 从相册选择图片
   */
  const openPicker = async () => {
    try {
      const options: Options = {
        cropping: cropping,
        multiple: multiple,
        maxFiles: maxFiles,
        includeBase64: includeBase64,
        mediaType: 'photo',
        compressImageQuality: 0.9,
        compressImageMaxWidth: 1920,
        compressImageMaxHeight: 1920,
        cropperToolbarTitle: '编辑图片',
        cropperChooseText: '确定',
        cropperCancelText: '取消',
        cropperChooseColor: '#2196F3',
        cropperCancelColor: '#FF5252',
        showCropGuidelines: true,
        showCropFrame: true,
        hideBottomControls: false,
        enableRotationGesture: true,
      };

      const images = await ImagePicker.openPicker(options);
      setSelectedImages(Array.isArray(images) ? images : [images]);
      Alert.alert('成功', `选择了 ${Array.isArray(images) ? images.length : 1} 张图片`);
    } catch (error: any) {
      if (error.code !== 'E_PICKER_CANCELLED') {
        Alert.alert('错误', error.message || '选择图片失败');
      }
    }
  };

  /**
   * 调用相机拍照
   */
  const openCamera = async () => {
    try {
      const options: Options = {
        cropping: cropping,
        includeBase64: includeBase64,
        mediaType: 'photo',
        compressImageQuality: 0.9,
        compressImageMaxWidth: 1920,
        compressImageMaxHeight: 1920,
        cropperToolbarTitle: '编辑图片',
        cropperChooseText: '确定',
        cropperCancelText: '取消',
        cropperChooseColor: '#2196F3',
        cropperCancelColor: '#FF5252',
        showCropGuidelines: true,
        showCropFrame: true,
        enableRotationGesture: true,
        useFrontCamera: false,
      };

      const image = await ImagePicker.openCamera(options);
      setSelectedImages([image]);
      Alert.alert('成功', '拍照成功');
    } catch (error: any) {
      if (error.code !== 'E_PICKER_CANCELLED') {
        Alert.alert('错误', error.message || '拍照失败');
      }
    }
  };

  /**
     * 裁剪指定图片
     */
    const openCropper = async (imagePath: string) => {
      try {
        const image = await ImagePicker.openCropper({
          path: imagePath,
          mediaType: 'photo',
          width: 300,
          height: 400,
          cropping: true,
          cropperToolbarTitle: '裁剪图片',
          cropperChooseText: '确定',
          cropperCancelText: '取消',
          cropperChooseColor: '#2196F3',
          cropperCancelColor: '#FF5252',
          showCropGuidelines: true,
          showCropFrame: true,
          enableRotationGesture: true,
          freeStyleCropEnabled: false,
          cropperCircleOverlay: false,
        });
      
        const updatedImages = selectedImages.map(img => 
          img.path === imagePath ? image : img
        );
        setSelectedImages(updatedImages);
        Alert.alert('成功', '裁剪成功');
      } catch (error: any) {
        if (error.code !== 'E_PICKER_CANCELLED') {
          Alert.alert('错误', error.message || '裁剪失败');
        }
      }
    };
  /**
   * 清理缓存图片
   */
  const cleanImages = async () => {
    try {
      await ImagePicker.clean();
      setSelectedImages([]);
      Alert.alert('成功', '缓存已清理');
    } catch (error: any) {
      Alert.alert('错误', error.message || '清理失败');
    }
  };

  /**
   * 删除单张图片
   */
  const removeImage = (index: number) => {
    const updatedImages = [...selectedImages];
    updatedImages.splice(index, 1);
    setSelectedImages(updatedImages);
  };

  /**
   * 清理单张图片缓存
   */
  const cleanSingleImage = async (imagePath: string) => {
    try {
      await ImagePicker.cleanSingle(imagePath);
      const updatedImages = selectedImages.filter(img => img.path !== imagePath);
      setSelectedImages(updatedImages);
      Alert.alert('成功', '图片缓存已清理');
    } catch (error: any) {
      Alert.alert('错误', error.message || '清理失败');
    }
  };

  return (
    <SafeAreaView style={styles.container}>
      <ScrollView style={styles.scrollView}>
        <View style={styles.header}>
          <Text style={styles.headerTitle}>图片选择与裁剪</Text>
          <Text style={styles.headerSubtitle}>
            react-native-image-crop-picker
          </Text>
        </View>

        {/* 设置区域 */}
        <View style={styles.settingsContainer}>
          <Text style={styles.sectionTitle}>设置选项</Text>
    
          <View style={styles.settingRow}>
            <Text style={styles.settingLabel}>启用裁剪</Text>
            <TouchableOpacity
              style={[
                styles.toggleButton,
                cropping ? styles.toggleButtonActive : styles.toggleButtonInactive,
              ]}
              onPress={() => setCropping(!cropping)}
            >
              <Text
                style={[
                  styles.toggleButtonText,
                  cropping ? styles.toggleButtonTextActive : styles.toggleButtonTextInactive,
                ]}
              >
                {cropping ? '开' : '关'}
              </Text>
            </TouchableOpacity>
          </View>

          <View style={styles.settingRow}>
            <Text style={styles.settingLabel}>多选模式</Text>
            <TouchableOpacity
              style={[
                styles.toggleButton,
                multiple ? styles.toggleButtonActive : styles.toggleButtonInactive,
              ]}
              onPress={() => setMultiple(!multiple)}
            >
              <Text
                style={[
                  styles.toggleButtonText,
                  multiple ? styles.toggleButtonTextActive : styles.toggleButtonTextInactive,
                ]}
              >
                {multiple ? '开' : '关'}
              </Text>
            </TouchableOpacity>
          </View>

          <View style={styles.settingRow}>
            <Text style={styles.settingLabel}>包含 Base64</Text>
            <TouchableOpacity
              style={[
                styles.toggleButton,
                includeBase64 ? styles.toggleButtonActive : styles.toggleButtonInactive,
              ]}
              onPress={() => setIncludeBase64(!includeBase64)}
            >
              <Text
                style={[
                  styles.toggleButtonText,
                  includeBase64 ? styles.toggleButtonTextActive : styles.toggleButtonTextInactive,
                ]}
              >
                {includeBase64 ? '开' : '关'}
              </Text>
            </TouchableOpacity>
          </View>

          {multiple && (
            <View style={styles.settingRow}>
              <Text style={styles.settingLabel}>最大数量: {maxFiles}</Text>
              <TouchableOpacity
                style={styles.counterButton}
                onPress={() => setMaxFiles(Math.max(1, maxFiles - 1))}
              >
                <Text style={styles.counterButtonText}>-</Text>
              </TouchableOpacity>
              <TouchableOpacity
                style={styles.counterButton}
                onPress={() => setMaxFiles(Math.min(9, maxFiles + 1))}
              >
                <Text style={styles.counterButtonText}>+</Text>
              </TouchableOpacity>
            </View>
          )}
        </View>

        {/* 操作按钮区域 */}
        <View style={styles.buttonsContainer}>
          <TouchableOpacity style={styles.button} onPress={openPicker}>
            <Text style={styles.buttonText}>📷 从相册选择</Text>
          </TouchableOpacity>

          <TouchableOpacity style={styles.button} onPress={openCamera}>
            <Text style={styles.buttonText}>📸 拍照</Text>
          </TouchableOpacity>

          <TouchableOpacity
            style={[styles.button, styles.buttonSecondary]}
            onPress={cleanImages}
          >
            <Text style={[styles.buttonText, styles.buttonSecondaryText]}>
              🗑️ 清理缓存
            </Text>
          </TouchableOpacity>
        </View>

        {/* 图片展示区域 */}
        <View style={styles.galleryContainer}>
          <Text style={styles.sectionTitle}>
            已选择图片 ({selectedImages.length})
          </Text>

          {selectedImages.length === 0 ? (
            <View style={styles.emptyState}>
              <Text style={styles.emptyStateText}>暂无图片</Text>
              <Text style={styles.emptyStateHint}>
                点击上方按钮选择或拍照
              </Text>
            </View>
          ) : (
            <View style={styles.gallery}>
              {selectedImages.map((image, index) => (
                <View key={index} style={styles.imageItem}>
                  <Image
                    source={{ uri: image.path }}
                    style={styles.galleryImage}
                    resizeMode="cover"
                  />
                  <View style={styles.imageActions}>
                    <TouchableOpacity
                      style={styles.imageActionButton}
                      onPress={() => openCropper(image.path)}
                    >
                      <Text style={styles.imageActionText}>✂️</Text>
                    </TouchableOpacity>
                    <TouchableOpacity
                      style={styles.imageActionButton}
                      onPress={() => removeImage(index)}
                    >
                      <Text style={styles.imageActionText}></Text>
                    </TouchableOpacity>
                  </View>
                  <Text style={styles.imageInfo}>
                    {image.width}x{image.height}
                  </Text>
                </View>
              ))}
            </View>
          )}
        </View>

        {/* 使用说明 */}
        <View style={styles.tipsContainer}>
          <Text style={styles.sectionTitle}>使用说明</Text>
          <Text style={styles.tipText}>• 从相册选择: 支持单选和多选</Text>
          <Text style={styles.tipText}>• 拍照功能: 支持前后摄像头切换</Text>
          <Text style={styles.tipText}>• 图片裁剪: 支持自由裁剪和圆形裁剪</Text>
          <Text style={styles.tipText}>• 图片压缩: 自动压缩,优化存储</Text>
          <Text style={styles.tipText}>• 缓存管理: 支持清理单张或全部缓存</Text>
        </View>
      </ScrollView>
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F5F5F5',
  },
  scrollView: {
    flex: 1,
  },
  header: {
    backgroundColor: '#2196F3',
    paddingVertical: 24,
    paddingHorizontal: 20,
    alignItems: 'center',
  },
  headerTitle: {
    fontSize: 24,
    fontWeight: 'bold',
    color: '#FFFFFF',
    marginBottom: 4,
  },
  headerSubtitle: {
    fontSize: 14,
    color: 'rgba(255, 255, 255, 0.9)',
  },
  settingsContainer: {
    backgroundColor: '#FFFFFF',
    margin: 16,
    borderRadius: 12,
    padding: 16,
    elevation: 2,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
  },
  sectionTitle: {
    fontSize: 18,
    fontWeight: '600',
    color: '#333333',
    marginBottom: 12,
  },
  settingRow: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    paddingVertical: 12,
    borderBottomWidth: 1,
    borderBottomColor: '#F0F0F0',
  },
  settingLabel: {
    fontSize: 16,
    color: '#333333',
  },
  toggleButton: {
    paddingHorizontal: 20,
    paddingVertical: 8,
    borderRadius: 20,
    minWidth: 60,
    alignItems: 'center',
  },
  toggleButtonActive: {
    backgroundColor: '#2196F3',
  },
  toggleButtonInactive: {
    backgroundColor: '#E0E0E0',
  },
  toggleButtonText: {
    fontSize: 14,
    fontWeight: '600',
  },
  toggleButtonTextActive: {
    color: '#FFFFFF',
  },
  toggleButtonTextInactive: {
    color: '#666666',
  },
  counterButton: {
    width: 32,
    height: 32,
    borderRadius: 16,
    backgroundColor: '#2196F3',
    alignItems: 'center',
    justifyContent: 'center',
    marginLeft: 8,
  },
  counterButtonText: {
    fontSize: 18,
    fontWeight: 'bold',
    color: '#FFFFFF',
  },
  buttonsContainer: {
    paddingHorizontal: 16,
    marginBottom: 16,
  },
  button: {
    backgroundColor: '#2196F3',
    paddingVertical: 14,
    borderRadius: 12,
    alignItems: 'center',
    marginBottom: 12,
    elevation: 2,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
  },
  buttonSecondary: {
    backgroundColor: '#FFFFFF',
    borderWidth: 2,
    borderColor: '#2196F3',
  },
  buttonText: {
    fontSize: 16,
    fontWeight: '600',
    color: '#FFFFFF',
  },
  buttonSecondaryText: {
    color: '#2196F3',
  },
  galleryContainer: {
    backgroundColor: '#FFFFFF',
    margin: 16,
    borderRadius: 12,
    padding: 16,
    elevation: 2,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
  },
  emptyState: {
    alignItems: 'center',
    paddingVertical: 40,
  },
  emptyStateText: {
    fontSize: 16,
    color: '#999999',
    marginBottom: 8,
  },
  emptyStateHint: {
    fontSize: 14,
    color: '#CCCCCC',
  },
  gallery: {
    flexDirection: 'row',
    flexWrap: 'wrap',
    marginHorizontal: -6,
  },
  imageItem: {
    width: '48%',
    aspectRatio: 1,
    margin: '1%',
    borderRadius: 8,
    overflow: 'hidden',
    backgroundColor: '#F0F0F0',
  },
  galleryImage: {
    width: '100%',
    height: '100%',
  },
  imageActions: {
    position: 'absolute',
    top: 8,
    right: 8,
    flexDirection: 'row',
  },
  imageActionButton: {
    width: 32,
    height: 32,
    borderRadius: 16,
    backgroundColor: 'rgba(0, 0, 0, 0.6)',
    alignItems: 'center',
    justifyContent: 'center',
    marginLeft: 8,
  },
  imageActionText: {
    fontSize: 16,
  },
  imageInfo: {
    position: 'absolute',
    bottom: 0,
    left: 0,
    right: 0,
    backgroundColor: 'rgba(0, 0, 0, 0.6)',
    color: '#FFFFFF',
    fontSize: 12,
    padding: 4,
    textAlign: 'center',
  },
  tipsContainer: {
    backgroundColor: '#FFFFFF',
    margin: 16,
    borderRadius: 12,
    padding: 16,
    elevation: 2,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    marginBottom: 32,
  },
  tipText: {
    fontSize: 14,
    color: '#666666',
    lineHeight: 22,
    marginBottom: 4,
  },
});

export default ImageCropPickerExample;

🎨 实际应用场景

react-native-image-crop-picker 可以应用于以下实际场景:

  1. 头像上传: 用户选择或拍摄头像,支持裁剪
  2. 图片编辑: 裁剪、压缩图片,优化存储
  3. 相册管理: 多选图片,批量操作
  4. 证件上传: 拍摄证件照,自动裁剪
  5. 商品图片: 电商应用中商品图片选择和编辑
  6. 社交分享: 选择图片分享到社交平台

⚠️ 注意事项与最佳实践

1. 权限配置

// ✅ 推荐: 在 module.json5 中正确配置权限
{
  "requestPermissions": [
    {
      "name": "ohos.permission.CAMERA",
      "reason": "需要使用相机拍照功能",
      "usedScene": {
        "abilities": ["EntryAbility"],
        "when": "inuse"
      }
    },
    {
      "name": "ohos.permission.READ_IMAGEVIDEO",
      "reason": "需要读取相册中的图片",
      "usedScene": {
        "abilities": ["EntryAbility"],
        "when": "inuse"
      }
    },
    {
      "name": "ohos.permission.WRITE_IMAGEVIDEO",
      "reason": "需要保存编辑后的图片",
      "usedScene": {
        "abilities": ["EntryAbility"],
        "when": "inuse"
      }
    }
  ]
}

2. 图片选择

// ✅ 推荐: 从相册选择图片
const pickImage = async () => {
  try {
    const image = await ImagePicker.openPicker({
      mediaType: 'photo',
      cropping: true,
      compressImageQuality: 0.9,
      cropperToolbarTitle: '编辑图片',
      cropperChooseText: '确定',
      cropperCancelText: '取消',
    });
    console.log('选择的图片:', image.path);
  } catch (error) {
    console.error('选择图片失败:', error);
  }
};

3. 多选图片

// ✅ 推荐: 多选图片
const pickMultipleImages = async () => {
  try {
    const images = await ImagePicker.openPicker({
      multiple: true,
      maxFiles: 9,
      mediaType: 'photo',
      compressImageQuality: 0.9,
    });
    console.log('选择的图片数量:', images.length);
  } catch (error) {
    console.error('选择图片失败:', error);
  }
};

4. 相机拍照

// ✅ 推荐: 调用相机拍照
const takePhoto = async () => {
  try {
    const image = await ImagePicker.openCamera({
      mediaType: 'photo',
      cropping: true,
      useFrontCamera: false,
      compressImageQuality: 0.9,
      cropperToolbarTitle: '编辑图片',
    });
    console.log('拍照成功:', image.path);
  } catch (error) {
    console.error('拍照失败:', error);
  }
};

5. 图片裁剪

// ✅ 推荐: 裁剪指定图片
const cropImage = async (imagePath: string) => {
  try {
    const image = await ImagePicker.openCropper({
      path: imagePath,
      width: 300,
      height: 400,
      cropping: true,
      cropperCircleOverlay: false,
      freeStyleCropEnabled: false,
    });
    console.log('裁剪成功:', image.path);
  } catch (error) {
    console.error('裁剪失败:', error);
  }
};

6. 缓存管理

// ✅ 推荐: 清理缓存
const cleanCache = async () => {
  try {
    // 清理所有缓存
    await ImagePicker.clean();
  
    // 清理单张图片缓存
    await ImagePicker.cleanSingle(imagePath);
  } catch (error) {
    console.error('清理缓存失败:', error);
  }
};

7. HarmonyOS 特殊处理

在 HarmonyOS 上使用时,需要注意:

  • Autolink 支持: 版本 0.40.5 支持(RN 0.72),版本 0.50.2 和 0.51.2 不支持(RN 0.77/0.82)
  • 权限配置: 必须在 module.json5 中配置相机和相册权限
  • 导入库名: 使用 react-native-image-crop-picker 导入
  • 需要 Codegen: 使用前需要执行 Codegen 生成桥接代码

8. 最佳实践

// ✅ 推荐: 封装图片选择工具类
class ImagePickerHelper {
  // 选择单张图片
  static async pickSingleImage(options?: Options): Promise<ImageType | null> {
    try {
      const defaultOptions: Options = {
        mediaType: 'photo',
        cropping: true,
        compressImageQuality: 0.9,
        cropperToolbarTitle: '编辑图片',
        cropperChooseText: '确定',
        cropperCancelText: '取消',
        ...options,
      };
      const image = await ImagePicker.openPicker(defaultOptions);
      return image;
    } catch (error) {
      console.error('选择图片失败:', error);
      return null;
    }
  }

  // 选择多张图片
  static async pickMultipleImages(maxFiles: number = 9): Promise<ImageType[]> {
    try {
      const images = await ImagePicker.openPicker({
        multiple: true,
        maxFiles: maxFiles,
        mediaType: 'photo',
        compressImageQuality: 0.9,
      });
      return Array.isArray(images) ? images : [images];
    } catch (error) {
      console.error('选择图片失败:', error);
      return [];
    }
  }

  // 拍照
  static async takePhoto(options?: Options): Promise<ImageType | null> {
    try {
      const defaultOptions: Options = {
        mediaType: 'photo',
        cropping: true,
        useFrontCamera: false,
        compressImageQuality: 0.9,
        cropperToolbarTitle: '编辑图片',
        ...options,
      };
      const image = await ImagePicker.openCamera(defaultOptions);
      return image;
    } catch (error) {
      console.error('拍照失败:', error);
      return null;
    }
  }

  // 裁剪图片
  static async cropImage(imagePath: string, width: number = 300, height: number = 400): Promise<ImageType | null> {
    try {
      const image = await ImagePicker.openCropper({
        path: imagePath,
        mediaType: 'photo',
        width: width,
        height: height,
        cropping: true,
      });
      return image;
    } catch (error) {
      console.error('裁剪失败:', error);
      return null;
    }
  }
}

🧪 测试验证

1. Android 平台测试

npm run android

测试要点:

  • 测试相册选择
  • 验证相机拍照
  • 测试图片裁剪
  • 检查多选功能
  • 验证缓存清理

2. iOS 平台测试

npm run ios

测试要点:

  • 测试基本功能
  • 验证权限请求
  • 检查裁剪效果
  • 测试图片压缩

3. HarmonyOS 平台测试

npm run harmony

测试要点:

  • 验证 Codegen 配置
  • 测试权限配置
  • 检查基本功能
  • 验证裁剪效果

4. 常见问题排查

问题 1: 无法打开相册

  • 检查是否配置了 ohos.permission.READ_IMAGEVIDEO 权限
  • 确认权限的 usedScene 配置正确
  • 验证应用是否已授予权限

问题 2: 无法调用相机

  • 检查是否配置了 ohos.permission.CAMERA 权限
  • 确认相机权限的 reason 描述
  • 验证设备是否有摄像头

问题 3: 裁剪功能异常

  • 检查 cropping 参数是否设置为 true
  • 确认裁剪相关的配置参数
  • 验证图片路径是否正确

问题 4: 多选功能不生效

  • 检查 multiple 参数是否设置为 true
  • 确认 maxFiles 参数配置
  • 验证返回的数据类型

📊 API 参考

核心方法

方法 描述 参数类型 返回类型
openPicker() 打开相册选择 Options Promise <Image | Image[]>
openCamera() 打开相机拍照 Options Promise <Image>
openCropper() 打开裁剪编辑 Options Promise <Image>
clean() 清理所有缓存 - Promise <void>
cleanSingle() 清理单张图片缓存 string Promise <void>

Options 配置项

参数 类型 默认值 描述
mediaType string ‘any’ 媒体类型:photo/video
cropping boolean false 是否启用裁剪
multiple boolean false 是否支持多选
maxFiles number 5 最大选择数量
minFiles number 1 最小选择数量
includeBase64 boolean false 是否包含 Base64 数据
compressImageQuality number 1 图片压缩质量(0-1)
compressImageMaxWidth number - 最大宽度
compressImageMaxHeight number - 最大高度
cropperToolbarTitle string - 裁剪标题栏文本
cropperChooseText string - 确定按钮文本
cropperCancelText string - 取消按钮文本
cropperChooseColor string - 确定按钮颜色
cropperCancelColor string - 取消按钮颜色
showCropGuidelines boolean true 显示裁剪辅助线
showCropFrame boolean true 显示裁剪框
freeStyleCropEnabled boolean false 自由裁剪
cropperCircleOverlay boolean false 圆形裁剪
enableRotationGesture boolean true 启用旋转手势
useFrontCamera boolean false 是否使用前置相机

Image 返回数据

属性 类型 描述
path string 图片文件路径
width number 图片宽度
height number 图片高度
size number 文件大小
mime string MIME 类型
data string Base64 数据
modificationDate string 修改日期
source string 图片来源

📝 总结

通过集成 react-native-image-crop-picker,我们为项目添加了完整的图片选择与裁剪功能。这个库功能强大、易于使用,支持从相册选择、相机拍照、图片裁剪等多种操作,广泛应用于头像上传、图片编辑、相册管理等场景,是实现图片处理功能的首选库。

关键要点回顾

  • 安装依赖: npm install @react-native-ohos/react-native-image-crop-picker
  • 配置平台: 版本 0.40.5 支持 Autolink(RN 0.72),版本 0.50.2 和 0.51.2 不支持(RN 0.77/0.82)
  • Codegen: 需要执行生成三方库桥接代码
  • 导入库名: 使用 react-native-image-crop-picker 导入
  • 权限配置: 必须在 module.json5 中配置相机和相册权限
  • 功能完整: 支持相册选择、相机拍照、图片裁剪、缓存管理
  • 高度可定制: 支持丰富的配置选项

实际效果

  • Android: 完整支持,功能丰富
  • iOS: 完整支持,功能丰富
  • HarmonyOS: 完整支持,需要配置权限
Logo

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

更多推荐