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

📌 开发环境声明:本文基于 React Native 0.72.90 版本进行开发适配


🚀 一、开篇引言

图片编辑是移动应用中常见的需求,无论是头像裁剪、图片压缩还是格式转换,都离不开图片处理能力。@react-native-community/image-editor 是 React Native 社区中轻量级的图片编辑组件,提供图片裁剪、缩放、压缩等核心功能。本文将带你深入了解如何在 HarmonyOS 平台上集成和使用这个实用的图片处理组件。

1.1 你将学到什么?

  • ✅ image-editor 的核心概念与工作原理
  • ✅ HarmonyOS 平台的完整集成流程
  • ✅ 图片裁剪与缩放处理
  • ✅ API 的深度解析
  • ✅ 实际应用场景的最佳实践

1.2 适用人群

  • 正在进行 React Native 鸿蒙化迁移的开发者
  • 需要实现图片裁剪功能的开发者
  • 对跨平台图片处理开发感兴趣的技术爱好者

1.3 为什么选择 image-editor?

特点 说明
轻量级 专注于图片裁剪,无冗余功能
跨平台一致 iOS、Android、HarmonyOS 表现一致
简单易用 API 极简,一个方法完成裁剪
功能实用 支持裁剪、缩放、压缩、格式转换
性能优秀 原生实现,处理速度快

📦 二、库概览

2.1 基本信息

项目 内容
库名称 @react-native-ohos/image-editor
原库名称 @react-native-community/image-editor
版本信息 3.2.1 (RN 0.72) / 4.3.1 (RN 0.77)
官方仓库 https://github.com/callstack/react-native-image-editor
鸿蒙仓库 https://gitcode.com/openharmony-sig/rntpc_react-native-image-editor
开源协议 MIT

2.2 版本兼容性

三方库版本 支持RN版本 是否支持Autolink
~4.3.1 0.77 No
~3.2.1 0.72 Yes
<=3.2.0-nc.0.1.3@deprecated 0.72 No

2.3 核心能力矩阵

能力项 描述 HarmonyOS 支持
图片裁剪 cropImage 方法 ✅ 完全支持
指定偏移量 offset 参数 ✅ 完全支持
指定尺寸 size 参数 ✅ 完全支持
缩放显示 displaySize 参数 ✅ 完全支持
质量压缩 quality 参数 ✅ 完全支持
格式转换 format 参数 ✅ 完全支持

2.4 技术架构图

原生平台层

Bridge Layer

React Native 应用层

ImageEditor Module

cropImage

Native Module

ImageEditorPackage

ImageEditorModule

Android
Bitmap

iOS
UIImage

HarmonyOS
PixelMap

2.5 典型应用场景

场景 描述 示例
头像裁剪 用户头像方形裁剪 👤 个人中心、账号设置
图片压缩 减小图片体积 📤 图片上传、节省流量
格式转换 PNG/JPEG 格式转换 🖼️ 格式统一、兼容处理
缩略图生成 生成指定尺寸缩略图 📋 列表展示、预览图

🔧 三、环境准备

3.1 安装依赖

在这里插入图片描述

在项目根目录执行以下命令:

npm install @react-native-ohos/image-editor@3.2.1-rc.1

或使用 yarn:

yarn add @react-native-ohos/image-editor@3.2.1-rc.1

3.2 验证安装

安装完成后,检查 package.json 文件中是否包含以下依赖:

{
  "dependencies": {
    "@react-native-ohos/image-editor": "^3.2.1-rc.1"
  }
}

⚙️ 四、原生配置

4.1 配置 oh-package.json5

打开 harmony/oh-package.json5,添加 overrides 配置(一定要根据自己项目的版本来配置):
在这里插入图片描述

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

4.2 配置 entry/oh-package.json5

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

{
  "dependencies": {
    "@react-native-ohos/image-editor": "file:../../node_modules/@react-native-ohos/image-editor/harmony/image_editor.har"
  }
}

4.3 配置 CMakeLists.txt

打开 harmony/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/image-editor/src/main/cpp" ./image-editor)
# RNOH_END: manual_package_linking_1

