开发工具: 华为云码道

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() 做逐路径判定。

KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页

选中的部位会整体填充为指定颜色,色值可以按 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 SDK3.47.7-ohos-1.0.0Flutter 编译与 OHOS 平台工具链
Flutter 分支oh-3.47-devoh-flutter 对应开发分支
Dart SDK3.13.3Dart 语言与包管理环境
DevEco Studio26.0.0(26.0.0.851)工程 Sync、签名配置与 hvigor 构建
HarmonyOS 开发套件7.0.0(API 26)开发套件版本及对应的 API 级别
compileSdkVersion26.0.0编译时使用的 SDK API
targetSdkVersion26.0.0应用面向的行为版本
compatibleSdkVersion6.1.1(24)当前工程声明的最低兼容版本
插件版本0.0.1pubspec.yaml 中的包版本
原生语言ArkTSHarmonyOS 插件实现
插件产物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/*.svg4 张人体地图,每个 <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.dartCustomPainter:描边、填充、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

Parser 单例

rootBundle 读取 SVG

path_drawing + svg_path_parser 解析 Path

SizeController 单例

Stack + CustomPaint

SvgPainter 绘制与 hitTest

HumanBodySelectorPlatform

MethodChannel human_body_selector

ArkTS HumanBodySelectorPlugin

实线部分是鸿蒙上真正执行的路径:HumanBodySelector 在 initState 的 postFrameCallback 里解析当前地图与背面地图,之后每次点击只是对已解析的 Path 做填充切换,不再碰 SVG 文件。虚线部分是插件模板的通道层,业务代码不会经过它。

4.1.1 一次点选的时序
SvgPainter Parser HumanBodySelector 业务页面 SvgPainter Parser HumanBodySelector 业务页面 postFrameCallback 中完成,只做一次 多选累加 / 单选替换 / 长按移除 放入组件(map/multiSelect/selectedColor…) svgToCityList(正面地图) List<BodyPart>(路径已解析) svgToCityList(背面地图) 点击某部位 GestureDetector.onTap hitTest: path.contains(逆缩放坐标) onChanged(selectedPartsList, lastSelected)

正面与背面两份地图在首次进入时一次性解析完成,切换正面/背面只是在两份列表之间挑选,因此切换没有可感知的延迟。

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.mdOHOS 新增能力、适配版本、兼容限制与测试范围
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 模块配置自动签名:

  1. 用 DevEco Studio 打开 example/ohos,不是仓库根目录;
  2. 等待工程 Sync 成功,确认 Project 视图中存在 entry 模块;
  3. 打开 File > Project Structure > Signing Configs;
  4. 为 default product 选择或生成签名;
  5. 确认设备、应用包名、证书和 Profile 匹配;
  6. 再回到终端执行 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 在设备上测试部位选择

  1. 打开应用,确认首页渲染出女性正面人体图,每个部位中心有一个蓝色定位点;
  2. 点击胸部,确认部位填充为红色且出现黑色 focus 光晕,底部文案显示"已选 1 个部位";
  3. 依次点击腹部、大腿,确认多选累加,文案计数同步增加;
  4. 点击配色行的 Teal,确认已选部位重新渲染为 teal 色(选项变化会重置选中,重新点几个部位验证);
  5. 切换到 Male 地图,确认男性正面图加载正常,点选部位有效;
  6. 关闭"多选模式",连续点击三个部位,确认每次点击只保留最后一个;
  7. 长按已选部位,确认该部位被移除、恢复白色半透明填充;
  8. 点击右上角眼睛图标关闭启用状态,再点击任意部位,确认无任何响应,底部文案不变;
  9. 重新打开启用状态并切到 Orange 配色,一次选中六个部位,确认填充与计数都正确。

本文已在真机完成第 1–9 步(见 8.6,设备 TLR-AL00 / API 24)。

8.6 鸿蒙设备运行效果

完成适配后,HumanBodySelector 在鸿蒙设备上可以正常加载 4 张 SVG 地图并完成全部交互:点选、多选、单选替换、长按移除、禁用与配色切换的行为与 Android/iOS 一致。

首页与多选基础交互:

示例首页多选三个部位Teal 配色
女性正面图,部位中心蓝色定位点胸、腹、大腿填充 redAccent,底部计数同步选项重置后重新选中,填充为 teal

Male 地图与单选模式:

Male 地图单选模式长按移除
第二套 SVG 资源正常加载,选中有效连续点击三个部位,只保留最后一个长按后部位恢复未选状态

禁用与多配色综合验证:

KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页

禁用态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 的版本是否匹配。

处理顺序:

  1. 在 DevEco Studio SDK Manager 中确认 API 26 组件已经下载完整;
  2. 检查 Flutter 和 DevEco Studio 使用的 SDK 路径是否一致;
  3. 避免误用 /Applications/DevEco-Studio.app/Contents/sdk 之类的不完整目录;
  4. 确认 SDK 根目录下存在 toolchains、ets、js、native、previewer;
  5. 执行 flutter config --ohos-sdk <正确路径>;
  6. 重新执行 flutter doctor -v 和 DevEco Studio Sync。
为什么连接 API 24 手机仍然会报这个错误?

因为 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;
  • default product 和 target 已正确关联;
  • 当前账号、证书和调试设备状态有效。

9.4 人体图显示空白,或部位点击没有反应

按以下顺序检查:

  1. 先看日志里有没有资源加载报错。4 张 SVG 走 packages/human_body_selector/assets/ 包资源路径加载,pubspec.yaml 的 flutter: assets: 段如果被删(flutter create 补全工程时有过覆盖案例),rootBundle.loadString 会直接抛异常,整个 Stack 是空的;
  2. 确认 map 传入的是 Maps.HUMAN / Maps.HUMAN1 / Maps.MALE / Maps.MALE1 之一——这些常量就是文件名,传其他字符串同样报资源错误;
  3. 检查 enabled 是否为 false,以及外层有没有包一层 AbsorbPointer;
  4. 组件尺寸是否给够:width/height 默认 double.infinity,放在无约束的 Row 里会布局失败; demo 用 width: 260, height: 480 配合 SingleChildScrollView 是比较稳的写法;
  5. 点了没反应但图正常时,确认点的是部位轮廓内部——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 时可能不同)时,后解析的地图会扩大并集边界,先进入的地图按新边界重新计算缩放,视觉上表现为"突然变小"。

处理思路(按改动量从小到大):

  1. 业务上避免同屏两个地图:正面/背面切换用 map 参数切换,而不是放两个组件;
  2. 把 SizeController 的单例改成随 Parser 调用传递的实例,让每个 HumanBodySelector 持有自己的尺寸计算——这需要改动 svg_painter/ 的多个文件,属于上游重构,本次适配没有做。

相关链接

Logo

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

更多推荐