欢迎加入 Flutter 鸿蒙化社区(oh-flutter 组织):鸿蒙系统Flutter开源库社区 - 开源代码托管,代码协作 - AtomGit 本文所用适配版插件的仓库地址:adaptive_image_picker:Flutter adaptive_image_picker 的 HarmonyOS/OpenHarmony 适配版:支持系统相册选择、相机拍照、裁剪与压缩,保留上游 API 和 Git 历史。 - AtomGit

一、应用背景:为什么鸿蒙上需要这个库

做跨平台业务的同学对"选头像"这个需求一定不陌生:从相册选一张图,按圆形裁一下,压到 100 KB 以内再上传。在 Flutter 生态里,这条链路过去通常要拼三个库——选图一个、裁剪一个、压缩一个。这不仅让依赖变重,还有几个长期痛点:

  1. 权限问题。传统选图方案依赖系统相册权限,应用市场审核经常因为权限声明与实际用途不匹配被驳回,隐私合规成本高。
  2. 裁剪依赖原生库。常见的 Flutter 裁剪库底层是原生实现,每引入一个就要多维护一份平台侧代码;鸿蒙(HarmonyOS / OpenHarmony)作为新平台,这类原生库往往"没有对应版本",直接导致整条链路在鸿蒙上断掉。
  3. 压缩不保证结果。多数压缩接口只给一个 quality 参数,压出来是 300 KB 还是 3 MB 全凭运气,而头像上传这类业务需要的是"必须小于某个字节数"的硬承诺。

adaptive_image_picker 正是针对这些痛点设计的 Flutter 媒体选择库:相册选择走系统 Photo Picker(零权限)、裁剪引擎完全用 Dart 实现(不依赖任何原生裁剪库)、压缩采用二分搜索逼近目标字节数(结果严格不超过上限)。它把选图、拍照、裁剪、压缩、URL 导入收进一个统一的 Dart API。

问题在于,这样一个"为跨平台而生"的库,官方只覆盖了 Android、iOS、Web 和桌面端。我们把它适配到了 HarmonyOS / OpenHarmony,并把适配成果开源在 AtomGit 上。本文就带你把这个库用在鸿蒙工程里,覆盖它的全部对外接口,并附上鸿蒙设备上的实测效果。

本文的验证环境:

  • 设备:HarmonyOS 7.0.0(API 26)手机模拟器;nova 15 真机(ROM 6.1+)用于相机拍照验证
  • IDE 与工具链:DevEco Studio 26.0.0 Release、Flutter OHOS 版本工具链
  • 插件版本:adaptive_image_picker 1.0.2 + 鸿蒙适配提交

二、这个库有哪些功能

adaptive_image_picker 的能力可以分为"平台相关"和"平台无关"两层,这个分层也决定了鸿蒙化的工作量分布。

平台无关层(纯 Dart,天然跨平台,无需适配即可在鸿蒙运行):

  • 交互式手势裁剪引擎:双指缩放、拖拽平移、九宫格参考线、圆形头像蒙版(抗锯齿透明通道)、90° 旋转、水平/垂直翻转,预设比例覆盖 1:1、16:9、4:3、3:2、9:16、自由与自定义;
  • 目标大小压缩引擎:以二分搜索逼近 maxBytes,保证输出严格不超过目标字节数,支持转 WebP / JPEG / PNG,自动烘焙 EXIF 方向;
  • 自适应弹窗 UI:底部动作面板(相机 / 相册 / URL 三源选择),支持完整的主题定制与完全自定义构建;
  • AdaptiveFile 统一模型:文件名、大小、宽高、MIME 类型、懒加载字节流、落盘与删除等文件操作。

平台相关层(需要鸿蒙原生实现,共 3 个通道方法):

  • 相册选择:鸿蒙侧基于系统 PhotoViewPicker 实现,单选、多选(最多 500 项)、图片/视频/混合媒体过滤均支持,且不申请任何媒体库权限;
  • 相机拍照:基于系统 cameraPicker 实现,支持前置/后置切换,同样零权限;
  • 平台版本诊断getPlatformVersion 返回形如 HarmonyOS x.x.x 的版本串,用于联调排查。

适配遵循了原库的取消语义:相册取消返回空列表,拍照取消返回 null,与 Android / iOS 行为完全一致,业务代码不需要写平台分支。

三、环境搭建

本文不展开环境安装步骤,请直接按照官方文档完成 Flutter 鸿蒙化环境搭建:

Flutter for OpenHarmony 环境搭建

完成后再执行 flutter doctor 确认输出中不缺少鸿蒙构建所需项,即可继续下文。