# RNOH_BEGIN: manual_package_linking_2
target_link_libraries(rnoh_app PUBLIC rnoh_image_editor)
# RNOH_END: manual_package_linking_2

4.4 配置 PackageProvider.cpp

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

#include "RNOH/PackageProvider.h"
#include "generated/RNOHGeneratedPackage.h"
#include "ReactNativeOhosReactNativeImageEditorPackage.h"

using namespace rnoh;

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

4.5 配置 RNPackagesFactory.ts

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

import { ImageEditorPackage } from '@react-native-ohos/image-editor/ts';

export function createRNPackages(ctx: RNPackageContext): RNPackage[] {
  return [
    new ImageEditorPackage(ctx)
  ];
}

4.6 同步依赖

在 DevEco Studio 中点击右上角的 sync 按钮,或在命令行执行:

cd harmony/entry
ohpm install

📖 五、API 详解

5.1 cropImage 方法

cropImage 是 image-editor 库的核心方法,用于裁剪指定 URI 的图片。该方法支持本地图片和网络图片,返回裁剪后图片的本地缓存路径。

方法签名:

static cropImage(uri: string, cropData: CropData): Promise<string>

参数说明:

参数名 类型 必填 说明
uri string 图片 URI,支持本地路径和网络 URL
cropData CropData 裁剪配置对象

返回值:

返回 Promise,成功时解析为裁剪后图片的本地缓存路径(string)。

使用示例:

import ImageEditor from "@react-native-community/image-editor";

const croppedImageURI = await ImageEditor.cropImage(
  imageUri,
  cropData
);

5.2 CropData 配置对象

CropData 是裁剪操作的核心配置对象,包含裁剪区域、输出尺寸、质量等参数。

offset - 裁剪偏移量

指定裁剪区域的左上角坐标,坐标系原点为原图左上角,x 轴向右为正,y 轴向下为正。

offset: {
  x: number,  // 水平偏移量(像素)
  y: number   // 垂直偏移量(像素)
}

示例:

offset: { x: 100, y: 100 }

这表示裁剪区域从原图坐标 (100, 100) 开始。

size - 裁剪尺寸

指定裁剪区域的宽度和高度,单位为像素。

size: {
  width: number,   // 裁剪宽度(像素)
  height: number   // 裁剪高度(像素)
}

示例:

size: { width: 300, height: 300 }

这表示裁剪一个 300x300 像素的正方形区域。

displaySize - 显示尺寸(可选)

指定裁剪后图片的最终显示尺寸。如果不设置,则保持裁剪区域的原始尺寸。

displaySize: {
  width: number,   // 输出宽度(像素)
  height: number   // 输出高度(像素)
}

示例:

displaySize: { width: 150, height: 150 }

这会将裁剪后的图片缩放到 150x150 像素。

resizeMode - 缩放模式(可选)

当设置了 displaySize 时,指定图片的缩放模式。

说明
‘cover’ 保持宽高比缩放,填满整个区域,可能裁剪内容
‘contain’ 保持宽高比缩放,完整显示内容,可能有留白

默认值: 'cover'

示例:

resizeMode: 'cover'
quality - 图片质量(可选)

指定输出图片的压缩质量,仅对 JPEG 格式有效。

值范围 说明
0.0 最低质量,最小文件体积
1.0 最高质量,最大文件体积

默认值: 0.9

示例:

quality: 0.8  // 80% 质量
format - 图片格式(可选)

指定输出图片的格式。

说明
‘jpeg’ JPEG 格式,适合照片类图片
‘png’ PNG 格式,适合透明背景图片

默认值: 根据输入图片格式自动判断,无法判断时使用 ‘jpeg’

示例:

format: 'jpeg'

5.3 完整 CropData 类型定义

interface CropData {
  offset: {
    x: number;
    y: number;
  };
  size: {
    width: number;
    height: number;
  };
  displaySize?: {
    width: number;
    height: number;
  };
  resizeMode?: 'cover' | 'contain';
  quality?: number;
  format?: 'jpeg' | 'png';
}

🎯 六、属性详解

