react-native-photo-editor:基于 React Native 鸿蒙生态的图片编辑库项目

原生照片编辑器(裁剪/贴纸/涂鸦)

Branch3Tags3

react-native-photo-editor

react-native-photo-editor 基于上游 react-native-photo-editor 开发。如果在使用过程中有任何问题,欢迎在GitCode提交Issue,会及时跟进。

项目介绍

react-native-photo-editor 为 React Native 应用提供全屏图片编辑能力:JS 侧一次调用 PhotoEditor.Edit(props) 即可拉起原生全屏编辑界面,支持涂鸦(画笔/橡皮擦/撤销)、文本、贴纸/Emoji、裁剪旋转、保存(覆盖写回源图或另存系统相册)与系统分享,编辑完成后按结果回吐 onDone/onCancel 回调。

react-native-photo-editor 是 OpenHarmony 适配版,JS 侧仍使用上游包名 react-native-photo-editor,鸿蒙侧 ohpm 包名为 @oh-rn/react-native-photo-editor。适配面包括:

  • 1 个 TurboModule:RNPhotoEditor
  • 1 个 TurboModule 方法:Edit
  • 2 个回调:onDone / onCancel
  • 4 个 props 字段:path / colors / hiddenControls / stickers
  • 1 套 HAR 导出的 ArkUI 编辑页组件(需宿主声明 Ability 装载)

集成指南

本节用于接入已有 RN 鸿蒙应用;运行本仓库示例请参见「快速验证(运行 Example)」。

1. 安装适配包

当前 npm 适配包为 @hxa-rn/react-native-photo-editor@1.0.15(与上游基线同号),业务别名为 react-native-photo-editor。在 RN 应用根目录执行:

npm install @hxa-rn/react-native-photo-editor@1.0.15 --save-exact --legacy-peer-deps

peerDependencies:{"react-native":">=0.72"}。还需已有可运行的 RNOH 宿主工程。

2. 配置 Metro 并导入

适配包已声明 harmony.alias。宿主 metro.config.js 合并以下配置,并保留原有的自定义设置:

const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
const {createHarmonyMetroConfig} = require('@react-native-oh/react-native-harmony/metro.config');

module.exports = mergeConfig(
  getDefaultConfig(__dirname),
  createHarmonyMetroConfig({
    reactNativeHarmonyPackageName: '@react-native-oh/react-native-harmony',
  }),
);

业务代码:

import PhotoEditor from 'react-native-photo-editor';

3. 注册鸿蒙原生模块

本包声明了 Autolinking 元数据:CMake 目标 photo_editor,ohpm 包 @oh-rn/react-native-photo-editor,C++ / ETS 类 PhotoEditorPackage。宿主启用 RNOH Autolinking 后,应检查生成的 HAR 依赖、CMake 链接及 C++ / ETS Package 注册是否齐全;已自动注册时不要重复手动注册。

Autolinking 不可用时,保留宿主已有配置,补齐以下四处:

  1. 在 harmony/entry/oh-package.json5 的 dependencies 追加下项,然后在 harmony/entry 执行 ohpm install 并同步工程:
"@oh-rn/react-native-photo-editor": "file:../../node_modules/@hxa-rn/react-native-photo-editor/harmony/photo_editor.har"
  1. 在 harmony/entry/src/main/cpp/CMakeLists.txt 添加子目录,并在已有 add_library(rnoh_app ...) 之后链接:
