ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Flutter鸿蒙适配:polylabel实现复杂多边形标签居中对齐

Flutter鸿蒙适配:polylabel实现复杂多边形标签居中对齐 1. 从标签飞出多边形说起为什么需要 polylabel做地图开发的朋友大概率都遇到过这个尴尬场景一个形状不规则的小区地块、一片湖面、或者一个行政区边界程序根据顶点坐标算了个中心点把小区名称、湖泊标注挂上去——结果标签却跑到了多边形外面的马路上甚至飘进了隔壁地块。坐标上明明居中了视觉上却完全不是那么回事。这个问题的根源在于几何学上的中心不等于人眼感知的中心。凸多边形还好一旦碰上 L 形、U 形、月牙形、带内环空洞的多边形普通的质心centroid和包围盒中心都会失灵。质心是面积加权平均点但凹多边形和带洞多边形的质心完全可能落在图形外面包围盒中心就更不用说了一个西北-东南走向的细长地块包围盒中心可能落在离地块老远的地方。这时候就需要 polylabel 这类最大内切圆圆心算法出场。Mapbox 最早实现了 polylabel用来解决地图矢量瓦片里的多边形标签定位问题。它的核心目标不是找几何中心而是找多边形内部一个尽量居中、离所有边界都尽可能远的点——这个点在视觉上几乎总是一个多边形最适合放文字标签的位置。名字里的 poly 是多边形label 是标签直接点明了它的用途。我在 Flutter 项目里遇到这个需求是因为做的是一个跨平台地理围栏可视化应用服务端下发复杂区域边界客户端要在地图上把区域高亮同时把区域名称、围栏编号精准地放在区域内部不能压边界更不能漂到外面去。iOS 和 Android 端 Flutter 的表现都正常但一到鸿蒙 HarmonyOS 端适配就暴露了问题——原有的地图标注逻辑依赖的地图 SDK 在鸿蒙上的行为不一致且 Flutter 插件包对鸿蒙平台没有原生实现跑起来要么崩溃要么标签位置全乱。于是就有了这次适配实战。如果你也是做 Flutter 地图应用、或者需要在鸿蒙端绘制复杂多边形并做标注对齐这篇文章值得看完。我会先讲清楚 polylabel 的计算逻辑再给出一套可落地的鸿蒙适配方案和性能优化思路最后复盘几个很容易踩的坑。2. polylabel 核心算法拆解网格细分逼近视觉中心2.1 质心、内切圆与 polylabel 的差别先说个容易混淆的点polylabel 并不是算质心它是用网格搜索逼近多边形内部的最大内切圆圆心。人眼判断一个多边形哪里最居中其实是在直觉上找一个圆这个圆放大到不能再放大之前必须完全落在多边形内部。圆的圆心就是一个很好的标签锚点圆的半径决定了标签缩放的安全空间。这个圆叫最大内切圆Maximum Inscribed Circle它的圆心就叫极点pole of inaccessibility直译是最不可达点学术上叫内接圆圆心。pole of inaccessibility这个英文术语比较拗口国内通常翻译成视觉中心或极点。质心centroid是另一个概念三角形质心是三边中线的交点对任意多边形则通过顶点坐标或面积加权计算。质心计算快但没有任何约束保证它落在多边形内部。考虑一个简单的 L 形多边形——两个长方形拼在一起它的质心可能落在缺口处也就是图形外。而 polylabel 的结果天然带一个约束必然在多边形内部且离边界尽可能远。我用一个对比表格来说明三者的差异这也是我在项目里对团队科普用的版本方案计算开销是否保证在多边形内部视觉中心效果适用场景包围盒中心极低不保证最差细长/凹多边形完全不可用粗精度的大致定位质心加权低不保证一般凹多边形会偏移甚至出界简单凸多边形polylabel中高保证带洞也能保证不落入洞内好近似最大内切圆圆心地图标注、复杂形状2.2 从包围盒开始的网格贪心搜索polylabel 的基本思路并不玄乎理解它有助于你在项目里调整精度参数。整个过程可以拆成四步第一步求包围盒。先找出多边形的 minX / minY / maxX / maxY把整个多边形放进一个矩形里。第二步网格化拆分成候选单元 cell。以包围盒边长的一半为初始边长把包围盒划分为若干正方形网格。每个 cell 的中心都是一个候选的标签点。这是一个可能包含最优解的空间。第三步计算每个候选 cell 的得分。对每个 cell计算它的中心到多边形所有边的最近距离。如果中心落在多边形内部距离取正值如果在外部或落在洞里距离取负值。这个距离本质上就是以该候选点为圆心能画出的最大内切圆半径。第四步优先队列 二分细分逼近最优。把所有 cell 按中心距离从大到小放进优先队列每次取出得分最高的 cell。如果它的理论上限中心距离 cell 对角线长度的一半仍然大于当前全局最优解就把这个 cell 一分为四生成 4 个子 cell 重新计算得分再压回队列。这个过程反复执行直到队列耗尽或者所有 cell 得分接近全局最优解精度阈值控制最后返回得分最高的 cell 中心作为标签锚点。这里的关键不是暴力穷举而是用分支定界思路一个 cell 内任意一点都不可能比中心距离半对角线更远如果这个上限已经不如当前最优解那这个 cell 整体都可以剪枝不用再细分。这就是 polylabel 能在大多边形上快速收敛的原因。实际使用中polylabel 的函数签名通常是polylabel(polygonCoords, precision)传入多边形外环和洞的坐标数组以及一个精度参数默认 1.0单位与输入坐标一致。返回[x, y, distance]其中 distance 就是最大内切圆的近似半径。在 Flutter / Dart 生态里已经有纯 Dart 的 polylabel 实现pub.dev 上搜 polylabel_dart 或直接 import 源码算法本身是纯计算逻辑不依赖任何平台 API。这意味着算法本身放到鸿蒙端完全不需要改动——真正要适配的是把坐标数据安全高效地送进算法、再把算出的标注点同步到鸿蒙地图原生层渲染这一整条链路。3. 鸿蒙端适配难点不是说好的纯 Dart 就够了3.1 Flutter 在鸿蒙上的现状鸿蒙适配的第一个认知要点是Flutter 在 HarmonyOS / OpenHarmony 上的运行方式和 iOS / Android 不完全一样。当前主流做法是使用 OpenHarmony SIG 维护的 Flutter 兼容版本flutter_flutter 的 ohos 分支Dart 层的框架代码可以复用但原生插件plugin不存在自动兼容。大多数 Flutter 地图相关插件比如基于高德、百度、Mapbox 的插件都是通过 MethodChannel 调用 Android 的 Java/Kotlin 和 iOS 的 Object-C/Swift 代码。鸿蒙端既没有 .android 也没有 .ios 的注册入口如果你用的插件没有主动提供 ohos 目录那在鸿蒙真机上调用插件方法会直接抛 MissingPluginException。这正是我遇到的坑原有方案里地图渲染依赖一个社区地图插件这个插件在 Android 上通过原生 SDK 画面叠加和标注。到了鸿蒙端我其实有两条路可以走方案 A换用鸿蒙原生地图 SDK 重写整个地图组件再以 PlatformView 方式接入 Flutter。方案 B保留 Flutter 侧的 polylabel 计算逻辑地图底图仍用 Flutter 层自绘只通过 MethodChannel 把标注点位和文字传给鸿蒙原生层渲染最上层的文字图层。我最后选了方案 B 为主、方案 A 为辅。原因很实际polylabel 算法是纯 Dart坐标转换和缓存也可以全放在 Dart 侧真正需要鸿蒙原生介入的只有把标注点覆盖在地图纹理之上这一步。这样改动范围最小也最容易控制风险——核心计算不依赖平台平台差异被压缩到一个薄薄的图层通道里。3.2 三端结构pubspec、MethodChannel 与原生实现我最终搭建的鸿蒙端适配结构是这样的flutter_polylabel_ohos/ ├── lib/ │ ├── polylabel_engine.dart # 纯 Dart 算法封装与 isolate 调度 │ ├── label_placement_manager.dart # 标注对齐、避让与缓存 │ └── ohos_bridge.dart # MethodChannel 封装 ├── ohos/ │ └── main/ │ ├── ets/ │ │ ├── entryability/ │ │ └── pages/ │ │ └── Index.ets # 注册 FlutterPlugin 与渲染 Overlay │ └── module.json5pubspec.yaml 里需要注意鸿蒙插件的注册方式和 Android 类似需要在flutter_plugin_ohos相关的配置中声明插件入口。如果你是自己写鸿蒙侧的 FlutterPlugin需要在 ohos 工程里实现一个继承自FlutterPlugin的类并在 Module 的配置里注册。原生侧的核心代码用 ArkTS 写。比如我定义了一个方法通道处理器接收 Dart 侧传过来的标注点、文字内容和样式参数然后创建一个 RenderNode 挂到 Flutter 的 Overlay 上作为顶层文本层。核心代码大致是这个思路// LabelOverlayPlugin.ets export class LabelOverlayPlugin implements FlutterPlugin { private channel: MethodChannel | null null; onAttachedToEngine(binding: FlutterPluginBinding): void { this.channel new MethodChannel(binding.getBinaryMessenger(), polylabel_ohos/overlay); this.channel.setMethodCallHandler((call, result) { if (call.method drawLabel) { const args call.arguments as DrawLabelArgs; // 在地图图层之上创建文本 RenderNode // 设置坐标、内容、字号、颜色等 result.success(true); } else if (call.method clearLabels) { // 清理当前所有标注 result.success(true); } }); } onDetachedFromEngine(binding: FlutterPluginBinding): void { this.channel?.setMethodCallHandler(null); this.channel null; } }Dart 侧的调用封装也很薄class OhosLabelBridge { static const _channel MethodChannel(polylabel_ohos/overlay); static Futurevoid drawLabel({ required Listdouble position, required String text, MapString, dynamic style const {}, }) async { await _channel.invokeMethod(drawLabel, { x: position[0], y: position[1], text: text, style: style, }); } }重要的是这个通道只负责画所有复杂的地理计算仍然留在 Dart 层。这样即使原生渲染层出了 bug也不会影响标注点的正确性反过来如果我在 Dart 层把 polylabel 参数调坏了也不会把原生层拖崩溃。适配的核心不是把 polylabel 算法迁移到 ArkTS而是建立一个清晰的责任边界。4. 核心适配实现坐标投影、Isolate 调度与标注对齐4.1 先投影成平面坐标再算内切圆否则就是个隐形的 bugpolylabel 算法内部要算点到线段的最短距离如果直接把经纬度坐标喂进去在低纬度地区误差还不明显但到了北纬 40 度以上经度 1 度对应的实际距离只有纬度 1 度的 70% 左右算法算出来的最大内切圆是按等距空间来的结果会明显偏斜。所以我在 Dart 侧做了一步WGS84 经纬度 - Web Mercator 平面坐标的转换等 polylabel 算完再把结果坐标反投影回经纬度回传给鸿蒙原生层。这里贴一段我在实际项目里的封装代码import dart:math as math; class MercatorProjection { static const double earthRadius 6378137.0; static Listdouble project(double lng, double lat) { final x lng * math.pi / 180.0 * earthRadius; final y math.log(math.tan(math.pi / 4.0 (lat * math.pi / 180.0) / 2.0)) * earthRadius; return [x, y]; } static Listdouble unproject(double x, double y) { final lng x / earthRadius * 180.0 / math.pi; final lat (math.atan(math.exp(y / earthRadius)) - math.pi / 4.0) * 2.0 * 180.0 / math.pi; return [lng, lat]; } } Listdouble computePolylabelForLngLatPolygon(ListListdouble polygon) { final projected polygon.map((p) MercatorProjection.project(p[0], p[1])).toList(); // 注意polylabel 的输入格式可能是 [x,y] 列表也可能是 Point 列表需要按所用包来适配 final result polylabel([projected], precision: 1.0); return MercatorProjection.unproject(result.x, result.y); }第一次适配时我图省事直接把经纬度丢给 polylabel 跑结果在北方的围栏标注全往南偏了一大截。排查了半天最后发现不是算法有问题而是坐标系不对。这个坑印象太深了强烈建议所有做地图计算的 Flutter 开发者形成肌肉记忆在地图空间里做几何计算先统一投影坐标系。4.2 用 Isolate 分批计算避免 UI 卡成幻灯片polylabel 虽然比暴力穷举快得多但它是逐次细分逼近的迭代过程。一个 200 个顶点的复杂多边形精度设到 1 米平均要迭代几百次 cell虽然单次计算只要几毫秒但如果一次性来几千个多边形在 UI isolate 上直接跑照样会让地图交互卡顿掉帧。我在项目里用 Dart 的Isolate.run做批处理计算。做法是维护一个待计算队列每次取最多 200 个多边形坐标序列化后丢给一个后台 isolate 统一计算计算完成后把结果以 Map 形式传回主 isolateFutureListLabelResult computeLabelPointsInBatch( ListGeoPolygon polygons, ) async { final rawData polygons .map((p) { id: p.id, coords: p.coordinates .map((latLng) [ latLng.lng.toDouble(), latLng.lat.toDouble(), ]) .toList(), }) .toList(); return await Isolate.run(() { final results LabelResult[]; for (final item in rawData) { final projected _toMercator(item[coords] as ListListdouble); final point polylabel([projected], precision: 1.0); final lngLat _unproject(point.x, point.y); results.add(LabelResult(id: item[id], lng: lngLat[0], lat: lngLat[1])); } return results; }); }Isolate.run是 Dart 2.19 以后引入的 API比手动Isolate.spawn加ReceivePort简洁得多。数据量大时可以再用一个固定大小的 isolate 池来并发处理但对于大多数围栏数量在 1 万以内的场景单个后台 isolate 分批已经足够了。这里要特别提醒一个细节isolate 之间传递的数据是深拷贝的传入传出的是纯数据而不是对象引用。所以传参时尽量用扁平结构比如ListListdouble而不是自定义类实例一方面减少序列化开销另一方面避免因为对象不可共享而导致的编译或运行问题。我最初把GeoPolygon对象直接丢进Isolate.run编译没问题但每次传输都有隐性的序列化损耗换成纯坐标数组后耗时又降了一截。4.3 标注对齐不止算出点还要放对位置拿到 polylabel 的锚点后还远没到结束。实际地图上有大量标注同时存在光靠每个多边形独立算内切圆标注之间可能互相覆盖。一个多边形内切圆圆心没问题但两个相邻多边形的标注可能正好挤在一起。我的对齐策略分三层第一层点位微调。在 polylabel 结果的基础上尝试上、下、左、右四个方向做小幅度平移通常每次移动 5~10 像素对应的地理距离每移动一次检查是否与已放置标注的包围盒相交。如果找到一个方向上不再碰撞就采用偏移后的点。第二层候选池。如果四个方向都碰撞不急着放弃而是保留 polylabel 点作为基准结合多边形内切圆半径换几个候选缩放比例比如文字更大或更小重新做第一层的碰撞检测。文字对地图阅读很重要宁可缩小一点也要放在多边形内部。第三层栅格索引。碰撞检测如果每加一个标注就遍历所有已放置标注时间复杂度是 O(n²)几千个多边形就非常慢。我用一个简单的二维栅格索引把平面投影坐标系分成固定大小的格子比如 200 米一格每个标注只登记到它所在的格子碰撞检测时只需检查邻近 9 个格子里的标注包围盒。这样复杂度降到近线性。class GridIndexT { final double cellSize; final Mapint, ListT _cells {}; GridIndex(this.cellSize); int _key(double x, double y) { final gx (x / cellSize).floor(); final gy (y / cellSize).floor(); return gx * 100000 gy; // 简单 hash注意负数坐标需偏移处理 } void insert(double x, double y, T value) { final k _key(x, y); _cells.putIfAbsent(k, () []).add(value); } ListT queryNearby(double x, double y) { final gx (x / cellSize).floor(); final gy (y / cellSize).floor(); final result T[]; for (var dx -1; dx 1; dx) { for (var dy -1; dy 1; dy) { final items _cells[gx * 100000 gy dx * 100000 dy]; if (items ! null) result.addAll(items); } } return result; } }注意上面的 hash 只适合坐标在 [0, 99999] 范围内的场景如果你要处理大范围跨带数据建议用字符串 key 或者真正的空间索引库。这个栅格足够应对地图围栏场景重点是把碰撞查询从扫全表变成查邻近格子。4.4 缓存与局部更新地图缩放时不要重算一切地图上最典型的交互是缩放和平移。拖动还好坐标不变一旦缩放级别变化多边形在屏幕上的大小和像素位置全部改变标注间距也需要重新适配。如果每次缩放都重新跑一遍所有多边形的 polylabel性能再优化也扛不住高频手势。我的方案是分两段缓存几何缓存一个多边形的 polylabel 结果只取决于多边形坐标本身与缩放级别无关。所以我用一个MapString, Listdouble缓存 polygonId - 标注经纬度只有围栏数据从服务端刷新时才失效。布局缓存每个缩放级别下的最终屏幕坐标、是否碰撞、是否隐藏则按 zoom level 分段缓存。只有进入一个新的缩放级别且该级别的缓存为空时才从几何缓存里读出锚点再跑一遍避让计算。实测下来几何缓存往往能挡住 90% 以上的重复计算。围栏数据不变时用户缩放地图只是做 O(n) 的屏幕坐标映射和 O(n) 的避让检查而不是 O(n log n) 的算法迭代。5. 复杂多边形标注对齐的实战细节补充实际操作中对齐两个字里面的门道比想象中多。做地图标注久了你会知道人眼对悬挂在地图上的文字的位置非常敏感。有几个细节如果不注意标注位置正确了观感还是不对。多边形的方向顺时针/逆时针会影响部分实现。polylabel 的某些移植版本在计算包含关系时会依赖环的方向判断内外。我在适配时发现Dart 版实现如果输入是逆时针外环结果偶尔会偏到边缘。保险做法是所有多边形坐标统一转成顺时针外环 逆时针内洞再送入算法。这就像格式化代码一样先统一输入规范能避免大量玄学问题。带洞多边形要额外处理。一个区域内部有个湖、有个绿化带、有个不能标注的禁飞区polygon 会包含一个或多个内环。polylabel 支持带洞输入但必须把洞的坐标作为负区域参与距离计算否则算出的内切圆可能落在洞里。鸿蒙适配时很多同事第一次接触这个算法会忽略这个问题——服务端下发的数据里 comments 字段写着有个 hole代码里却没把洞传进去。结果标签正好飘在湖中央测试一看就崩了。检查方式很朴素debug 时把 polylabel 返回点的坐标在鸿蒙原生层画一个标记圆半径等于返回的 distance肉眼检查是否和边界和洞都相切。长文本标签的锚点偏移。一个区域名如果是XX市XX区XX街道XX产业园区这种长文本锚点按下对齐的策略和短文本不一样。长文本应该让锚点略微偏向左下方把文本主体留在多边形视觉中心内。这一条没法靠算法自动解决我的做法是在对齐层给长文本加一个anchorOffset参数假设文字宽度大于一定像素就把锚点向右上角平移文字宽度的一半同时做碰撞检测保证偏移后不越界。这个规则是我在真机上一遍遍看渲染结果调出来的比较偏门但对那些区域名必长的政企类项目非常关键。鸿蒙原生侧文字图层的叠加层级。如果地图底图是 Flutter 自绘的顶层文字用鸿蒙 Overlay那要注意 z-orderFlutter 的 PlatformView 和原生 Overlay 之间在鸿蒙上的层级规则与 Android 有些差异尤其是涉及多窗口比如卡片分屏时原生 Overlay 可能被 Flutter 侧重新绘制覆盖。我在 Index.ets 里把标注 Overlay 挂到了最顶层的窗口栈上才避免了切后台再回前台时标注消失的问题。6. 地理围栏大数据量下的性能优化实测6.1 性能瓶颈定位适配完成后我第一时间做了性能摸底。测试机是鸿蒙开发机麒麟芯片8G 内存数据集是某城市 2300 个围栏多边形顶点数从 3 到 800 不等总顶点量约 23 万。优化前直接在 UI isolate 里串行计算结果非常惨烈阶段耗时数据拉取与解析180ms2300 个多边形 polylabel 计算3900ms避让碰撞检测520ms鸿蒙层绘制 2300 个标注240ms总耗时接近 4.8 秒加载围栏列表时地图明显卡死转圈圈转了 4 秒多。用户显然不可能接受这个体验。6.2 三步优化Isolate、顶点抽稀、缓存第一步做的就是把 polylabel 计算从 UI isolate 挪出去按每批 200 个多边形提交到后台 isolate一批算完再送下一批。结果计算阶段耗时从 3900ms 降到 1100ms 左右。UI 线程几乎不再卡顿但总等待时间还是很长。第二步是顶点抽稀。围栏数据里大量顶点是沿道路边界采样的一条直路上可能有几十个间隔 5 米的重复采样点这些点对最大内切圆的计算几乎没贡献却让每次点到线段的距离计算膨胀了十几倍。我用 Douglas-Peucker 抽稀epsilon 设为 0.5 米Web Mercator 平面坐标下把 23 万顶点抽到 6 万左右。这一步对结果质量影响极小但计算时间直接降到了 320ms。第三步是几何缓存。因为围栏数据是启动时一次性拉取的用户在会话期间不会频繁刷新所以我只对首次进入某围栏列表时全量计算一次之后所有缩放平移都走 4.4 节的布局缓存。效果叠加后首次加载总耗时约 1.4 秒后续进入列表页几乎无感。优化阶段polylabel 计算耗时整链路耗时UI 卡顿优化前UI isolate 串行3900ms4800ms严重卡死优化1Isolate 分批1100ms1900ms基本无感优化2顶点抽稀320ms1050ms无感优化3几何缓存320ms仅首算320ms后续跳变50ms无感6.3 精度参数怎么调polylabel 最后一个参数是精度 precision单位是输入坐标的单位。在 Web Mercator 平面坐标系下1 个单位约等于 1 米。我默认设 1.0但这并不意味着所有场景都合适如果你的标注文字很大比如 18 号字以上precision 设到 5.0 也够用计算速度会快非常多。如果多边形很小面积不到几百平方米precision 设 0.1 会特别慢这时不如直接取第一个顶点或质心polylabel 对微型多边形没意义。如果你会在 3D 地图上倾斜视角下看到标注最好额外判断内切圆半径换算成屏幕像素后是否大于文字高度的 1.2 倍否则标注会显得贴近边缘观感不佳。一个我常用的经验公式precision max(1.0, 包围盒短边长度 / 200)。这样既保证大区域内的计算精度又不会在小多边形上磨洋工。这是我跑了十几组数据后总结出来的经验值不一定普适但作为初始值很靠谱。7. 踩坑记录与经验总结7.1 坑一经纬度当平面坐标算全程偏航前面提过投影问题但这里值得再啰嗦一遍。鸿蒙端的地图 SDK 大多是 Web Mercator 或 GCJ-02 坐标系国内常见如果你的围栏数据来自 WGS84 的服务端不统一坐标系就开算结果可能偏出几百米。这个坑不是 polylabel 带来的是所有地理空间计算共享的坑。我的规范做法是在数据进入 Flutter 分析管线时先统一转成 WGS84 经纬度然后在代码里显式声明这里做几何计算前必须转 Web Mercator 平面坐标同时在工具类里加断言如果多边形顶点纬度绝对值小于某个阈值比如 0.0001就扔异常。这样至少能在早期发现坐标漂移。7.2 坑二退化多边形与空区域polylabel 的假定是输入一个合法的非退化多边形。但真实数据里什么都有三点共线成一条线段的面积为零的多边形、只有 2 个顶点、或者四边形但四个点都在一条直线上。这些数据丢给 polylabel有的实现会返回一个 NaN 或者直接抛异常。我的 fallback 策略是计算前先检查多边形面积如果面积小于 1 平方米就直接取所有顶点的平均坐标作为标注点跳过 polylabel。另外在 catch 到任何异常时统一回退到多边形第一个顶点 偏移的兜底方案保证标注一定出来只是位置不完美。地图标注偶尔偏一点可以接受崩溃和空白绝对不能接受。7.3 坑三鸿蒙原生层叠加文字被地图图层遮盖鸿蒙端如果用的是 Flutter 自绘地图顶层文字 Overlay 和地图 Canvas 的绘制顺序不总是和 Android 一致。我的标注 Overlay 一开始用的是普通 Page 里的高优先级 Overlay Entry结果在全屏地图上被地图 Canvas 的 SurfaceView 盖住了一部分区域。排查后发现鸿蒙对平台 SurfaceView 和普通 RenderNode 的混排有自己的规则。解决方式是给标注 Overlay 设置独立的WindowStage或UIExtensionComponent层级确保它挂在整个地图 Surface 之上。这个方案在平板分屏、折叠屏展开等多窗口场景下也能稳定工作。7.4 一个额外的建议把算法版本固化最后说个工程上的经验。polylabel 的 Dart 实现存在多个版本有的带 precision 参数有的内部硬编码精度有的返回Point对象有的返回Listdouble有些版本对自相交多边形的处理还有差异。适配鸿蒙后我直接把这个 Dart 包 vendor 到项目内部锁定版本不再跟随 pub.dev 上游更新。原因很简单地理计算属于边界情况爆炸的领域上游一个小改动可能让已缓存的结果全部失效而自己维护的拷贝虽然要手动跟进修复但至少可预测、可回滚、可单测。这次适配完成的最终结果是Flutter 侧通过 polylabel 计算出的标注锚点配合后端透传的围栏数据在鸿蒙端地图上实现了对齐、避让、缓存三件套2300 个复杂多边形围栏从数据下来到屏幕上稳定标注整个链路 1 秒上下完成交互不再卡顿。如果你也在做 Flutter 地图到鸿蒙的适配或者正被多边形标注往外跑折磨我建议先别急着换地图库花两天时间把 polylabel 接入、投影对齐、缓存做好大概率能解决 80% 的痛点。剩下的 20% 是原生层渲染细节那就要结合你用到的具体鸿蒙地图引擎去逐层排查了。
返回列表