属性名 描述 类型 必填 默认值 HarmonyOS 支持
offset 裁剪区域左上角坐标 object -
size 裁剪区域的尺寸 object -
displaySize 裁剪后图片的显示尺寸 object 裁剪尺寸
resizeMode 缩放模式 string ‘cover’
quality 压缩质量 (0.0-1.0) number 0.9
format 输出格式 string 自动判断

💡 七、使用示例

7.1 基础裁剪

最简单的裁剪方式,从图片左上角开始裁剪一个 200x200 的区域。

适用场景: 图片内容集中在左上角,需要快速裁剪固定尺寸。

import ImageEditor from "@react-native-community/image-editor";

const cropImage = async () => {
  try {
    const croppedImageURI = await ImageEditor.cropImage(
      'https://example.com/image.jpg',
      {
        offset: { x: 0, y: 0 },
        size: { width: 200, height: 200 },
      }
    );
    console.log('裁剪后的图片路径:', croppedImageURI);
  } catch (error) {
    console.error('裁剪失败:', error);
  }
};

代码解析:

  • offset: { x: 0, y: 0 } - 从原图左上角开始
  • size: { width: 200, height: 200 } - 裁剪 200x200 像素区域
  • 返回的 croppedImageURI 是裁剪后图片的本地缓存路径

7.2 带缩放和压缩的裁剪

在裁剪的同时进行缩放和压缩,适用于需要生成缩略图的场景。

适用场景: 生成头像缩略图、列表预览图等需要控制文件大小的场景。

const cropWithResize = async () => {
  try {
    const croppedImageURI = await ImageEditor.cropImage(
      imageUri,
      {
        offset: { x: 50, y: 50 },
        size: { width: 300, height: 300 },
        displaySize: { width: 100, height: 100 },
        quality: 0.8,
        format: 'jpeg',
      }
    );
    console.log('缩略图路径:', croppedImageURI);
  } catch (error) {
    console.error('处理失败:', error);
  }
};

代码解析:

  • offset: { x: 50, y: 50 } - 从原图 (50, 50) 位置开始裁剪
  • size: { width: 300, height: 300 } - 裁剪 300x300 像素区域
  • displaySize: { width: 100, height: 100 } - 缩放到 100x100 像素输出
  • quality: 0.8 - 80% 质量,平衡文件大小和画质
  • format: 'jpeg' - 输出为 JPEG 格式

7.3 正方形头像裁剪

自动计算裁剪区域,从图片中心裁剪出最大的正方形区域,常用于头像处理。

适用场景: 用户头像上传、个人资料图片处理。

const cropAvatar = async (imageUri, imageSize) => {
  const size = Math.min(imageSize.width, imageSize.height);
  const x = (imageSize.width - size) / 2;
  const y = (imageSize.height - size) / 2;

  try {
    return await ImageEditor.cropImage(imageUri, {
      offset: { x, y },
      size: { width: size, height: size },
      displaySize: { width: 200, height: 200 },
      quality: 0.9,
      format: 'jpeg',
    });
  } catch (error) {
    console.error('头像裁剪失败:', error);
    return null;
  }
};

// 使用示例
const avatarUri = await cropAvatar(
  'file:///path/to/image.jpg',
  { width: 1920, height: 1080 }
);

代码解析:

  • Math.min(imageSize.width, imageSize.height) - 取宽高中较小值作为正方形边长
  • x = (imageSize.width - size) / 2 - 计算水平居中偏移
  • y = (imageSize.height - size) / 2 - 计算垂直居中偏移
  • 最终从图片中心裁剪出最大正方形区域

7.4 结合图片选择器使用

实际项目中,通常需要先选择图片,获取尺寸后再进行裁剪。

适用场景: 完整的头像上传流程。

import { launchImageLibrary } from 'react-native-image-picker';
import ImageEditor from '@react-native-community/image-editor';
import { Image } from 'react-native';

