Flutter 人体部位选择插件 human_body_selector 鸿蒙适配实战,基于 SVG 路径实现正反面人体图点选,全部渲染与命中逻辑在 Dart 层完成
开发工具: 华为云码道
DevEco Studio:
26.0.0(26.0.0.851)原始仓库: Nizam54/human_body_selector
本文配套仓库: oh-flutter/human_body_selector
human_body_selector 是一个用 SVG 路径实现的人体部位选择器:它在页面里画出正面或背面的人体轮廓,把每个器官、关节、肢体拆成独立路径,用户点击或长按某个部位即可选中或取消,适合问诊登记、疼痛标注、康复打卡这类需要"在人体图上指位置"的场景。整库没有一行平台相关的业务代码——SVG 解析、路径绘制、命中测试全部在 Dart 层完成。本文以 human_body_selector 0.0.1 为例,介绍源码准备、OHOS 工程配置、ArkTS 插件壳和 example 真机运行。

插件目前支持 android、ios、ohos 平台,源码位于 AtomGit 配套仓库。文中的代码以提交 45d1705 为参考。

一、插件简介与适配目标
疼痛标注是人体部位选择器最典型的用途:康复科让患者标记疼痛位置,健身应用让用户标记酸痛肌群,问诊表单让医生记录检查部位。这类交互的共同点是需要按形状命中——点击的落点要判断"在哪个器官的轮廓里",而不是落在某个矩形按钮上。
human_body_selector 的做法是把 4 张 SVG 人体图(女性正/背面 human.svg / human1.svg,男性正/背面 male.svg / male1.svg)内置为资源,用正则提取每个 <path> 的 id、title 和路径数据,再用 path_drawing 和 svg_path_parser 把路径字符串解析成 Flutter 的 Path 对象。每个部位是一个叠在 Stack 里的 CustomPaint,绘制由 SvgPainter 完成,点击命中靠 Path.contains() 做逐路径判定。
选中的部位会整体填充为指定颜色,色值可以按 painLevel(0–10 的疼痛等级)从一条蓝色到红色的渐变里取;长按已选部位移除;multiSelect 控制多选或单选,enabled 控制是否响应手势。这些行为全部由参数声明,不依赖平台能力。
本次适配的目标因此非常聚焦:让插件在鸿蒙上被正常注册,让 SVG 资源能随包加载,让点选、长按、多选、单选、禁用这些交互在真机上与 Android/iOS 表现一致。适配本身不需要写任何业务通道代码——要写的只是一个能被注册的 ArkTS 插件壳。
二、环境准备
环境搭建参考社区文档:Flutter OH 开发环境搭建,完成 Flutter OH SDK 安装、环境变量和 DevEco Studio 配置。
完成后,在宿主机终端执行以下命令,确认当前选中的是支持 OHOS 的 Flutter 工具链,并能发现目标设备:
flutter --version
flutter doctor -v
hdc list targets


工程使用的工具链和 SDK 配置如下:
| 项目 | 版本或配置 | 用途 |
|---|---|---|
| Flutter OHOS SDK | 3.47.7-ohos-1.0.0 | Flutter 编译与 OHOS 平台工具链 |
| Flutter 分支 | oh-3.47-dev | oh-flutter 对应开发分支 |
| Dart SDK | 3.13.3 | Dart 语言与包管理环境 |
| DevEco Studio | 26.0.0(26.0.0.851) | 工程 Sync、签名配置与 hvigor 构建 |
| HarmonyOS 开发套件 | 7.0.0(API 26) | 开发套件版本及对应的 API 级别 |
compileSdkVersion | 26.0.0 | 编译时使用的 SDK API |
targetSdkVersion | 26.0.0 | 应用面向的行为版本 |
compatibleSdkVersion | 6.1.1(24) | 当前工程声明的最低兼容版本 |
| 插件版本 | 0.0.1 | pubspec.yaml 中的包版本 |
| 原生语言 | ArkTS | HarmonyOS 插件实现 |
| 插件产物 | HAR | 被应用 entry 模块依赖 |
2.1 开发套件版本与工程中的 SDK 版本配置
7.0.0(API 26) 和 26.0.0 涉及 HarmonyOS 开发套件版本(API 版本)及其底座 OpenHarmony 的版本号体系。在本文工程中,相关版本的含义与配置方式如下:
7.0.0(API 26)表示 HarmonyOS 开发套件版本为7.0.0,对应 API 26。26.0.0是本文 HarmonyOS 应用工程中compileSdkVersion和targetSdkVersion的属性值。6.1.1(24)是本文工程中compatibleSdkVersion的属性值,声明最低兼容 API 24。
对应的 product 配置为:
{
"name": "default",
"compatibleSdkVersion": "6.1.1(24)",
"targetSdkVersion": "26.0.0",
"runtimeOS": "HarmonyOS"
}

