华为鸿蒙开发高级篇08-地图与位置服务实战:骑行轨迹记录 App
高级系列第 8 篇 · 案例:骑行轨迹记录 App——地图显示、实时定位、轨迹绘制(polyline)、暂停/继续与后台续传。 目标:跑通鸿蒙地图 + 定位两条链路:地图组件加载与标记/轨迹绘制;位置服务申请、持续定位、权限与生命周期处理,并沉淀"轨迹记录器"核心代码。

一、案例背景
骑行、跑步、物流轨迹类应用的核心是:地图展示 + 高频定位采样 + 轨迹画线。难点不在单个 API,而在三者的协作:定位频率与功耗的平衡、轨迹数据的内存管理、页面退出/后台时定位是否继续。
二、前置准备
2.1 权限
{
"requestPermissions": [
{ "name": "ohos.permission.LOCATION" }, // 精确位置
{ "name": "ohos.permission.APPROXIMATELY_LOCATION" }, // 近似位置
{ "name": "ohos.permission.LOCATION_IN_BACKGROUND" } // 后台定位(如需后台续传)
]
}
运行时申请(与第 7 篇权限流程一致,用 abilityAccessCtrl),注意:先申请近似定位,再申请精确定位,用户拒绝时降级到近似定位。
2.2 地图
- 鸿蒙地图能力通常通过地图 SDK(如华为 Map Kit / 第三方)接入,需在 AGC 开通服务并配置 API Key。
- 工程内需配置证书指纹与 Key(具体以所接地图 SDK 文档为准);地图组件一般以
MapComponent(或自定义组件包装)形式加载。