const selectAndCropAvatar = async () => {
  // 1. 选择图片
  const result = await launchImageLibrary({
    mediaType: 'photo',
    quality: 1,
  });

  if (result.didCancel || result.errorCode) {
    return null;
  }

  const imageUri = result.assets[0].uri;

  // 2. 获取图片尺寸
  Image.getSize(imageUri, async (width, height) => {
    // 3. 计算正方形裁剪区域
    const size = Math.min(width, height);
    const x = (width - size) / 2;
    const y = (height - size) / 2;

    // 4. 执行裁剪
    const croppedUri = await ImageEditor.cropImage(imageUri, {
      offset: { x, y },
      size: { width: size, height: size },
      displaySize: { width: 200, height: 200 },
      quality: 0.9,
      format: 'jpeg',
    });

    return croppedUri;
  });
};

代码解析:

  • 使用 react-native-image-picker 选择图片
  • 使用 Image.getSize() 获取图片原始尺寸
  • 根据尺寸计算居中裁剪区域
  • 执行裁剪并返回结果

❓ 八、常见问题

8.1 常见问题解答

Q1: 裁剪后图片路径是什么?

A: 裁剪后的图片存储在应用缓存目录中,返回的是缓存路径。使用完毕后建议手动清理。

Q2: 支持哪些图片格式?

A: 支持 JPEG 和 PNG 格式,可通过 format 参数指定输出格式。

Q3: 如何处理网络图片?

A: cropImage 方法会自动下载网络图片进行裁剪,下载失败会抛出异常。

Q4: 裁剪尺寸超出原图范围会怎样?

A: 会在调用时报错,建议在裁剪前检查 offset 和 size 是否超出原图尺寸。

Q5: 如何获取原图尺寸?

A: 可以使用 Image.getSize() 或 Image.getSizeWithHeaders() 方法获取图片尺寸。

8.2 最佳实践

  1. 裁剪前检查尺寸:确保裁剪区域不超出原图范围
  2. 合理设置质量:根据场景选择合适的压缩质量
  3. 及时清理缓存:裁剪后的图片在缓存中,使用后及时删除
  4. 错误处理:添加 try-catch 处理可能的异常

💻 九、完整示例代码

图片裁剪器示例

在这里插入图片描述

import React, { useState } from 'react';
import {
  View,
  Text,
  StyleSheet,
  SafeAreaView,
  Image,
  TouchableOpacity,
  ScrollView,
  Dimensions,
  ActivityIndicator,
  Alert,
} from 'react-native';
import ImageEditor from '@react-native-community/image-editor';

const { width: SCREEN_WIDTH } = Dimensions.get('window');

const DEMO_IMAGES = [
  {
    uri: 'https://octodex.github.com/images/OctoAsians_dex_Full.png',
    width: 896,
    height: 896,
  },
  {
    uri: 'https://octodex.github.com/images/Professortocat_v2.png',
    width: 896,
    height: 896,
  },
];