set(OH_MODULE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../oh_modules")
add_subdirectory("${OH_MODULE_DIR}/@oh-rn/react-native-photo-editor/src/main/cpp" ./photo_editor)
# 放在 rnoh_app 目标创建之后:
target_link_libraries(rnoh_app PUBLIC photo_editor)
  1. 在 harmony/entry/src/main/cpp/PackageProvider.cpp 添加头文件,并把实例加入已有 getPackages 返回集合(本仓库模板可加入 ManualLinkingPackage 列表):
#include "PhotoEditorPackage.h"

// 加入已有 Package 集合:
std::make_shared<rnoh::PhotoEditorPackage>(ctx)
  1. 在 harmony/entry/src/main/ets/RNPackagesFactory.ets 添加默认导入,并把实例加入已有 createRNPackages 返回数组:
import PhotoEditorPackage from '@oh-rn/react-native-photo-editor';

// 加入 createRNPackages(ctx) 返回数组:
new PhotoEditorPackage(ctx)

确保宿主 RNApp 使用上述 createRNPackages,并启用 enableCAPIArchitecture: true。完成后重新生成宿主 Bundle、构建并安装 HAP;只刷新 JS 不会加载新增的原生模块。若 HAR 内 RNOH 相对依赖无法解析,在宿主鸿蒙根 oh-package.json5 的 overrides 中将 @rnoh/react-native-openharmony 指向宿主正在使用的同一 RNOH HAR,勿混用 debug/release 或不同版本。

4. 声明宿主 Ability 与编辑页(HAR 不能声明 Ability,本库唯一需要手动接线的一步)

HAR 不能自行声明 Ability,需由宿主补齐以下三处(与本仓示例相同;路径以宿主 RN 工程根目录为起点)。

① harmony/entry/src/main/module.json5:在 module.abilities 数组中新增一项。Ability 名必须为 PhotoEditorAbility,本库按此名拉起编辑页:

{
  "name": "PhotoEditorAbility",
  "srcEntry": "./ets/photoeditorability/PhotoEditorAbility.ets",
  "description": "$string:PhotoEditorAbility_desc",
  "label": "$string:PhotoEditorAbility_label",
  "launchType": "standard",
  "startWindowIcon": "$media:startIcon",
  "startWindowBackground": "$color:start_window_background",
  "exported": false
}

PhotoEditorAbility_desc、PhotoEditorAbility_label 需在 harmony/entry/src/main/resources/base/element/string.json 中定义(也可改用宿主已有的字符串资源)。

② harmony/entry/src/main/ets/photoeditorability/PhotoEditorAbility.ets:新建该文件,把本库传入的编辑配置转存到 AppStorage 的 config 键,再加载编辑页壳:

import UIAbility from '@ohos.app.ability.UIAbility';
import type Want from '@ohos.app.ability.Want';
import type AbilityConstant from '@ohos.app.ability.AbilityConstant';
import type window from '@ohos.window';
import { CONFIG_PARAM_KEY, registerStickerResolver, createRawfileStickerResolver } from '@oh-rn/react-native-photo-editor';

export default class PhotoEditorAbility extends UIAbility {
  onCreate(want: Want, _launchParam: AbilityConstant.LaunchParam): void {
    let cfgStr: string = '';
    const params: Record<string, Object> | undefined = want.parameters;
    if (params !== undefined) {
      const cfg: Object = params[CONFIG_PARAM_KEY];
      if (typeof cfg === 'string') {
        cfgStr = cfg;
      }
    }
    AppStorage.setOrCreate('config', cfgStr);
    // 可选:按资源名使用 stickers(如 stickers: ['sticker1'])时注册,贴纸图放在宿主 resources/rawfile/
    registerStickerResolver(createRawfileStickerResolver(this.context.resourceManager));
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/PhotoEditorHostPage', (err: Error) => {
      if (err && err.message) {
        console.error(`load PhotoEditorHostPage failed: ${err.message}`);
      }
    });
  }
}

不注册贴纸 resolver 时,资源名会回退为「按完整路径」语义处理,不会静默失败。

③ harmony/entry/src/main/ets/pages/PhotoEditorHostPage.ets:新建编辑页壳,装载本库导出的 PhotoEditorPage,把系统返回键转给 PhotoEditorController.onBackPress()(否则返回键无法正确触发 onCancel),编辑结束时把结果回传给本库:

import common from '@ohos.app.ability.common';
import { PhotoEditorPage, PhotoEditorController, RESULT_PATH_KEY } from '@oh-rn/react-native-photo-editor';

@Entry
@Component
struct PhotoEditorHostPage {
  private controller: PhotoEditorController = new PhotoEditorController();