这组配置使用 API 26 SDK 编译,并以 API 26 为目标版本,最低兼容 API 24。模板默认生成的是 compatibleSdkVersion: "26.0.0",装到 API 24 的测试机上会报 9568297 older sdk version,所以这里把它降到了与真机一致。
需要注意,降 compatibleSdkVersion 只影响"能不能装"和"按哪个行为版本运行",编译仍使用本机 DevEco 的 API 26 SDK。这是 hvigor 的默认行为:工程不写 compileSdkVersion 时按当前安装的 SDK 编译。
三、从源码仓库开始准备适配工程
3.1 将上游源码同步到 AtomGit
适配已有第三方插件时,先从 Pub 包信息或项目 README 确认上游地址、包名、版本和许可证。同步仓库时保留源码、许可证和提交历史,方便后续跟踪上游更新。
在 AtomGit 网页的新建仓库流程中,找到从已有仓库导入的入口,填写上游 Git 地址,选择自己有写权限的组织或个人空间,设置目标仓库名称后执行导入。完成后检查目标分支、pubspec.yaml、LICENSE 和提交历史是否完整,并记录适配起点的提交号。如果上游本身托管在 AtomGit,也可以通过 Fork 获得自己的工作仓库。
human_body_selector 现可从 AtomGit 获取。下面使用 AtomGit 地址拉取代码;需要提交修改时,使用自己有写权限的仓库或 Fork。
3.2 将代码拉取到宿主机
在安装了 Flutter OH 和 DevEco Studio 的开发电脑上打开终端,进入准备存放项目的目录,执行:
git clone https://atomgit.com/oh-flutter/human_body_selector.git
cd human_body_selector
pwd
ls
git remote -v
git status --short --branch
git rev-parse HEAD
git clone 会创建 human_body_selector/ 目录;cd 后的位置就是下文所说的插件仓库根目录,这里应能看到 pubspec.yaml、lib/、assets/ 和 example/。本例中 Git 仓库名与 Dart 包名相同,都是 human_body_selector,不需要像有的库那样区分两者。
适配后的仓库使用 main 分支,并在适配提交上打了 0.0.1-ohos-1.0.0 标签。需要与本文代码完全一致时,在干净仓库中执行:
git switch --detach 0.0.1-ohos-1.0.0
适配其他插件时,使用该插件对应版本的 tag 或 commit 作为分支起点。

图 1:在宿主机终端输入 AtomGit 仓库拉取命令。
3.3 在仓库根目录确认适配分支与版本标记
接着在 human_body_selector/ 根目录确认分支与版本标记。上游仓库只有一个 main 分支,适配改动直接落在这个分支上,并通过 tag 固定版本。分支名与 tag 的对应关系如下:
git branch --show-current
git tag --list
git log --oneline -4
git remote -v
git status --short --branch
分支策略与本系列其余库保持一致:分支名使用 feat/ohos_库名称_版本号,库名称取 pubspec.yaml 的 name,版本号取此次适配的基线版本,本例为 feat/ohos_human_body_selector_0.0.1。适配完成后合并到 main,再在合并后的提交上打 tag:
git switch -c feat/ohos_human_body_selector_0.0.1
git tag 0.0.1-ohos-1.0.0
如果该分支已存在,使用 git switch feat/ohos_human_body_selector_0.0.1 切换即可。

图 2:在 human_body_selector 仓库根目录查看分支、TAG 与远程地址。
3.4 自动补全 OHOS 适配结构
分支创建后,仍在同一个插件根目录执行结构补全。以下命令适用于尚无 ohos/ 目录的既有 Flutter 平台插件:
flutter create --template=plugin --platforms=ohos --project-name human_body_selector .
git status --short
git diff -- pubspec.yaml lib example
--template=plugin指定插件模板。--platforms=ohos指定需要补全的平台。--project-name human_body_selector使用 Dart 包名。- 最后的
.表示在当前插件目录补全工程,不是另建一层目录。
该命令生成 OHOS 平台脚手架,业务逻辑需要在 ArkTS 中实现。生成后通过 diff 检查 pubspec.yaml、lib/ 和 example/ 的变化,保留已有 API、其他平台注册项及依赖配置。不同 Flutter OH 版本生成的模板可能略有差异。
这个库在结构补全之外有三处需要注意。一是 pubspec.yaml 里必须显式声明 plugin_platform_interface:lib/human_body_selector_platform_interface.dart 直接 import 了它,但它既不在 dependencies 中,也不被 svg_path_parser、path_drawing、equatable 任何一个依赖传递引入,不加声明时 flutter analyze 直接报 Undefined name 'PlatformInterface'。二是 example 里的模板测试是坏的:test/widget_test.dart 引用了不存在的 MyApp,integration_test/plugin_integration_test.dart 把 HumanBodySelector 这个 widget 当插件对象调 getPlatformVersion(),都要重写。三是 flutter: assets: 段声明了 assets/,4 张 SVG 走包资源路径加载,结构补全时不要把它弄丢。
如果生成后 example/ohos/ 仍不存在,进入已有示例应用补全平台:
cd example
flutter create --platforms=ohos .
cd ..
配套仓库已经包含 ohos/ 和 example/ohos/,直接运行示例时可以跳过结构补全。新建插件则使用 flutter create --org com.nutpi --template=plugin --platforms=ohos human_body_selector;已有插件使用上面的 . 在当前目录补全。

图 3:在插件根目录输入 OHOS 结构补全命令,并检查依赖与平台声明的 diff。
3.5 适配后的项目目录
适配后的关键目录如下:
human_body_selector/
├── assets/
│ ├── human.svg # 女性正面,35 个可交互部位
│ ├── human1.svg # 女性背面,29 个
│ ├── male.svg # 男性正面,34 个
│ └── male1.svg # 男性背面,29 个
├── lib/
│ ├── human_body_selector.dart # 主组件 HumanBodySelector(255 行)
│ ├── human_body_selector_method_channel.dart
│ ├── human_body_selector_platform_interface.dart
│ └── svg_painter/
│ ├── constant.dart # 正则与资源路径常量
│ ├── maps.dart # 4 张地图文件名常量
│ ├── parser.dart # SVG 正则解析为 BodyPart 列表
│ ├── size_controller.dart # 地图尺寸与缩放计算(单例)
│ ├── models/body_part.dart # 部位模型(id/title/path/painLevel)
│ ├── models/map_area.dart # 地图边界模型
│ └── widgets/svg_painter.dart # CustomPainter 绘制与命中测试
├── ohos/
│ ├── index.ets
│ ├── oh-package.json5
│ └── src/main/
│ ├── ets/components/plugin/HumanBodySelectorPlugin.ets
│ └── module.json5
├── example/
│ ├── lib/main.dart # 交互式演示页(148 行)
│ └── ohos/
│ └── entry/
├── android/ ios/
└── pubspec.yaml
项目根目录如下,其中包含 ohos/、example/,以及 OpenHarmony 中英文说明和变更记录文件:

