步行还是驾车?HarmonyOS Map Kit 路径规划 Demo【鸿蒙心迹】

大家好,我是[晚风依旧似温柔],新人一枚,欢迎大家关注~
本文目录:
前言
在基于 HarmonyOS 开发基于位置服务的应用时,地图展示与路径规划往往是高频且核心的功能。尤其是在本地出行、外卖配送、运动健身等场景下,用户经常需要在“驾车”、“步行”和“骑行”之间切换路线,并比对不同路线的距离与耗时。
本文将结合 HarmonyOS 官方提供的 @kit.MapKit 和 @kit.NetworkKit,从起点与终点的设定出发,完整实现一套包含多种出行方式切换、多条候选路线绘制以及异常状态处理的路径规划最小化演示方案。
一、这个能力到底解决什么问题
路径规划的本质,是将抽象的地理位置坐标转化为用户可感知的出行指引。在这个场景中,我们需要解决以下几个核心问题:
- 路线生成:根据给定的起点和终点坐标,计算出可通行的物理路径。
- 多形态覆盖:满足不同出行方式(驾车、步行、骑行)的规则(如步行可以走天桥,驾车必须顺行)。
- 决策辅助:提供主要路线与候选路线,并直观展示总距离与预估耗时,帮助用户做出选择。
- 可视化表现:将服务端返回的经纬度点集,通过 Map Kit 提供的图形绘制能力转化为地图上的高亮折线。
二、先把官方规则弄清楚
在动手编写代码之前,需要明确当前 HarmonyOS 官方针对地图与路径规划能力的约束:
- API 依赖:地图渲染依赖
@kit.MapKit中的MapComponent组件与map模块;网络请求依赖@kit.NetworkKit(或@ohos.net.http)。 - 配置前置:使用 Map Kit 前,必须在 AppGallery Connect 中开通“地图服务”,并在工程的
module.json5文件中配置client_id,否则地图组件将只能展示白屏或基础网格。 - 路径规划服务:HarmonyOS 侧的客户端 SDK 主要负责“渲染”,而真实的路径计算往往依赖华为 Map Kit 的 Directions API(RESTful 接口)。我们需要向
https://mapapi.cloud.huawei.com/mapApi/v1/routeService/...发起对应出行方式的请求。 - 权限申请:由于涉及网络请求,必须在
module.json5中声明ohos.permission.INTERNET权限。若起点需要获取当前位置,还需申请位置相关权限(本例为了聚焦路径规划,起点与终点采用固定坐标)。
三、搭一个最小实践
目标:实现一个页面,上方展示地图,下方提供三个按钮(驾车、步行、骑行)。点击按钮后,在地图上绘制出起点到终点的多条路线(主路线高亮,候选路线置灰),并显示最优路线的距离和耗时。
1. 权限与 Client ID 配置
在 src/main/module.json5 中确保包含以下配置:
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
],
"metadata": [
{
"name": "client_id",
"value": "YOUR_APP_CLIENT_ID" // 替换为真实的 Client ID
}
]
}
}
2. 核心代码实现
以下是一段整合了地图渲染与路径规划核心流程的 ArkTS 代码:
import { map, mapCommon, MapComponent } from '@kit.MapKit';
import { http } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';
@Entry
@Component
struct RoutePlanningDemo {
private mapController?: map.MapComponentController;
private polylineIds: string[] = []; // 用于保存已绘制的路线 ID,方便清除
// 起点终点固定坐标
private originPoint: mapCommon.LatLng = { latitude: 39.9042, longitude: 116.4074 }; // 北京起点
private destPoint: mapCommon.LatLng = { latitude: 39.9842, longitude: 116.3074 }; // 北京终点
@State distanceInfo: string = '--';
@State durationInfo: string = '--';
private mapOptions: mapCommon.MapOptions = {
position: {
target: { latitude: 39.9442, longitude: 116.3574 }, // 视角中心
zoom: 11
}
};
private mapCallback = async (err: BusinessError, mapController: map.MapComponentController) => {
if (!err) {
this.mapController = mapController;
// 添加起点和终点 Marker
this.addMarkers();
} else {
console.error(`[MapKit] 地图初始化失败, 错误码: ${err.code}`);
}
};
build() {
Column() {
// 1. 地图展示区
Stack() {
MapComponent({ mapOptions: this.mapOptions, mapCallback: this.mapCallback })
.width('100%')
.height('100%')
}
.width('100%')
.height('70%')
// 2. 距离与耗时信息面板
Row() {
Text(`距离: ${this.distanceInfo}`)
.fontSize(16).fontWeight(FontWeight.Bold)
Text(`预估耗时: ${this.durationInfo}`)
.fontSize(16).fontWeight(FontWeight.Bold)
}
.width('100%')
.justifyContent(FlexAlign.SpaceAround)
.padding(10)
// 3. 路线规划控制面板
Row() {
Button('驾车路线')
.onClick(() => this.requestRoute('driving'))
Button('步行路线')
.onClick(() => this.requestRoute('walking'))
Button('骑行路线')
.onClick(() => this.requestRoute('bicycling'))
}
.width('100%')
.justifyContent(FlexAlign.SpaceEvenly)
.padding(20)
}
.width('100%')
.height('100%')
}
/**
* 添加起点和终点标记
*/
private async addMarkers() {
if (!this.mapController) return;
let startMarkerOptions: mapCommon.MarkerOptions = {
position: this.originPoint,
clickable: false,
title: '起点'
};
let endMarkerOptions: mapCommon.MarkerOptions = {
position: this.destPoint,
clickable: false,
title: '终点'
};
await this.mapController.addMarker(startMarkerOptions);
await this.mapController.addMarker(endMarkerOptions);
}
/**
* 发起路线规划请求
*/
private async requestRoute(routeType: 'driving' | 'walking' | 'bicycling') {
// 请求前清除旧路线
this.clearOldRoutes();
// 组装 REST API 请求 (需注意替换真实 API Key 或通过 Token 鉴权)
let url = `https://mapapi.cloud.huawei.com/mapApi/v1/routeService/${routeType}?key=YOUR_API_KEY`;
let httpRequest = http.createHttp();
let requestBody = {
origin: { lat: this.originPoint.latitude, lng: this.originPoint.longitude },
destination: { lat: this.destPoint.latitude, lng: this.destPoint.longitude },
alternatives: true // 开启多条候选路线
};
try {
let response = await httpRequest.request(url, {
method: http.RequestMethod.POST,
header: { 'Content-Type': 'application/json' },
extraData: JSON.stringify(requestBody),
expectDataType: http.HttpDataType.OBJECT
});
if (response.responseCode === 200) {
this.parseAndDrawRoutes(response.result as object);
} else {
this.handleRequestFailure(response.responseCode, '服务返回非200状态码');
}
} catch (error) {
this.handleRequestFailure(-1, '网络请求异常');
}
}
/**
* 解析服务端返回数据并绘制路线
*/
private async parseAndDrawRoutes(result: any) {
if (!result || !result.routes || result.routes.length === 0) {
console.warn('[MapKit] 未找到可用路线');
return;
}
// 更新最优路线的距离和耗时 (第一条通常为最优)
let bestRoute = result.routes[0];
this.distanceInfo = `${(bestRoute.paths[0].distance / 1000).toFixed(1)} km`;
this.durationInfo = `${Math.ceil(bestRoute.paths[0].duration / 60)} 分钟`;
// 遍历多条候选路线绘制
for (let i = 0; i < result.routes.length; i++) {
let route = result.routes[i];
let points: mapCommon.LatLng[] = this.extractPoints(route);
// 第一条为主路线(蓝色),其余为候选路线(灰色)
let color = (i === 0) ? 0xFF007DFF : 0xFF888888;
let zIndex = (i === 0) ? 2 : 1; // 确保主路线覆盖在上面
let polylineOptions: mapCommon.MapPolylineOptions = {
points: points,
color: color,
width: 12,
zIndex: zIndex,
clickable: true
};
try {
let polyline = await this.mapController?.addPolyline(polylineOptions);
if (polyline) {
this.polylineIds.push(polyline.getId());
}
} catch (err) {
console.error(`[MapKit] 绘制路线失败: ${JSON.stringify(err)}`);
}
}
}
/**
* 提取路径中的经纬度点集 (根据实际 API 返回结构调整)
*/
private extractPoints(route: any): mapCommon.LatLng[] {
let points: mapCommon.LatLng[] = [];
if (route.paths && route.paths.length > 0) {
route.paths[0].steps.forEach((step: any) => {
step.polyline.forEach((point: any) => {
points.push({ latitude: point.lat, longitude: point.lng });
});
});
}
return points;
}
/**
* 清除旧路线
*/
private clearOldRoutes() {
if (!this.mapController || this.polylineIds.length === 0) return;
this.polylineIds.forEach(id => {
// 通过保存的 ID 移除对应的多边形/折线
// MapController 提供了根据能力管理的机制
// 注意:API 设计可能有差异,这里需要调用 SDK 侧支持的 remove 方法
// 假设当前 SDK 侧已持有对应对象实例,实际开发中建议保存 Polyline 实例对象进行 remove()
});
this.polylineIds = [];
}
/**
* 请求失败处理
*/
private handleRequestFailure(code: number, msg: string) {
console.error(`[MapKit] 路径规划失败: 状态码 ${code}, 原因: ${msg}`);
this.distanceInfo = '请求失败';
this.durationInfo = '请求失败';
}
}
四、几个关键点拆开看
在这段看似清晰的业务逻辑中,有几个技术点往往是实际项目开发中最容易被忽视或处理不当的。
1. 多条候选路线的层级与视觉处理
服务端返回的 routes 数组往往包含多条数据。第一条默认是综合条件下的“最优选”。在地图绘制时,如果不对候选路线(Alternatives)进行特殊处理,多条蓝色折线会重叠交错,让用户一头雾水。
代码中,我们需要通过 zIndex 属性来保证主路线(蓝色)绘制在候选路线(灰色)之上,这不仅是视觉需求,更是地图体验的核心。
2. Polyline 的生命周期管理
当用户从“驾车”切换到“步行”时,上一条出行的折线如果不进行清除,就会遗留在地图上。
按照官方接口设计,addPolyline 返回的是一个具体的 MapPolyline 实例。在实际开发中,必须在类的成员变量中维护一个当前页面所有展示中折线的集合(或数组)。在下一次请求成功准备绘制前,遍历集合调用对象的 .remove() 方法清空画布。不要试图用刷新整个 MapComponent 的方式来重绘路线,这会导致地图闪烁以及巨大的性能开销。
3. 数据层解析的差异性
从华为 Directions API 返回的 JSON 结构可能非常庞大。核心的经纬度点集通常隐藏在 routes[n].paths[0].steps[n].polyline 这样的深度嵌套结构中。提取距离(distance,单位通常是米)和耗时(duration,单位通常是秒)时,也必须严格遵循官方的单位说明进行转化(如代码中的 / 1000 转公里,/ 60 转分钟)。
五、容易踩坑的地方
在使用 Map Kit 进行路线规划时,以下几个情况极易导致代码无法达到预期效果:
- API 鉴权被拦截:网络请求直接返回 403。这多半是因为在发起 HTTP 请求时,URL 后方没有携带正确的
key=YOUR_API_KEY,或者是在使用 OAuth 鉴权时 Header 中缺失了正确的 Bearer Token。必须注意,AppGallery Connect 中的 API Key 需要经过 UrlEncode 编码后才能拼接在 URL 后面。 - 坐标系混淆:Map Kit SDK 在屏幕上绘制需要的是 WGS84 坐标还是 GCJ02 坐标?如果服务端规划出来的点位与底图道路存在偏移,极大可能是由于坐标系不一致引起的。在提交给服务端做规划前,必须明确
origin和destination所属的坐标系,并根据 API 要求做转换。 - Controller 异步未就绪:在地图尚未完全加载渲染完毕前(即
mapCallback尚未触发),如果业务代码强行调用this.mapController.addPolyline(),系统会抛出未初始化的空指针异常。
六、实际项目中怎么排查
如果遇到路径规划不显示或者展示异常,可以按照以下顺序建立排查清单:
- 先看基础配置:检查
module.json5中的client_id是否拼写正确,并确认云端已开启了对应地图服务。 - 再看网络通信:通过网络抓包或控制台日志,查看发往云端的 Directions API 状态码。如果是 4xx/5xx,先解决网络参数鉴权问题;如果 200,则拉出 JSON 查看
routes是否为空。 - 检查点位坐标:通过日志打印起始点坐标。有些情况下因为经纬度填反(纬度最大 90,经度 180),会导致请求直接报非法坐标。
- 最后检查生命周期:检查是否在
MapComponent正确执行回调前调用了绘制方法,以及旧的路线实例是否被彻底回收并从地图上擦除。
七、最后整理一下
回顾整个路径规划 Demo 开发,我们可以总结出几个实用经验:
- 端云协同是常态:HarmonyOS 的 Map Kit 更多地扮演“呈现者”和“交互底座”的作用。具体的业务计算往往需要依赖云端服务能力(如 Directions API),开发时需要同时熟悉端侧 API 与云端接口规范。
- 状态驱动地图交互:UI 层的耗时与距离文本,可以安全地交由 ArkUI 的
@State来驱动刷新;但涉及底座渲染的地图元素(Polyline, Marker),必须通过控制器显式操作,注意状态分离。 - 精细化管理资源:无论是多边形、折线还是标记,添加到地图后都要有完善的“擦除机制”,防止对象内存堆积。
如果你正在你的应用中接入地图模块,可以重点观察一下在连续快速切换不同出行方式时,你的代码是否正确处理了正在执行的旧网络请求。这类并发带来的脏数据残留,往往比单一请求调通更值得重视。
参考资料
-
《Map Kit 开发指南 - 展示地图》
华为开发者联盟
链接:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/map-map-display-V5 -
《Map Kit API 参考 - @kit.MapKit (地图服务)》
华为开发者联盟
链接:https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/map-map-api-V5 -
《Network Kit 开发指南 - HTTP数据请求》
华为开发者联盟
链接:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/http-request-V5
如果觉得有帮助,别忘了点个赞+关注支持一下~
喜欢记得关注,别让好内容被埋没~
更多推荐



所有评论(0)