  private finish(resultCode: number, imagePath?: string): void {
    const context = getContext(this) as common.UIAbilityContext;
    const params: Record<string, Object> = {};
    if (imagePath !== undefined) {
      params[RESULT_PATH_KEY] = imagePath;
    }
    context.terminateSelfWithResult({ resultCode, want: { parameters: params } });
  }

  onBackPress(): boolean | undefined {
    return this.controller.onBackPress();
  }

  build() {
    Column() {
      PhotoEditorPage({
        controller: this.controller,
        onFinish: (resultCode: number, imagePath?: string) => this.finish(resultCode, imagePath),
      })
    }
    .width('100%')
    .height('100%')
  }
}

并在 harmony/entry/src/main/resources/base/profile/main_pages.json 的 src 数组中加入 "pages/PhotoEditorHostPage"。

未完成本步接线时,Edit 调用会因拉不起编辑页而回 onCancel。完整实现可参照本仓 PhotoEditorAbility 与 PhotoEditorHostPage。

使用说明

最小调用

import PhotoEditor from 'react-native-photo-editor';

PhotoEditor.Edit({
  path: '/data/storage/el2/base/haps/entry/files/photo.jpg',
  onDone: (imagePath: string) => console.log('saved ->', imagePath),
  onCancel: (resultCode: number) => console.log('cancelled, code =', resultCode),
});

完整调用

PhotoEditor.Edit({
  path: 'file:///data/storage/el2/base/haps/entry/files/photo.png', // file:// 前缀会被内部剥离
  colors: ['#000000', '#f00', '#80FF0000'],   // 支持 #RRGGBB / #RGB / #AARRGGBB 及 Android 颜色名;非法串被剔除、其余正常显示
  hiddenControls: ['crop', 'share'],           // 取值域:text/clear/draw/save/share/sticker/crop,大小写不敏感
  stickers: ['sticker1', '/data/storage/el2/base/haps/entry/files/star.png'], // 资源名(需宿主注册 resolver)或完整路径均可
  onDone: (imagePath) => { /* 【完成】回吐剥离前缀后的源路径;【保存】回吐相册 URI */ },
  onCancel: (resultCode) => { /* 取消/失败统一回 0 */ },
});

接口文档

PhotoEditor.Edit(props, onDone, onCancel) props:

props 字段 类型 默认值 说明
path string 无(必填) 源图路径或 URI
colors string[] 内置 13 色 十六进制(#RGB/#ARGB/#RRGGBB/#AARRGGBB)或 Android 颜色名调色板
hiddenControls string[] [] 要隐藏的工具,取值域 7 键:text/clear/draw/save/share/sticker/crop,大小写不敏感
stickers string[] [](内置 26 张) 贴纸列表,资源名(需宿主注册 resolver)或完整路径均可

回调:

回调 类型 说明
onDone (imagePath: string) => void 【完成】回吐归一化后的源路径;【保存】回吐系统相册 URI(file://media/Photo/…),源图不变
onCancel (resultCode: number) => void 用户取消、保存失败、path 非法、拉起失败均回 0(四因同码),返回码不可单独作为成因判据

返回值:void。异常:不抛出,结果异步经 onDone/onCancel 二选一回吐。

贴纸解析集成函数(HAR 导出):

函数 说明
registerStickerResolver(fn) 注册"贴纸资源名 → 图片资源"的解析函数
createRawfileStickerResolver(rm) 开箱即用的 rawfile 映射实现
hasStickerResolver() 查询是否已注册 resolver

HAR 导出组件:

PhotoEditorPage / PhotoEditorController,供宿主自建的 @Entry 页壳装载与转发系统返回事件,配合上方「集成指南」第 5 步使用。

快速验证(运行 Example)

前置条件

依赖 版本要求
Node.js >= 20(示例工程 engines 要求)
DevEco Studio 6.1.1 及以上(示例 targetSdkVersion 为 6.1.1(24))
HarmonyOS SDK API 18+(example/harmony 的 compatibleSdkVersion 为 5.1.0(18))
路径 Windows 下仓库根目录的完整路径不超过 40 个字符,否则原生构建的中间文件路径会超过系统上限

运行步骤

1. 克隆仓库

git clone -b br_rnoh0.72 https://gitcode.com/hxa-rn/react-native-photo-editor.git
cd react-native-photo-editor

Windows 下可在克隆时指定短目录名,例如 git clone -b br_rnoh0.72 https://gitcode.com/hxa-rn/react-native-photo-editor.git C:\rpe。

2. 进入 example 目录,安装依赖

cd example
npm install --legacy-peer-deps

Example 从 npm 公仓安装 @hxa-rn/react-native-photo-editor@1.0.15,不需要在仓根执行 npm pack。

3. 生成 JS Bundle

npm run dev

产物:harmony/entry/src/main/resources/rawfile/bundle.harmony.js

4. 用 DevEco Studio 打开鸿蒙工程

  • 打开 DevEco Studio,选择 example/harmony 目录
  • 首次构建请在 File → Project Structure → Signing Configs 勾选 Automatically generate signature,Apply 后 Sync
  • 等待 Sync 完成

5. 编译并运行 HAP

在 DevEco Studio 中点击运行按钮,将 HAP 安装到设备。

example_auto(自动化用例工程)步骤相同,把 example 换成 example_auto 即可,两者的依赖与构建输出相互独立。

注意:Example 已预置 Package 注册、Metro alias 与 HAR 依赖,无需手动 Link。编 HAP 前必须先执行 npm run dev;改过 JS 后重新执行再运行。

常见问题

  • 找不到 JS 模块:检查适配包安装、Metro 别名及对应入口,修改 Metro 后重启打包进程。
  • 找不到 npm 包或 HAR:核对 example/node_modules/@hxa-rn/react-native-photo-editor/harmony/photo_editor.har 是否存在;不要将 example_auto 的 node_modules 当成 example 的依赖。
  • 原生模块或组件未注册:检查 HAR、CMake、C++ 和 ETS 注册,排除重复注册并重建 HAP。
  • SDK / 签名报错:按当前工程配置安装对应 SDK,并重新配置本机签名。
  • Ninja .rsp 创建失败:查看完整日志;确认是路径长度问题后,缩短工程目录、清理旧路径构建缓存再构建。

约束与限制

兼容性:

项 值
上游基线 react-native-photo-editor@1.0.13
npm 适配版本 @hxa-rn/react-native-photo-editor@1.0.15
RN / RNOH RN 0.72.5 + @react-native-oh/react-native-harmony@0.72.143
鸿蒙最低兼容 API API 18(compatibleSdkVersion 5.1.0(18))
目标 API API 24(targetSdkVersion 6.1.1(24))
运行系统 HarmonyOS
已验证通过的设备与档位 API22 / API23 / API24 / API26 真机,共四档

权限:

本库 HAR module.json5 的 requestPermissions 为空数组。【保存】经系统相册确认框完成,无需申请存储权限;【分享】走系统分享面板,无需权限。

使用约束:

  • HAR 不能声明 Ability,也不能被宿主直接加载页面,需按「集成指南」第 4 步手动接线,否则拉不起编辑页 / 系统返回键无法正确取消。
  • 保存像素尺寸按源图真实像素尺寸导出(裁剪后为选区像素),与上游 Android「视图渲染尺寸」不同——这是有意优于上游的改进,非缺陷。
  • 取消返回码「四因同码」:用户取消 / 保存失败 / path 非法 / 拉起失败统一回 onCancel(0),四种成因不可仅凭返回码区分。
  • 贴纸资源名(如 sticker1)需宿主注册 resolver 才能命中应用内资源,未接线时回退为按完整路径处理,不会崩溃或静默失败。

开源 License

本项目基于 Apache-2.0 License 开源,与上游协议一致。

问题反馈渠道

使用过程中发现任何问题,欢迎在GitCode 提交 Issue,也欢迎提交 PR 参与共建。

Introduction

原生照片编辑器(裁剪/贴纸/涂鸦)

Customize your domain