告别系统蓝点,实现高精度自定义定位箭头,实时响应手机朝向,为运动轨迹应用增添使用交互体验。
完整源码:SportTrackDemo
在上一节中,我们已经实现了运动轨迹记录、后台长时任务申请等功能。但系统默认的"我的位置"蓝点带有光圈。虽然可以自定义样式,但是无法实现箭头跟随手机方向旋转。
本文将手把手教你如何关闭系统定位图层,用自定义 Marker 替换,并通过传感器获取设备方向,让箭头实时指向手机朝向。
一、最终效果

二、实现思路
- 关闭系统定位图层 :
setMyLocationEnabled(false),避免显示默认蓝点。 - 自定义定位 Marker:创建带箭头的 Marker,锚点设为中心,便于旋转。
- 监听定位变化:实时更新 Marker 的位置。
- 监听方向传感器:获取设备方位角(0~360°,0=北,顺时针)。
- 实时旋转 :调用
marker.setRotation(angle)。
三、核心代码实现
3.1 在 MapManager 中添加自定义定位箭头管理
javascript
// MapManager.ets
private myLocationMarker?: map.Marker;
/**
* 更新自定义定位箭头的位置和旋转角度
* @param lat 纬度(GCJ02)
* @param lng 经度(GCJ02)
* @param rotation 旋转角度(0~360°,0=正北)
*/
async updateMyLocationMarker(lat: number, lng: number, rotation?: number): Promise<void> {
if (!this.myLocationMarker) {
const options: mapCommon.MarkerOptions = {
position: { latitude: lat, longitude: lng },
icon: $r('app.media.ic_location_arrow'), // 箭头图标
anchorU: 0.5,
anchorV: 0.5,
flat: false, // 面对相机,旋转效果明显
clickable: false
};
this.myLocationMarker = await this.mapController?.addMarker(options);
} else {
this.myLocationMarker.setPosition({ latitude: lat, longitude: lng });
}
if (rotation !== undefined) {
this.myLocationMarker.setRotation(rotation);
}
}
setMyLocationRotation(rotation: number): void {
this.myLocationMarker?.setRotation(rotation);
}
removeMyLocationMarker(): void {
this.myLocationMarker?.remove();
this.myLocationMarker = undefined;
}
关键点:
anchorU/V: 0.5确保旋转中心在图标中心。flat: false使图标始终面向相机,旋转效果清晰。- 如果你使用的是"圆点+箭头"组合图标(例如一个圆点旁边带箭头),且将锚点设为 (0.5, 0.5) 中心点,那么圆点必须位于图标的几何中心,箭头从中心向外延伸。否则旋转时圆点会偏离实际定位位置,看起来"跑偏"。
- 另外一种就是修改锚点 但是这是邪修法不合适。我不会设计图随便找了一个icon,所以现在也是'跑偏的'。
3.2 修改 init 方法关闭系统定位
javascript
init(controller: map.MapComponentController): void {
this.mapController = controller;
controller.setMapType(mapCommon.MapType.STANDARD);
// 关闭系统定位图层
controller.setMyLocationEnabled(false);
controller.setMyLocationControlsEnabled(false);
}
3.3 传感器管理类 SensorManager(单例)
javascript
import { sensor } from "@kit.SensorServiceKit";
/**
* 方位角回调函数类型
* @param azimuth 设备方向角,单位:度,范围 0~360,0°=正北,顺时针增加
*/
export type AngleCallback = (azimuth: number) => void;
// 传感器管理类
export class SensorManager {
private static instance: SensorManager;
private callback?: AngleCallback; // 用户注册的回调
private isListening: boolean = false; // 是否正在监听
static getInstance(): SensorManager {
if (!SensorManager.instance) {
SensorManager.instance = new SensorManager();
}
return SensorManager.instance;
}
// 开始监听设备方向(方位角)
start(callback: AngleCallback, interval: number = 100_000_000): void {
if (this.isListening) {
console.warn('传感器已在监听中,请先 stop()');
return;
}
// 检查设备是否支持方向传感器
try {
// 推荐直接获取指定类型的传感器列表
const sensors = sensor.getSensorListSync();
if (sensors.length === 0) {
console.error('设备不支持方向传感器,无法获取方位角');
return;
}
} catch (error) {
console.error(`获取方向传感器列表失败: ${JSON.stringify(error)}`);
return;
}
this.callback = callback;
// 订阅方向传感器
try {
sensor.on(
sensor.SensorId.ORIENTATION,
(data: sensor.OrientationResponse) => {
const azimuth = data.alpha; // 0~360°
this.callback?.(azimuth);
},
{ interval: interval }
);
} catch (error) {
console.error(`订阅方向传感器失败: ${JSON.stringify(error)}`);
return;
}
this.isListening = true;
console.info(`开始监听设备方向,间隔 ${interval} ns`);
}
stop(): void {
if (!this.isListening) return;
try {
sensor.off(sensor.SensorId.ORIENTATION);
} catch (error) {
console.error(`停止传感器监听失败: ${JSON.stringify(error)}`);
} finally {
this.isListening = false;
this.callback = undefined;
console.info('停止传感器监听');
}
}
}
四、地图箭头指向与现实手机指向相反
箭头指向与手机实际朝向相反或偏差90°/180°的问题,原因有二:
- 传感器
alpha定义:0° = 北,顺时针增加。但某些设备的传感器可能返回相反方向(例如北为180°)。 - 图标自身方向:如果图标设计为指向右侧(90°),则需减去90°。
通用修正公式:
javascript
let corrected = azimuth;
// 情况1:完全相反 → 加180°
corrected = (azimuth + 180) % 360;
// 情况2:镜像翻转 → 取反
corrected = (360 - azimuth) % 360;
// 情况3:偏某个固定角度
corrected = (azimuth + offset + 360) % 360;
我们的是第一种情况完全相反
调试建议:
- 用系统指南针 App 对比,记录手机朝北时打印的
azimuth值。 - 若朝北打印 0,无需修正;若打印 180,则加180°。
- 若朝北打印其他值,使用
(360 - azimuth) % 360。
我在实际测试中发现设备返回的 alpha 正好与地图旋转角相反,因此采用 (azimuth + 180) % 360 解决了问题。
| 未修正方向 | 修正后方向 |
|---|---|
![]() |
![]() |
地图页监听传感器的方位角并且记录下来,页面初始化开始监听,页面结束释放,定图更新除传递坐标,增加方位角。以下代码只专注更新自定义箭头位置,完整代码仓库下载。
javascript
// Index.ets
import { SensorManager } from '../common/managers/SensorManager';
import { MapManager } from '../common/managers/MapManager';
import { geoLocationManager } from '@kit.LocationKit';
@Entry
@Component
struct Index {
private mapManager: MapManager = new MapManager();
private currentAzimuth: number = 0;
aboutToAppear() {
// 启动方向传感器
SensorManager.getInstance().start((azimuth) => {
// 反转方向:加上 180 度再模 360 不然方向正好相反
const corrected = (azimuth + 180) % 360;
this.currentAzimuth = corrected;
this.mapManager.setMyLocationRotation(corrected);
});
}
// ==================== 定位与轨迹处理方法 ====================
private onLocationUpdate(rawLoc: geoLocationManager.Location) {
if (!rawLoc.latitude || !rawLoc.longitude) return;
// 立即更新地图上的蓝点
// this.mapController?.setMyLocation(rawLoc);
// 坐标转换
const gcj = MapManager.convertWgs84ToGcj02(rawLoc.latitude, rawLoc.longitude);
// 更新自定义圆点
this.mapManager.updateMyLocationMarker(gcj.latitude, gcj.longitude, this.currentAzimuth);
}
private async moveToMyLocationIfNeeded() {
if (this.hasMovedToMyLocation) return;
if (!this.hasLocationPermission) {
console.warn('无定位权限,跳过移动相机');
return;
}
try {
if (!geoLocationManager.isLocationEnabled()) {
this.showToast('请开启设备位置服务');
return;
}
// 优先使用缓存位置
const lastLocation = geoLocationManager.getLastLocation();
if (lastLocation && lastLocation.latitude && lastLocation.longitude) {
const gcj = MapManager.convertWgs84ToGcj02(lastLocation.latitude, lastLocation.longitude);
// 更新自定义定位箭头(位置 + 当前方向)
this.mapManager.updateMyLocationMarker(gcj.latitude, gcj.longitude, this.currentAzimuth);
this.moveCameraToPoint(gcj.latitude, gcj.longitude, SportConstants.MAP_ZOOM_LEVEL);
this.hasMovedToMyLocation = true;
console.info('使用缓存位置移动相机');
}
const request: geoLocationManager.SingleLocationRequest = {
locatingPriority: geoLocationManager.LocatingPriority.PRIORITY_LOCATING_SPEED,
locatingTimeoutMs: SportConstants.SINGLE_LOCATION_TIMEOUT_MS
};
const location = await geoLocationManager.getCurrentLocation(request);
this.mapController?.setMyLocation(location);
const gcj = MapManager.convertWgs84ToGcj02(location.latitude, location.longitude);
this.moveCameraToPoint(gcj.latitude, gcj.longitude, SportConstants.MAP_ZOOM_LEVEL);
this.hasMovedToMyLocation = true;
console.info('已移动到用户当前位置');
} catch (error) {
console.error('定位失败', error);
this.showToast('无法获取当前位置,请点击开始运动后自动跟随', SportConstants.TOAST_DURATION_LONG);
}
}
aboutToDisappear() {
// 停止传感器,释放资源
SensorManager.getInstance().stop();
this.mapManager.removeMyLocationMarker();
this.stopTracking();
}
}
五、完整流程图
启动应用
↓
关闭系统定位图层
↓
创建自定义Marker(箭头图标)
↓
启动定位监听 → 更新Marker位置
↓
启动方向传感器 → 实时旋转Marker
↓
用户转动手机 → 箭头同步旋转
六、性能与功耗优化
- 传感器间隔 :
interval默认为 100ms(100_000_000 ns),既保证流畅又不过度耗电。可调整为'game'(约20ms)提升顺滑度。 - 避免重复创建 Marker:只在首次定位时创建,后续复用。
- 页面销毁时停止传感器 :在
aboutToDisappear中调用SensorManager.stop()。
七、总结
通过关闭系统定位图层、自定义 Marker 并接入方向传感器,我们实现了高精度、可自定义的定位箭头旋转效果。整个过程无需申请额外权限,代码清晰,性能优异。