三、实际开发代码:轨迹记录 App
3.1 轨迹数据模型
// model/TrackModels.ets
export interface LatLng {
lat: number
lng: number
}
export interface TrackPoint extends LatLng {
timestamp: number // 采样时间(毫秒)
altitude: number // 海拔(米)
}
export class TrackSession {
points: TrackPoint[] = []
paused: boolean = false
startTime: number = 0
pauseAccum: number = 0 // 累计暂停时长(毫秒)
get duration(): number {
if (this.startTime === 0) return 0
return Date.now() - this.startTime - this.pauseAccum
}
get distanceMeters(): number {
// 逐段累加两两间距离(Haversine 近似即可)
let sum = 0
for (let i = 1; i < this.points.length; i++) {
sum += haversine(this.points[i - 1], this.points[i])
}
return sum
}
}
// 两点球面距离(米)
export function haversine(a: LatLng, b: LatLng): number {
const R = 6371000
const dLat = toRad(b.lat - a.lat)
const dLng = toRad(b.lng - a.lng)
const s = Math.sin(dLat / 2) ** 2
+ Math.cos(toRad(a.lat)) * Math.cos(toRad(b.lat)) * Math.sin(dLng / 2) ** 2
return 2 * R * Math.asin(Math.sqrt(s))
}
function toRad(deg: number): number {
return (deg * Math.PI) / 180
}
3.2 定位服务封装
// service/LocationService.ets
import { geoLocationManager } from '@kit.LocationKit'
import { BusinessError } from '@kit.BasicServicesKit'
import { LatLng, TrackPoint } from '../model/TrackModels'
export class LocationService {
private locationChange: geoLocationManager.LocationCallback | undefined
private lastPoint: TrackPoint | undefined
onPoint: (p: TrackPoint) => void = () => {}
// 开始持续定位:按场景选择定位优先级
start() {
// 场景:骑行高频定位(按实际需求选 1~5 秒间隔)
const request: geoLocationManager.LocationRequest = {
priority: geoLocationManager.LocationRequestPriority.PRECISION, // 精确优先
scenario: geoLocationManager.LocationRequestScenario.SPORT, // 运动场景
maxAccuracy: 10,
timeInterval: 2, // 采样间隔(秒)
distanceInterval: 5 // 或按距离采样(米)
}
this.locationChange = {
onLocationReport: (location: geoLocationManager.Location) => {
const point: TrackPoint = {
lat: location.latitude,
lng: location.longitude,
timestamp: Date.now(),
altitude: location.altitude ?? 0
}
this.lastPoint = point
this.onPoint(point)
},
onErrorReceive: (err: BusinessError) => {
console.error(`定位错误 code=${err.code} msg=${err.message}`)
}
}
geoLocationManager.on('locationChange', request, this.locationChange)
}
stop() {
if (this.locationChange) {
geoLocationManager.off('locationChange', this.locationChange)
this.locationChange = undefined
}
}
}
要点:
timeInterval+distanceInterval双条件采样,兼顾平滑与省电。- 场景
SPORT会适配运动轨迹的定位参数(以 SDK 支持为准,不支持则用通用场景)。 - 停止时
off移除回调,避免泄漏。
3.3 轨迹绘制:地图 polyline
地图组件的具体 API 随 SDK 而异,这里给出通用思路(以类 MapKit 接口示意,按所接 SDK 调整):
// components/TrackMap.ets
import { MapComponent, MapLatLng, MapPolylineOptions } from '地图SDK声明' // 按实际 SDK 引入
@Component
export struct TrackMap {
// 轨迹点(父组件传入,实时更新)
@Prop points: TrackPoint[] = []
build() {
// 地图组件:以真实 SDK 的组件/控制器为准
MapComponent()
.onMapReady((map: MapController) => {
// 1) 绘制轨迹线
if (this.points.length > 1) {
const polyline: MapPolylineOptions = {
points: this.points.map((p) => ({ lat: p.lat, lng: p.lng }) as MapLatLng),
color: '#007DFF',
width: 6
}
map.addPolyline(polyline)
}
// 2) 视野跟随最后一点(骑行场景)
const last = this.points[this.points.length - 1]
if (last) {
map.moveCamera({ target: { lat: last.lat, lng: last.lng }, zoom: 17 })
}
})
}
}
说明:地图 SDK 的组件名、控制器方法与 polyline 选项各不相同,以上为结构示意;集成时以所选 SDK 的类型声明为准,并将
MapComponent/MapController替换为实际 API。
3.4 页面:记录器(开始/暂停/继续/结束)
// pages/RidePage.ets
import { LocationService } from '../service/LocationService'
import { TrackSession, TrackPoint } from '../model/TrackModels'
import { TrackMap } from '../components/TrackMap'
@Entry
@Component
struct RidePage {
private locSvc: LocationService = new LocationService()
private session: TrackSession = new TrackSession()
@State points: TrackPoint[] = []
@State running: boolean = false
@State paused: boolean = false
@State distanceText: string = '0.00 km'
@State durationText: string = '00:00'
private timer: number = -1
aboutToAppear() {
this.locSvc.onPoint = (p: TrackPoint) => {
if (this.paused) return // 暂停时不采样
this.session.points.push(p)
this.points = [...this.session.points] // 触发地图重绘(新数组引用)
}
}
private start() {
this.session = new TrackSession()
this.session.startTime = Date.now()
this.locSvc.start()
this.running = true
this.paused = false
// 定时刷新统计栏
this.timer = setInterval(() => {
this.distanceText = (this.session.distanceMeters / 1000).toFixed(2) + ' km'
this.durationText = this.fmt(this.session.duration)
}, 1000) as unknown as number
}
private togglePause() {
this.paused = !this.paused
if (this.paused) {
this.session.pauseAccum = Date.now() - this.session.startTime - this.session.pauseAccum
} else {
// 恢复:重置暂停起点(简化处理:用恢复时间校正)
this.session.pauseAccum = 0
}
}
private finish() {
this.locSvc.stop()
clearInterval(this.timer)
this.running = false
this.paused = false
// 保存轨迹(可接第 6 篇 RDB 持久化)
console.info(`本次骑行 ${this.distanceText},耗时 ${this.durationText}`)
}
private fmt(ms: number): string {
const s = Math.floor(ms / 1000)
return `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`
}
aboutToDisappear() {
this.locSvc.stop()
clearInterval(this.timer)
}
build() {
Column() {
// 地图(轨迹实时绘制)
TrackMap({ points: this.points })
.width('100%')
.layoutWeight(1)
// 统计栏
Row({ space: 24 }) {
Column() {
Text(this.distanceText).fontSize(20).fontWeight(FontWeight.Bold)
Text('距离').fontSize(12).fontColor('#86909C')
}
Column() {
Text(this.durationText).fontSize(20).fontWeight(FontWeight.Bold)
Text('时长').fontSize(12).fontColor('#86909C')
}
}
.justifyContent(FlexAlign.Center)
.padding(16)
// 控制按钮
Row({ space: 24 }) {
if (!this.running) {
Button('开始骑行').onClick(() => this.start())
} else if (!this.paused) {
Button('暂停').onClick(() => this.togglePause())
Button('结束').onClick(() => this.finish())
} else {
Button('继续').onClick(() => this.togglePause())
Button('结束').onClick(() => this.finish())
}
}
.padding(16)
}
.width('100%').height('100%')
}
}
制按钮)。
### 3.5 后台续传(进阶)
骑行时可能切后台,需要持续定位:
1. 申请后台定位权限(`LOCATION_IN_BACKGROUND`),并配置后台任务/长时任务(`@kit.BackgroundTasksKit` 的 continuous task,类型 `LOCATION`)。
2. 定位回调收到点后写入内存/RDB;回前台时地图一次性补齐未绘制的点。
3. 释放时机:后台任务结束回调里 `stop()`。
```typescript
import { continuousTask } from '@kit.BackgroundTasksKit'
// 申请持续任务(定位类型)
async function startBgTask(context: common.Context) {
await continuousTask.startContinuousTask(context, {
wantAgent: { /* 通知栏条目,见官方文档 */ },
backgroundModes: [continuousTask.BackgroundMode.LOCATION]
})
}
说明:后台任务配置涉及通知、弹窗与权限的多项约束,以官方文档为准,本文只给入口思路。
四、优化与踩坑
| 问题 | 处理 |
|---|---|
| 轨迹点太密 | 用 distanceInterval 采样;绘制前按阈值抽稀(道格拉斯-普克) |
| 地图重绘全量刷新卡顿 | 只 addPolyline 增量段,或限制轨迹点数上限 |
| 后台定位被系统回收 | 使用持续任务 + 申请后台定位权限 |
| 定位精度漂移 | 开启 maxAccuracy 约束;必要时做滤波(卡尔曼/均值平滑) |
| 权限被拒后无提示 | 引导到设置页;先申请近似定位降级可用 |
| 模拟器无定位/地图 | 真机验证;模拟器可用模拟定位源 |
五、小结与延伸
- 轨迹 App 三件套:定位服务(采样)+ 地图组件(polyline 绘制)+ 会话管理(开始/暂停/结束 + 距离时长统计)。
- 案例沉淀:
LocationService(可复用的定位封装)、TrackSession(轨迹会话与距离计算)、RidePage(记录器页面),可直接扩展为跑步/物流轨迹应用。 - 延伸:轨迹保存与回放(接第 6 篇 RDB)、多段轨迹与骑行统计(爬升/均速)、离线地图、与第 8 篇分布式结合做家人位置共享。
下一篇预告:安全加固与发布运维——密钥管理、混淆加固与崩溃监控(登录态安全存储案例)。

587

被折叠的 条评论
为什么被折叠?