图 4:适配后的 human_body_selector 项目目录。
| 文件 | 主要职责 |
|---|---|
assets/*.svg | 4 张人体地图,每个 <path> 一个部位,title 即部位名 |
lib/human_body_selector.dart | 主组件:状态管理、手势分发、选中列表与回调 |
lib/svg_painter/parser.dart | 按正则提取 SVG 里的 id/title/d,解析成 Path |
lib/svg_painter/size_controller.dart | 汇总所有路径边界,计算地图尺寸与缩放比例 |
lib/svg_painter/widgets/svg_painter.dart | CustomPainter:描边、填充、focus 光晕、命中测试 |
lib/human_body_selector_platform_interface.dart | 平台接口层(仅模板方法) |
HumanBodySelectorPlugin.ets | 注册 human_body_selector 通道,提供 getPlatformVersion |
插件 module.json5 | 声明 HAR 模块及设备类型 |
example/lib/main.dart | 演示页:地图切换、多选开关、配色、启用开关与已选文案 |
四、Dart 接口与渲染实现分析
这个库的分层很清晰:业务接口只有 HumanBodySelector 一个 widget,渲染与命中全部在 Dart 层的 svg_painter/ 里完成;platform_interface 和 method_channel 两个文件是 flutter create 插件模板自带的标准结构,只有 getPlatformVersion 一个模板方法,没有任何业务调用。读源码时可以先跳过这两个文件,把注意力放在 SVG 解析和绘制上。
本例的对应关系如下:
| Dart 入口或模型 | 通道协议 | OHOS 实现 | 应保持的行为 |
|---|---|---|---|
HumanBodySelector(...) widget | 无 | 无 | 解析 SVG、绘制、命中全部在 Flutter 层 |
Parser.instance.svgToCityList(map) | 无 | 无 | 从包资源读取 SVG,正则提取并解析路径 |
onChanged(parts, active) 回调 | 无 | 无 | 选中列表变化时回调,参数为 List<BodyPart> |
HumanBodySelectorPlatform.getPlatformVersion() | human_body_selector / getPlatformVersion | 返回 OpenHarmony <系统全称> | 模板方法,Dart 业务层无调用方 |
原生端需要保持通道名与方法名与 Dart 一致。本库没有需要桥接的系统能力,ArkTS 侧除了模板方法之外不需要实现任何东西。
4.1 跨端架构与调用时序
整库只有一条通道,且它只在模板接口层出现:
实线部分是鸿蒙上真正执行的路径:HumanBodySelector 在 initState 的 postFrameCallback 里解析当前地图与背面地图,之后每次点击只是对已解析的 Path 做填充切换,不再碰 SVG 文件。虚线部分是插件模板的通道层,业务代码不会经过它。
4.1.1 一次点选的时序
正面与背面两份地图在首次进入时一次性解析完成,切换正面/背面只是在两份列表之间挑选,因此切换没有可感知的延迟。
4.2 数据模型:BodyPart 与地图资源
BodyPart 是库的核心模型,用 equatable 以 id 判等:
class BodyPart extends Equatable {
String id;
String title;
Path path;
Path? transformedPath;
int painLevel = 0;
void transform(Matrix4 matrix) =>
transformedPath = path.transform(matrix.storage);
List<Object?> get props => [id];
}
4 张地图的规模如下(id + title 成对出现的 <path> 数):
| 地图文件 | 视角 | 可交互部位 |
|---|---|---|
human.svg | 女性正面 | 35 |
human1.svg | 女性背面 | 29 |
male.svg | 男性正面 | 34 |
male1.svg | 男性背面 | 29 |
title 是英文部位名(head、chest、breast、left forearm、groin 等),会原样出现在 onChanged 回调的 BodyPart.title 里;中文应用需要在业务层做一层映射。SVG 根元素的 height="1320" width="726" 被 Parser 读取为地图边界,比逐路径取 bounds 更稳定。
选中色有两种来源:传入 selectedColor 时整个选中部位用该色填充;不传时按 painLevel 从组件内置的 11 色渐变(0xff9ddefe 浅蓝到 0xffe72526 深红)取色,配合 updateSelectionColor 可以做出"拖动滑杆调疼痛等级、人体图实时变色"的交互。
4.3 渲染与命中:SvgPainter
每个部位是 Stack 里的一个 CustomPaint,SvgPainter 的绘制逻辑分四层:
if (isSelected) {
canvas.drawPath(city.path, selectedPen); // 选中填充
if (isFocused) {
// focus 部位再叠一层黑色外发光
maskPaint.maskFilter = MaskFilter.blur(BlurStyle.outer, 12);
canvas.drawPath(city.path, maskPaint);
}
} else {
canvas.drawPath(city.path, fillPen); // 未选:白色半透明
}
canvas.drawCircle(bounds.center, 3.0, redDot); // 部位中心蓝点
canvas.drawPath(city.path, pen); // 白色描边
命中测试在 hitTest 中做逆缩放变换后调用 Path.contains:
bool hitTest(Offset position) {
double inverseScale = sizeController.inverseOfScale(scale);
return city.path.contains(position.scale(inverseScale, inverseScale));
}
由于每个部位都是独立的 GestureDetector,重叠区域(例如手与前臂交界)会由路径包含关系精确判定,这是矩形按钮做不到的。shouldRepaint 恒为 true,配合可选的 repaint Listenable 使用;本库部件数量在 35 以内,全量重绘的开销可以忽略。
4.4 平台接口与两个单例的注意事项
HumanBodySelectorPlatform 是标准的 federated 插件接口:继承 PlatformInterface,用 _token 防止外部误继承,默认实例是 MethodChannelHumanBodySelector。库内没有任何地方调用它的方法,保留这层结构只是为了符合插件模板的形态。
有两个单例需要留意。一是 Parser,它缓存解析结果吗?不缓存——每次 svgToCityList 都重新读文件、重新正则、重新解析;好在 HumanBodySelector 只在 initState 里调两次,运行期不再触发。二是 SizeController,它以单例方式汇总所有 addBounds 调用的并集并算出 mapSize 与缩放比例。同一页面放两个 HumanBodySelector 时,两个实例会共享同一个 SizeController,后解析的地图会扩大边界、影响前者的缩放计算。单地图页面没有问题;多地图并存时,建议分两次路由进入,或接受缩放按较大地图计算的结果。
4.5 plugin_platform_interface 为什么必须显式声明
适配中发现的一个真实问题:flutter analyze 报
error - Undefined name 'PlatformInterface' - lib\human_body_selector_platform_interface.dart:12
原因是 lib/human_body_selector_platform_interface.dart 第一行 import 了 package:plugin_platform_interface/plugin_platform_interface.dart,但 pubspec.yaml 的 dependencies 里没有它——上游从没在装有完整依赖链的环境外跑过 analyze,而这个包也不被 svg_path_parser、path_drawing、equatable 中的任何一个传递引入。补上声明即可:
dependencies:
svg_path_parser: ^1.1.1
path_drawing: ^1.0.1
equatable: ^2.0.3
plugin_platform_interface: ^2.1.8
flutter:
sdk: flutter
这类"模板代码引用了未声明依赖"的问题只影响静态检查与解析,不影响已编译产物(Flutter 工具在构建 example 时会从别处解析到同名包),但作为发布到鸿蒙仓库的插件,依赖声明必须完整。
五、补全 OHOS 原生实现与工程配置
5.1 在 HumanBodySelectorPlugin.ets 中补全插件壳
业务侧不直接接触 ArkTS。本库没有需要桥接的系统能力,ArkTS 侧要做的是实现 FlutterPlugin 生命周期、注册一个与 Dart 侧同名的通道,并对模板方法给出返回值。
原生插件位于:
ohos/src/main/ets/components/plugin/HumanBodySelectorPlugin.ets
完整实现 47 行:
import deviceInfo from '@ohos.deviceInfo';
import {
FlutterPlugin,
FlutterPluginBinding,
MethodCall,
MethodCallHandler,
MethodChannel,
MethodResult,
} from '@ohos/flutter_ohos';
export default class HumanBodySelectorPlugin
implements FlutterPlugin, MethodCallHandler {
private channel: MethodChannel | null = null;
getUniqueClassName(): string {
return "HumanBodySelectorPlugin"
}
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.channel = new MethodChannel(
binding.getBinaryMessenger(), "human_body_selector");
this.channel.setMethodCallHandler(this)
}
onDetachedFromEngine(binding: FlutterPluginBinding): void {
if (this.channel != null) {
this.channel.setMethodCallHandler(null)
this.channel = null
}
}
onMethodCall(call: MethodCall, result: MethodResult): void {
try {
if (call.method == "getPlatformVersion") {
// Mirrors Android ("Android <release>") / iOS ("iOS <systemVersion>").
result.success(`OpenHarmony ${deviceInfo.osFullName}`);
} else {
result.notImplemented()
}
} catch (e) {
result.error("PLUGIN_ERROR",
`human_body_selector: failed to handle ${call.method}`, null)
}
}
}
几个要点:
- 通道名
"human_body_selector"必须与lib/human_body_selector_method_channel.dart中的写法逐字一致; getPlatformVersion返回OpenHarmony <osFullName>,对齐 Android/iOS 模板的"<os> <version>"格式;- 其余方法统一
notImplemented,不要静默返回空值; try/catch把原生异常转成PLUGIN_ERROR错误码,避免调用方挂住。
5.2 声明插件和宿主权限
本库不需要任何系统权限:SVG 从应用包内读取,绘制与命中都在 Flutter 渲染层完成,原生侧唯一的通道方法读取的是系统版本属性。
5.2.1 插件 HAR 的权限
ohos/src/main/module.json5 保持模板内容即可,不需要 requestPermissions:
{
"module": {
"name": "human_body_selector",
"type": "har",
"deviceTypes": ["default", "tablet"]
}
}
5.2.2 应用 entry 的权限
example/ohos/entry/src/main/module.json5 里有一条模板自带的 ohos.permission.INTERNET:
"requestPermissions": [
{ "name": "ohos.permission.INTERNET" }
]
这条声明与人体图的选择、绘制都无关,删掉不影响示例运行。保留时也需要注意:权限声明和运行时授权是两个步骤,module.json5 中的声明不会自动完成授权。
5.3 注册并导出插件
pubspec.yaml 通过以下配置声明 OHOS 插件类:
flutter:
plugin:
platforms:
android:
package: cs.nizam.human_body_selector
pluginClass: HumanBodySelectorPlugin
ios:
pluginClass: HumanBodySelectorPlugin
ohos:
pluginClass: HumanBodySelectorPlugin
assets:
- assets/
assets 段与 plugin 段同级,4 张 SVG 以 packages/human_body_selector/assets/human.svg 的包资源路径被 Parser 读取(对应 lib/svg_painter/constant.dart 里的 ASSETS_PATH)。结构补全时如果 diff 显示 assets: 段被移动或删除,必须还原,否则运行时报资源加载失败。
插件的 ohos/index.ets 需要导出实现:
import HumanBodySelectorPlugin from './src/main/ets/components/plugin/HumanBodySelectorPlugin';
export default HumanBodySelectorPlugin;
执行 flutter pub get 和构建后,Flutter 工具会为应用生成插件注册代码。本例生成的注册文件包含两个插件:
flutterEngine.getPlugins()?.add(new IntegrationTestPlugin());
flutterEngine.getPlugins()?.add(new HumanBodySelectorPlugin());
IntegrationTestPlugin 来自 example 的 integration_test 开发依赖,与插件功能无关。通常不应手工编辑 GeneratedPluginRegistrant.ets,因为下次构建可能覆盖它。
注册异常的排查步骤见第九节 MissingPluginException。
5.4 检查 example 的 OHOS 应用结构
本例的 example/ohos/build-profile.json5 应在 products 中设置版本。下面是需核对的配置片段,请合并到现有工程;其中 signingConfig: "default" 需要与本机配置的签名名称一致,签名材料保留在本地。
{
"app": {
"products": [
{
"name": "default",
"signingConfig": "default",
"compatibleSdkVersion": "6.1.1(24)",
"runtimeOS": "HarmonyOS",
"targetSdkVersion": "26.0.0"
}
]
},
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [
{
"name": "default",
"applyToProducts": ["default"]
}
]
}
]
}
配置后,在 DevEco Studio 中执行一次 Sync Project。如果 entry 模块正常识别,Project 视图中会出现 entry,并能打开 entry 模块的签名配置。
本例 AppScope/app.json5 里的 bundleName 沿用了同系列示例的 com.example.color_system_example。签名 Profile 是按包名签发的,改包名必须同时换一份匹配的 Profile,否则 SignHap 会失败;如果要把示例发布成独立应用,记得把这两处一起换掉。
六、补全交付文件并提交适配分支
6.1 除代码外还要补全哪些文件
代码适配完成后,还需要整理安装说明、接口文档、版本记录和开源信息。接收仓库有专用模板时,按其格式填写:
| 文件 | 应写清楚的内容 |
|---|---|
README.OpenSource | 上游名称、源码地址、适配版本或提交、版权及许可证信息;按仓库模板列出第三方依赖 |
README.md | 原项目说明、OHOS 支持入口、配套 Demo 和文档链接;保留上游信息 |
README.OpenHarmony_CN.md | 简介、AtomGit 安装方式、版本对应关系、环境约束、权限、接口表、示例、已验证范围和遗留问题 |
README.OpenHarmony.md | 与中文说明对应的英文文档 |
CHANGELOG.OpenHarmony.md | OHOS 新增能力、适配版本、兼容限制与测试范围 |
LICENSE / NOTICE | 保留上游许可证;NOTICE 按许可证和原项目要求保留或补充 |
example/README.md | 依赖方式、运行目录、签名、操作步骤与效果图;覆盖点选、长按、多选与配色 |
pubspec.yaml、ohos/oh-package.json5 | 核对包名、版本、插件注册、仓库地址、许可证和依赖 |
.gitignore | 忽略构建缓存及本机签名材料,不漏提交必要源码和配置 |
README.OpenSource 记录库本身的来源与版本。本例的包名为 human_body_selector,版本为 0.0.1,采用 MIT 许可证;Flutter 和 HarmonyOS SDK 版本写入环境说明。两份 README.OpenHarmony 里的依赖地址、TAG 名称和环境版本要与最终提交保持一致——本次适配就校正过三处:组织地址从 CPF-Flutter 改为 oh-flutter,TAG 从未发布的 0.0.1-ohos-1.0.0-beta.1 改为 0.0.1-ohos-1.0.0,DevEco/SDK/ROM 版本行对齐真机实测值。
6.2 提交前检查
提交前先完成第八节的插件、example 和真机测试,再从根目录检查改动:
git branch --show-current
git diff --check
git status --short
git diff --stat
git diff
检查 diff 中的接口、平台注册和依赖变化,移除本机路径及签名信息,并使文档中的版本和分支与提交内容一致。
6.3 提交并推送到 AtomGit
文档和代码整理完成后,在根目录暂存并提交。文件名按项目实际情况调整:
git add ohos pubspec.yaml example test assets .gitignore
git add README.md README.OpenSource README.OpenHarmony_CN.md
git add README.OpenHarmony.md CHANGELOG.OpenHarmony.md
git diff --cached --check
git diff --cached --stat
git commit -m "feat: add OHOS support for human_body_selector 0.0.1"
git push -u origin feat/ohos_human_body_selector_0.0.1
本次适配实际拆成了四个提交:3100a52 补 ohos 结构、插件壳与交互式示例页,d4fc1f8 把 compatibleSdkVersion 对齐到真机的 6.1.1(24),351d632 补 plugin_platform_interface 声明并重写两个坏掉的模板测试,45d1705 修正演示页在切换选项后已选文案与画面不同步的问题。TAG 0.0.1-ohos-1.0.0 打在 45d1705 上。
DevEco 可能向 example/ohos/build-profile.json5 写入本机签名配置,需要在提交前从暂存内容中移除。推送时,origin 应指向自己的 AtomGit 仓库。推送后在 AtomGit 发起合并请求,说明上游来源和版本、OHOS 实现范围、依赖及权限、测试环境、操作结果、已知限制,并附 Demo 运行图。目标分支和评审流程以接收仓库要求为准。
七、使用根目录 example 演示接入
仓库自带 example/,可以直接用来调试插件和体验人体部位选择。
7.1 本地适配时使用路径依赖
当前 example/pubspec.yaml 的依赖是:
dependencies:
flutter:
sdk: flutter
human_body_selector:
path: ../
../ 相对于 example/pubspec.yaml 指向插件根目录,修改根目录插件后可直接联调。本库没有需要额外补的 OHOS 依赖——它不使用 shared_preferences 之类的需要平台实现的包,全部依赖都是纯 Dart 包。
从插件根目录执行:
cd example
flutter pub get
flutter pub deps
检查依赖树中 human_body_selector 指向本地路径,且 svg_path_parser、path_drawing、equatable、plugin_platform_interface 都能正常解析。
7.2 通过 AtomGit 引入插件
业务应用通过 AtomGit 引入时,将 path 配置替换为下面的 Git 依赖。这里固定到本文使用的 TAG:
dependencies:
flutter:
sdk: flutter
human_body_selector:
git:
url: https://atomgit.com/oh-flutter/human_body_selector.git
ref: 0.0.1-ohos-1.0.0
使用自己的适配版本时,先推送分支,再将 ref 改为 feat/ohos_human_body_selector_0.0.1。正式发布后可固定到 tag 或 commit。
7.3 调用接口实现部位选择
下面的页面展示一个最小可用的人体图,可用于 example/lib/main.dart:
import 'package:flutter/material.dart';
import 'package:human_body_selector/human_body_selector.dart';
import 'package:human_body_selector/svg_painter/maps.dart';
void main() => runApp(const MaterialApp(home: BodyPage()));
class BodyPage extends StatefulWidget {
const BodyPage({super.key});
State<BodyPage> createState() => _BodyPageState();
}
class _BodyPageState extends State<BodyPage> {
final List<String> _selected = [];
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('人体部位选择')),
body: Center(
child: SingleChildScrollView(
child: HumanBodySelector(
map: Maps.HUMAN,
multiSelect: true,
selectedColor: Colors.redAccent,
strokeColor: Colors.blueGrey,
dotColor: Colors.blue,
width: 260,
height: 480,
onChanged: (parts, active) => setState(() {
_selected
..clear()
..addAll(parts.map((p) => p.id));
}),
onLevelChanged: (_) {},
),
),
),
bottomNavigationBar: SafeArea(
child: Padding(
padding: const EdgeInsets.all(12),
child: Text('已选 ${_selected.length} 个部位'),
),
),
);
}
}
onChanged 的第一个参数是当前选中列表,第二个是最近一次点中的部位(未选中任何部位时为 null);onLevelChanged 在选中部位的 painLevel 变化时触发,配合 updateSelectionColor 使用,单纯做选择可以传空实现。
仓库中的完整 Demo 还提供 Human/Male 地图切换、多选开关、四种配色、启用开关和已选部位文案:
HumanBodySelector(
key: ValueKey('$_map-$_multiSelect-$_enabled-$_selectedColor'),
map: _map,
multiSelect: _multiSelect,
enabled: _enabled,
selectedColor: _selectedColor,
...
)
这里有一个容易踩的坑:ValueKey 里拼进了所有选项,任何一项变化都会让 Flutter 销毁重建组件,HumanBodySelector 内部的选中状态随之清零。如果业务层自己也维护一份已选列表(比如底部文案),必须同步清空,否则会出现"画面上没有选中部位、文案却显示已选 3 个"的不一致。演示页为此封装了一个 _applyOption:先执行选项变更,再清空父层列表。
7.4 部位名的本地化
BodyPart.title 是 SVG 里的英文原文。中文界面在 onChanged 回调里做一层映射即可,无需改动库:
const partNames = {
'head': '头部', 'chest': '胸部', 'breast': '乳房', 'groin': '腹股沟',
'left forearm': '左前臂', 'right forearm': '右前臂',
// ...按业务需要补全
};
正面与背面的部位集合不同(35/29/34/29),映射表建议按地图分开维护,或对未收录的 title 降级显示原文。
八、验证、构建与鸿蒙设备运行效果
8.1 分别验证插件与 example
从插件仓库根目录执行:
flutter pub get
flutter analyze
flutter test
cd example
flutter pub get
flutter analyze
flutter test
本机实测结果:
- 插件
flutter analyze:0 error、0 warning,20 条 info。其中 7 条prefer_const_constructors/prefer_const_literals_to_create_immutables类提示集中在lib/human_body_selector.dart和lib/svg_painter/maps.dart,2 条unnecessary_type_check在size_controller.dart,另有若干unnecessary_import、unnecessary_string_escapes、unnecessary_this风格提示;2 条deprecated_member_use(setMockMethodCallHandler)在test/human_body_selector_test.dart,是 Flutter 3.9 后该 API 换了调用入口。这些属于上游代码风格,适配没有顺手清理,避免混入与平台无关的改动。修复plugin_platform_interface之前,这里还有 1 条Undefined name 'PlatformInterface'的 error,见 4.5。 - example
flutter analyze:No issues found。适配中重写了两个坏掉的模板测试,widget_test.dart改为验证HumanBodySelector能被正常构建并渲染部位蓝点,integration_test/plugin_integration_test.dart改为通过真实平台接口调用getPlatformVersion。 flutter test:本机工具链无法完成。flutter_tester启动后报Invalid SDK hash直接挂住(与此前几篇库的情况相同),因此测试用例本次未执行,插件行为以真机验证为准。仓库里的test/保留了重写后的用例,在环境正常的机器上可以直接运行。
Dart 测试覆盖接口层,部位选择的真实交互需要在鸿蒙设备上验证,这也是第八节其他小节的主要内容。
8.2 确认设备连接
hdc list targets
flutter devices
设备首次连接电脑时,需要在手机端确认调试授权。列表为空时,检查 USB 连接、调试模式和电脑授权。
8.3 配置签名
真机安装的 HAP 通常需要有效签名。推荐使用 DevEco Studio 为 entry 模块配置自动签名:
- 用 DevEco Studio 打开
example/ohos,不是仓库根目录; - 等待工程 Sync 成功,确认 Project 视图中存在
entry模块; - 打开 File > Project Structure > Signing Configs;
- 为
defaultproduct 选择或生成签名; - 确认设备、应用包名、证书和 Profile 匹配;
- 再回到终端执行 Flutter 构建或运行。
签名材料保存在本机,公开仓库中只保留构建所需的通用配置。
8.4 运行示例
以下命令在 example/ 目录执行,将 <device-id> 替换为设备列表中的实际 ID:
flutter run -d <device-id>
也可以先构建 HAP:
flutter build hap --debug
典型产物位于:
example/ohos/entry/build/default/outputs/default/
目录中通常包含已签名和未签名 HAP。真机安装应选择与当前设备匹配的已签名产物。
本次适配时,flutter build hap --debug 在本机停在 hvigor 的 ohpm install 阶段,报 00306053 Specification Limit Violation 和 BATCH RECURSION exceeds STACK limits。这种情况下可以先手动装依赖,再直接调 hvigor:
cd example/ohos
"D:/Program Files/Huawei/DevEco Studio/tools/ohpm/bin/ohpm" install
然后在 PowerShell 中补上 JAVA_HOME 再执行(路径含空格,用 Bash 直接调 .bat 容易失败):
$env:NODE_OPTIONS=""
$env:JAVA_HOME="D:\Program Files\Huawei\DevEco Studio\jbr"
$env:PATH="$env:JAVA_HOME\bin;" + $env:PATH
cd D:\flutter\lib2\human_body_selector\example\ohos
& "D:\Program Files\Huawei\DevEco Studio\tools\hvigor\bin\hvigorw.bat" assembleHap -p product=default -p buildMode=debug --no-daemon
两个细节:$env:NODE_OPTIONS="" 用于清空本机注入的 Node 安全钩子——保留它时 hvigor 批量删除构建缓存会报 SAFE_DELETE_BULK_CONFIRM_REQUIRED 并以 COMPILE RESULT:FAIL 结束,容易被误判成 ArkTS 语法错误;另外,本机环境下构建结束阶段 hvigor 调用 wmic.exe 采集系统信息会被安全策略拦截,PowerShell 显示退出码 1,但 BUILD SUCCESSFUL 已经打出、HAP 已正常产出,以产物文件为准即可。
完成后产物出现在同一路径下,用 hdc install 安装即可:
hdc -t <device-id> install -r example/ohos/entry/build/default/outputs/default/entry-default-signed.hap
这条绕行方式是为了绕开本机 ohpm 的环境问题,正式发布仍应走 flutter build hap。
8.5 在设备上测试部位选择
- 打开应用,确认首页渲染出女性正面人体图,每个部位中心有一个蓝色定位点;
- 点击胸部,确认部位填充为红色且出现黑色 focus 光晕,底部文案显示"已选 1 个部位";
- 依次点击腹部、大腿,确认多选累加,文案计数同步增加;
- 点击配色行的 Teal,确认已选部位重新渲染为 teal 色(选项变化会重置选中,重新点几个部位验证);
- 切换到 Male 地图,确认男性正面图加载正常,点选部位有效;
- 关闭"多选模式",连续点击三个部位,确认每次点击只保留最后一个;
- 长按已选部位,确认该部位被移除、恢复白色半透明填充;
- 点击右上角眼睛图标关闭启用状态,再点击任意部位,确认无任何响应,底部文案不变;
- 重新打开启用状态并切到 Orange 配色,一次选中六个部位,确认填充与计数都正确。
本文已在真机完成第 1–9 步(见 8.6,设备 TLR-AL00 / API 24)。
8.6 鸿蒙设备运行效果
完成适配后,HumanBodySelector 在鸿蒙设备上可以正常加载 4 张 SVG 地图并完成全部交互:点选、多选、单选替换、长按移除、禁用与配色切换的行为与 Android/iOS 一致。
首页与多选基础交互:
| 示例首页 | 多选三个部位 | Teal 配色 |
|---|---|---|
| 女性正面图,部位中心蓝色定位点 | 胸、腹、大腿填充 redAccent,底部计数同步 | 选项重置后重新选中,填充为 teal |
Male 地图与单选模式:
| Male 地图 | 单选模式 | 长按移除 |
|---|---|---|
| 第二套 SVG 资源正常加载,选中有效 | 连续点击三个部位,只保留最后一个 | 长按后部位恢复未选状态 |
禁用与多配色综合验证:
| 禁用态 | Orange 多选 |
|---|---|
enabled: false 后点击部位无响应,文案保持"点击身体部位进行选择" | 一次选中 6 个部位,填充与计数一致 |
整个验证过程未出现任何平台相关的异常:SVG 资源通过包路径正常加载,CustomPaint 的路径填充、focus 光晕与蓝点渲染与 Android 模拟器上的表现一致,手势命中在重叠区域的表现也符合预期。
九、FAQ:适配过程与使用问题
9.1 Missing SDK components
典型错误如下:
Missing SDK components. SDK path: ...,
missing components: toolchains,ets,js,native,previewer.
这个错误发生在 Hvigor 同步阶段。通常需要检查构建工具使用的 SDK 路径、组件是否完整,以及 Hvigor 与 SDK 的版本是否匹配。
处理顺序:
- 在 DevEco Studio SDK Manager 中确认 API 26 组件已经下载完整;
- 检查 Flutter 和 DevEco Studio 使用的 SDK 路径是否一致;
- 避免误用
/Applications/DevEco-Studio.app/Contents/sdk之类的不完整目录; - 确认 SDK 根目录下存在
toolchains、ets、js、native、previewer; - 执行
flutter config --ohos-sdk <正确路径>; - 重新执行
flutter doctor -v和 DevEco Studio Sync。
因为 Sync 和 Compile 首先读取本机 SDK。手机 API 版本只在部署、安装和运行时参与兼容判断。即使完全不连接手机,本地 SDK 不完整时也会得到相同错误。
当前工程的 compatibleSdkVersion 是 6.1.1(24),因此 API 24 设备在安装版本门槛上是满足的;人体部位选择器不依赖任何设备专属能力,装上即可使用。
9.2 DevEco Studio 中看不到 entry 模块
插件的 ohos/ 目录是 HAR 模块,可安装应用的 entry 模块位于 example/ohos/entry。
请直接使用 DevEco Studio 打开:
human_body_selector/example/ohos
如果仍看不到 entry,先解决 SDK Sync 错误,再检查 example/ohos/build-profile.json5 的 modules 是否包含 ./entry。同步失败时,Project Structure 无法正确解析模块,签名界面也可能不显示 entry。
9.3 无法手动签名
签名配置依附于可构建的应用模块和 product。只有 HAR 插件模块、工程 Sync 失败,或者打开了错误目录时,DevEco Studio 都可能无法提供 entry 签名入口。
建议先确认:
- 打开的是
example/ohos; - SDK 组件完整并且 Sync 成功;
entry的模块类型为entry;defaultproduct 和 target 已正确关联;- 当前账号、证书和调试设备状态有效。
9.4 人体图显示空白,或部位点击没有反应
按以下顺序检查:
- 先看日志里有没有资源加载报错。4 张 SVG 走
packages/human_body_selector/assets/包资源路径加载,pubspec.yaml的flutter: assets:段如果被删(flutter create补全工程时有过覆盖案例),rootBundle.loadString会直接抛异常,整个Stack是空的; - 确认
map传入的是Maps.HUMAN/Maps.HUMAN1/Maps.MALE/Maps.MALE1之一——这些常量就是文件名,传其他字符串同样报资源错误; - 检查
enabled是否为false,以及外层有没有包一层AbsorbPointer; - 组件尺寸是否给够:
width/height默认double.infinity,放在无约束的Row里会布局失败; demo 用width: 260, height: 480配合SingleChildScrollView是比较稳的写法; - 点了没反应但图正常时,确认点的是部位轮廓内部——
hitTest用的是路径包含判定,部位之间的空隙(例如两腿之间)不属于任何路径。
9.5 MissingPluginException
这通常表示 Dart 通道找不到已注册的原生插件。新增原生插件后需要重新构建应用。从仓库根目录执行:
cd example
flutter clean
flutter pub get
flutter run -d <device-id>
如果仍然出现,检查自动生成的插件注册文件中是否包含 HumanBodySelectorPlugin,同时核对 pubspec.yaml 的 flutter.plugin.platforms.ohos、ohos/index.ets 和 oh-package.json5。
需要说明的是:本库的部位选择功能不走通道,即使插件未注册,人体图加载、点选、长按也全部正常——这也是验证插件壳是否注册成功的最简单方式。真正受 MissingPluginException 影响的只有模板方法 getPlatformVersion。
9.6 flutter analyze 报 Undefined name 'PlatformInterface'
上游 pubspec.yaml 漏声明了 plugin_platform_interface,而 lib/human_body_selector_platform_interface.dart 直接 import 了它,且这个包不被库的任何依赖传递引入。处理方式见 4.5:在 dependencies 中补 plugin_platform_interface: ^2.1.8,重新 flutter pub get。
从配套仓库的 0.0.1-ohos-1.0.0 起该依赖已补齐;如果基于更早的提交或上游原始代码适配,需要自己加上。
9.7 编译成功但安装失败
常见原因包括:
- HAP 未签名或使用了错误的 Profile;
- 设备未加入调试设备列表;
- 包名与签名 Profile 不匹配;
- 安装包的
compatibleSdkVersion高于设备 API; - 手机上已经安装了使用不同证书签名的同包名应用。
示例工程的 bundleName 是 com.example.color_system_example,与同系列其他示例相同。如果设备上已经装过同包名、不同证书的应用,安装会失败,需要先卸载旧应用或换一个包名(换包名要同时换签名 Profile)。
9.8 flutter create 不认识 ohos,或包名不合法
先执行 flutter --version,确认使用的是 OHOS 版工具链,环境配置回到第二节的社区链接核对。本例仓库目录名与 Dart 包名相同,--project-name human_body_selector 可以省略;适配其他目录名与包名不一致的库时要显式传入。生成后检查 diff,确认 assets: 段仍在,再补充 ArkTS 插件壳。
9.9 AtomGit 依赖提示找不到分支或无权限
先检查 URL 是否指向已同步的目标仓库,再确认 feat/ohos_human_body_selector_0.0.1 已推送。分支尚未推送时,可以先使用第七节的 TAG 0.0.1-ohos-1.0.0 或提交号 45d1705。私有仓库还需在本机配置 Git 认证。
9.10 改了本地 ArkTS 或 SVG 资源,Demo 为什么没变化
先检查 example/pubspec.yaml:Git 依赖读取远程提交,不会自动读取本地插件改动。本地联调切回 path: ../;测试远程版本则先提交推送,再更新依赖并核对 pubspec.lock 的 resolved-ref。原生代码变动后停止应用并重新构建运行,不能只做 Dart 热重载;SVG 资源走 rootBundle,部分情况下热重载也不会重新读取,稳妥起见同样完整重启。
9.11 同一页面放两个 HumanBodySelector,缩放互相影响
SizeController 是单例(见 4.4),所有实例的 addBounds 都写进同一份边界数据,mapSize 取的是所有调用结果的并集。两个地图尺寸不同(本库 4 张图的 SVG 根元素尺寸一致,但自定义替换 SVG 时可能不同)时,后解析的地图会扩大并集边界,先进入的地图按新边界重新计算缩放,视觉上表现为"突然变小"。
处理思路(按改动量从小到大):
- 业务上避免同屏两个地图:正面/背面切换用
map参数切换,而不是放两个组件; - 把
SizeController的单例改成随Parser调用传递的实例,让每个HumanBodySelector持有自己的尺寸计算——这需要改动svg_painter/的多个文件,属于上游重构,本次适配没有做。
相关链接
更多推荐



所有评论(0)