四、在应用中引入三方库(AtomGit 仓库方式)

鸿蒙化版本托管在 AtomGit 的 oh-flutter 组织下,通过 Git 依赖直接引入。在应用工程的 pubspec.yaml 中添加:

dependencies:
  adaptive_image_picker:
    git:
      url: https://atomgit.com/oh-flutter/adaptive_image_picker.git

如果想锁定到某个确定提交(推荐在团队协作中固定),可以追加 ref

dependencies:
  adaptive_image_picker:
    git:
      url: https://atomgit.com/oh-flutter/adaptive_image_picker.git
      ref: main

然后拉取依赖:

flutter pub get

不需要任何额外的鸿蒙工程改动。 适配版本的 pubspec.yaml 已经注册了 ohos 平台的 pluginClass: AdaptiveImagePickerPlugin,ArkTS 侧实现随包携带,flutter pub get 之后构建鸿蒙 HAP 时插件会自动完成注册。如果你的工程是第一次跑鸿蒙目标,可以用下面命令构建并安装示例:

flutter build hap --debug

之后用 DevEco Studio(或 devecocli run)把生成的 HAP 装到模拟器或真机上即可。

五、调用接口实现功能(附鸿蒙设备实测效果)

代码里统一这样导入:

import 'package:adaptive_image_picker/adaptive_image_picker.dart';

下面按"选图 → 拍照 → 多选 → 裁剪 → 压缩 → 弹窗 → URL 导入"的完整业务链路,把库的全部对外接口过一遍。所有截图均为鸿蒙设备实拍。

5.1 平台版本诊断:getPlatformVersion

联调时先确认插件在鸿蒙侧注册成功,最简单的办法就是调一次版本接口:

final String? version = await AdaptiveImagePicker.getPlatformVersion();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
  SnackBar(content: Text('当前平台:$version')),
);

在鸿蒙设备上返回 HarmonyOS 7.0.0(模拟器与真机的具体版本串以 deviceInfo.displayVersion 为准)。如果这里能正常弹出,说明 MethodChannel 已通,后面的接口都有保障。

5.2 相册单图选择:pickImage(source: ImageSource.gallery)

最基础的能力,一行代码拉起系统相册:

final AdaptiveFile? file = await AdaptiveImagePicker.pickImage(
  source: ImageSource.gallery,
);
if (file != null) {
  debugPrint('选中:${file.name},${file.formattedSize},${file.width}x${file.height}');
}

鸿蒙侧拉起的是系统 Photo Picker 界面,应用全程不需要申请媒体库权限。选中图片后,插件会把 Picker 返回的临时 URI 复制到应用缓存目录,再以普通文件路径返回给 Dart,name / size / mimeType / lastModified 等元数据一应俱全。用户在 Picker 里点返回取消时,得到的是 null,不会抛异常。

值得注意的是 Picker 界面上的"安全访问图库"提示:应用仅可访问用户选定的图片和视频,无法读取图库的其余内容——这正是"零权限"特性的直接来源,也是它与传统需要存储权限的选图方案的本质区别。

选择完成后返回示例应用,底部 Gallery 标签的徽标更新为 1,表示已收集到 1 个媒体文件。此时用 hdc 检查应用缓存目录,能看到生成的 adaptive_picker_*.jpg(实测 24,225 字节)——"Picker 临时 URI 复制到应用缓存、再以普通文件路径返回 Dart"这条链路完整闭环。

5.3 相机拍照:pickImage(source: ImageSource.camera)

final AdaptiveFile? photo = await AdaptiveImagePicker.pickImage(
  source: ImageSource.camera,
  preferredCameraDevice: CameraDevice.rear, // 前置用 CameraDevice.front
);

鸿蒙侧基于系统 cameraPicker 实现,拍照同样零权限。preferredCameraDevice 控制拉起时的前后摄像头;用户取消拍照时返回 null,与 Android / iOS 契约一致。拍照结果会写入应用缓存并以 AdaptiveFile 返回。拍照链路已在 nova 15 真机上验证通过(模拟器没有可用摄像头)。

5.4 多图与混合媒体选择:pickMultiple

final List<AdaptiveFile> files = await AdaptiveImagePicker.pickMultiple(
  maxCount: 9,
  mediaType: MediaType.image, // 视频用 MediaType.video,混合用 MediaType.all
);
for (final f in files) {
  debugPrint('${f.name}: ${f.formattedSize}');
}

maxCount 会透传给系统 Picker 的最大可选数(鸿蒙侧系统上限为 500,插件会做收敛);mediaType 控制图片 / 视频 / 混合三种过滤模式。取消时返回空列表。配合 compressionOptions 可以对选中的每一张批量压缩:

final List<AdaptiveFile> files = await AdaptiveImagePicker.pickMultiple(
  maxCount: 9,
  compressionOptions: const CompressionOptions(
    format: OutputFormat.webp,
    maxWidth: 1920,
    maxHeight: 1080,
  ),
);

5.5 交互式裁剪:cropImage 与 CropOptions

裁剪引擎是纯 Dart 实现,在鸿蒙上与在其他平台表现完全一致。既可以在选图时通过 cropOptions 直接串联,也可以对已有文件独立调用:

// 方式一:选图后直接进入裁剪(pickImage 的 cropOptions 参数)
final AdaptiveFile? avatar = await AdaptiveImagePicker.pickImage(
  source: ImageSource.gallery,
  context: context,
  cropOptions: CropOptions.circle(title: '设置头像'), // 圆形头像蒙版
);

// 方式二:对已有文件独立打开裁剪器
final AdaptiveFile? cropped = await AdaptiveImagePicker.cropImage(
  file: existingFile,
  context: context,
  options: const CropOptions(
    showGrid: true,        // 九宫格参考线
    allowRotation: true,   // 90° 旋转按钮
    allowFlipping: true,   // 水平/垂直翻转
    aspectRatioPreset: CropAspectRatio.ratio16x9,
    lockAspectRatio: true,
  ),
);

在鸿蒙设备上,双指缩放、拖拽、圆形蒙版、旋转翻转全部可用,手势跟手性与其他平台一致。

5.6 目标大小压缩:compressImage

这是这个库最有辨识度的能力——保证输出严格不超过目标字节数

final AdaptiveFile compressed = await AdaptiveImagePicker.compressImage(
  file: largeFile,
  options: CompressionOptions.targetSize(
    150 * 1024,                      // 输出保证 ≤ 150 KB
    format: OutputFormat.webp,       // 同时转成 WebP
  ),
);
debugPrint('${largeFile.formattedSize} -> ${compressed.formattedSize}');

内部实现是二分搜索质量参数,配合必要的降采样,因此对任意输入都能收敛到上限以内。此外还有几个开箱即用的预设:CompressionOptions.avatar()(头像)、CompressionOptions.webOptimized()(Web 上传)、CompressionOptions.thumbnail()(缩略图)。

下图为示例应用中目标大小压缩的配置界面:Strict Max File Size Target 拖到 250 KB,输出格式选 WebP (Recommended - Ultra Efficient)。

5.7 自适应来源弹窗:showPickerModal

想让用户自己挑"拍照还是相册"?showPickerModal 弹出三源选择面板,选中后自动进入对应流程,还可以把裁剪、压缩参数一次配齐:

final AdaptiveFile? file = await AdaptiveImagePicker.showPickerModal(
  context: context,
  options: const PickerOptions(
    modalTitle: '选择图片来源',
    sources: [ImageSource.camera, ImageSource.gallery, ImageSource.url],
  ),
  cropOptions: CropOptions.circle(title: '调整头像'),
  compressionOptions: CompressionOptions.avatar(maxBytes: 100 * 1024),
);

PickerOptions 还支持 theme 定制配色与圆角,甚至可以用 customModalBuilder 完全接管弹窗 UI(比如做成品牌化的自绘面板)。

5.8 URL 图片导入:fromUrl

从网络图直接进入处理管线(下载 → 可选裁剪 → 可选压缩):

final AdaptiveFile file = await AdaptiveImagePicker.fromUrl(
  'https://picsum.photos/800/600',
  context: context,
  cropOptions: const CropOptions(
    shape: CropShape.circle,
    aspectRatioPreset: CropAspectRatio.ratio1x1,
  ),
  compressionOptions: const CompressionOptions(maxBytes: 200 * 1024),
);

唯一需要配置的场景来了:URL 导入要访问网络,需要在鸿蒙工程的 entry/src/main/module.json5 中声明网络权限(相册选择和拍照都不需要任何权限声明):

// entry/src/main/module.json5
{
  "module": {
    // ...
    "requestPermissions": [
      { "name": "ohos.permission.INTERNET" }
    ]
  }
}

漏掉这一声明是 URL 导入失败最常见的原因,见文末 FAQ。

5.9 AdaptiveFile:统一的结果模型

所有接口的返回值都是 AdaptiveFile,常用属性和方法如下:

file.name;            // 文件名(含扩展名)
file.formattedSize;   // 可读大小,如 "148.2 KB"
file.width; file.height;  // 像素宽高
file.aspectRatio;     // 宽高比
file.mimeType;        // MIME 类型
file.path;            // 本地路径(鸿蒙上是应用缓存目录内路径)