export default function App() {
  const [currentImage, setCurrentImage] = useState(DEMO_IMAGES[0]);
  const [croppedUri, setCroppedUri] = useState<string | null>(null);
  const [isProcessing, setIsProcessing] = useState(false);
  const [cropSize, setCropSize] = useState({ width: 300, height: 300 });

  const handleCrop = async () => {
    setIsProcessing(true);
    try {
      const result = await ImageEditor.cropImage(currentImage.uri, {
        offset: { x: 100, y: 100 },
        size: cropSize,
        displaySize: { width: 200, height: 200 },
        quality: 0.9,
        format: 'jpeg',
      });
      setCroppedUri(result);
    } catch (error) {
      Alert.alert('裁剪失败', '请检查图片是否可访问');
    } finally {
      setIsProcessing(false);
    }
  };

  const handleCropSquare = async () => {
    setIsProcessing(true);
    try {
      const size = Math.min(currentImage.width, currentImage.height);
      const result = await ImageEditor.cropImage(currentImage.uri, {
        offset: { x: 0, y: 0 },
        size: { width: size, height: size },
        displaySize: { width: 200, height: 200 },
        quality: 0.9,
        format: 'jpeg',
      });
      setCroppedUri(result);
    } catch (error) {
      Alert.alert('裁剪失败', '请检查图片是否可访问');
    } finally {
      setIsProcessing(false);
    }
  };

  const switchImage = () => {
    const nextIndex = DEMO_IMAGES.indexOf(currentImage) === 0 ? 1 : 0;
    setCurrentImage(DEMO_IMAGES[nextIndex]);
    setCroppedUri(null);
  };

  const clearCropped = () => {
    setCroppedUri(null);
  };

  return (
    <SafeAreaView style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.title}>图片裁剪器</Text>
      </View>

      <ScrollView style={styles.content} contentContainerStyle={styles.scrollContent}>
        <View style={styles.section}>
          <Text style={styles.sectionTitle}>原图</Text>
          <ScrollView horizontal showsHorizontalScrollIndicator={false}>
            <Image
              source={{ uri: currentImage.uri }}
              style={styles.originalImage}
              resizeMode="contain"
            />
          </ScrollView>
        </View>

        <View style={styles.buttonGroup}>
          <TouchableOpacity
            style={[styles.button, styles.primaryButton]}
            onPress={handleCrop}
            disabled={isProcessing}
          >
            {isProcessing ? (
              <ActivityIndicator color="#fff" />
            ) : (
              <Text style={styles.buttonText}>裁剪 300x300</Text>
            )}
          </TouchableOpacity>

          <TouchableOpacity
            style={[styles.button, styles.secondaryButton]}
            onPress={handleCropSquare}
            disabled={isProcessing}
          >
            <Text style={styles.secondaryButtonText}>正方形裁剪</Text>
          </TouchableOpacity>

          <View style={styles.rowButtons}>
            <TouchableOpacity
              style={[styles.button, styles.outlineButton]}
              onPress={switchImage}
            >
              <Text style={styles.outlineButtonText}>切换图片</Text>
            </TouchableOpacity>

            <TouchableOpacity
              style={[styles.button, styles.outlineButton]}
              onPress={clearCropped}
            >
              <Text style={styles.outlineButtonText}>清除结果</Text>
            </TouchableOpacity>
          </View>
        </View>

        {croppedUri && (
          <View style={styles.section}>
            <Text style={styles.sectionTitle}>裁剪结果</Text>
            <View style={styles.resultContainer}>
              <Image
                source={{ uri: croppedUri }}
                style={styles.croppedImage}
                resizeMode="contain"
              />
              <Text style={styles.resultPath} numberOfLines={2}>
                {croppedUri}
              </Text>
            </View>
          </View>
        )}
      </ScrollView>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#1a1a2e',
  },
  header: {
    padding: 20,
    backgroundColor: '#16213e',
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    color: '#fff',
    textAlign: 'center',
  },
  content: {
    flex: 1,
  },
  scrollContent: {
    padding: 16,
  },
  section: {
    marginBottom: 24,
  },
  sectionTitle: {
    fontSize: 18,
    fontWeight: '600',
    color: '#fff',
    marginBottom: 12,
  },
  originalImage: {
    width: 300,
    height: 300,
    borderRadius: 12,
    backgroundColor: '#16213e',
  },
  buttonGroup: {
    gap: 12,
    marginBottom: 24,
  },
  button: {
    paddingVertical: 14,
    borderRadius: 12,
    alignItems: 'center',
  },
  primaryButton: {
    backgroundColor: '#00d4ff',
  },
  secondaryButton: {
    backgroundColor: '#16213e',
    borderWidth: 2,
    borderColor: '#00d4ff',
  },
  outlineButton: {
    flex: 1,
    backgroundColor: 'transparent',
    borderWidth: 1,
    borderColor: '#444',
    paddingVertical: 12,
  },
  rowButtons: {
    flexDirection: 'row',
    gap: 12,
  },
  buttonText: {
    fontSize: 16,
    color: '#1a1a2e',
    fontWeight: '600',
  },
  secondaryButtonText: {
    fontSize: 16,
    color: '#00d4ff',
    fontWeight: '600',
  },
  outlineButtonText: {
    fontSize: 14,
    color: '#888',
  },
  resultContainer: {
    alignItems: 'center',
    backgroundColor: '#16213e',
    borderRadius: 16,
    padding: 20,
  },
  croppedImage: {
    width: 200,
    height: 200,
    borderRadius: 8,
    marginBottom: 16,
  },
  resultPath: {
    fontSize: 12,
    color: '#666',
    textAlign: 'center',
  },
});

🔗 十、相关资源

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