final bytes = await file.readAsBytes();          // 懒加载字节流
final saved = await file.saveToDirectory(dirPath); // 按原文件名保存到目录
await file.delete();                              // 删除本地文件

需要注意:鸿蒙上相册/拍照结果落在应用缓存目录cacheDir),适合做即时上传、预览;如果业务要长期保留,请用 saveToDirectory 转存到应用文档目录,不要依赖缓存路径长期有效。

六、FAQ:适配与使用过程中的常见问题

Q1:URL 导入一直失败,报网络错误?

九成是没在 module.json5 里声明 ohos.permission.INTERNET(配置见 5.8 节)。声明后重新构建安装即可。相册与拍照不需要额外权限;如果你发现相册选择也在申请什么权限,那说明用的不是本适配版本。

Q2:运行时报 MissingPluginException 怎么办?

先确认依赖确实解析到了 AtomGit 版本(看 pubspec.lockadaptive_image_picker 的来源是否为 git 描述),然后重新执行 flutter pub get 让鸿蒙工程重新生成插件注册文件。如果是在自己改造插件代码时出现,检查 Dart 侧插件类的导出是否完整——鸿蒙生成的注册链路按包入口解析插件类,仅在某一个分平台实现文件里声明是找不到的。

Q3:选到的图片路径可以长期持有吗?

不可以。系统 Picker 返回的是临时 URI,插件已在返回前把它复制进应用缓存并校验非空,但缓存目录随时可能被系统回收。需要长期保留就 saveToDirectory 转存。这也是插件的设计契约:返回给你的一定是可直接读取的普通文件路径,而不是要求你自己处理 URI。

Q4:Windows 上构建鸿蒙工程时报奇怪的路径错误?

Windows 下 Hvigor 构建链路对路径长度敏感,工程路径接近 260 字符时可能失败。把工程放到 C:\Dev\<项目名> 这类短 ASCII 路径下再构建。我们自己适配时就踩过这个坑:换到短路径后一次通过,源码本身没有任何问题。

Q5:库本身有问题,如何提交 issue?

在插件的 AtomGit 仓库提 issue:issues - 鸿蒙系统Flutter开源库社区/adaptive_image_picker - AtomGit

为了保证可定位,请附上:

  1. flutter doctor -v 完整输出;
  2. 设备型号与系统版本(模拟器注明 API 版本);
  3. 最小复现代码片段与完整调用参数;
  4. 预期行为与实际行为,以及 devecocli log 中相关日志;
  5. 能截图的尽量附截图。

Q6:我能修复它,如何提交 PR?

欢迎直接参与共建,流程如下:

  1. Fork adaptive_image_picker:Flutter adaptive_image_picker 的 HarmonyOS/OpenHarmony 适配版:支持系统相册选择、相机拍照、裁剪与压缩,保留上游 API 和 Git 历史。 - AtomGit 到个人账号,克隆到短路径目录;
  2. main 拉出功能分支(如 fix/cache-copy-validation);
  3. 修改并保证本地验证全部通过:flutter analyze 零问题、flutter test 全绿、flutter build hap --debug 构建成功,涉及设备行为的改动必须在模拟器或真机实测;
  4. 提交并推送到你的 Fork,在 AtomGit 上对源仓库 main 发起 Pull Request;
  5. PR 描述中写清问题、改动点与验证证据(分析/测试/构建/设备截图),便于维护者复核。

七、总结

本文以 adaptive_image_picker 为例,完整走了一遍"鸿蒙化三方库在业务中的落地":从 AtomGit 仓库引入依赖,到相册、拍照、多选、裁剪、压缩、弹窗、URL 导入的全部接口实测。对业务方来说,它用零权限的系统 Picker 省掉了隐私合规的心智负担,用纯 Dart 的裁剪引擎绕开了"新平台没有原生裁剪库"的困境,用二分压缩给出了"不超过目标大小"的硬承诺;对想做鸿蒙共建的同学来说,这类纯 Dart 为主、少量平台通道的库,恰好是入手鸿蒙化适配的理想对象——平台相关面越小,验证成本越低。

如果你在适配或使用中踩到新的坑,欢迎到仓库提 issue / PR,也欢迎加入社区一起把 Flutter 鸿蒙生态补齐。

欢迎加入 Flutter 鸿蒙化社区(oh-flutter 组织):鸿蒙系统Flutter开源库社区 - 开源代码托管,代码协作 - AtomGit

Logo

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

更多推荐