鸿蒙运动健康实战:自定义定位箭头跟随手机方向旋转

告别系统蓝点,实现高精度自定义定位箭头,实时响应手机朝向,为运动轨迹应用增添使用交互体验。

完整源码:SportTrackDemo

在上一节中,我们已经实现了运动轨迹记录、后台长时任务申请等功能。但系统默认的"我的位置"蓝点带有光圈。虽然可以自定义样式,但是无法实现箭头跟随手机方向旋转。

本文将手把手教你如何关闭系统定位图层,用自定义 Marker 替换,并通过传感器获取设备方向,让箭头实时指向手机朝向。

一、最终效果

二、实现思路

  1. 关闭系统定位图层setMyLocationEnabled(false),避免显示默认蓝点。
  2. 自定义定位 Marker:创建带箭头的 Marker,锚点设为中心,便于旋转。
  3. 监听定位变化:实时更新 Marker 的位置。
  4. 监听方向传感器:获取设备方位角(0~360°,0=北,顺时针)。
  5. 实时旋转 :调用 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°的问题,原因有二:

  1. 传感器 alpha 定义:0° = 北,顺时针增加。但某些设备的传感器可能返回相反方向(例如北为180°)。
  2. 图标自身方向:如果图标设计为指向右侧(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 并接入方向传感器,我们实现了高精度、可自定义的定位箭头旋转效果。整个过程无需申请额外权限,代码清晰,性能优异。

相关推荐
Swift社区3 小时前
鸿蒙游戏的数据流是怎么跑的?
游戏·华为·harmonyos
前端不太难3 小时前
State 驱动鸿蒙游戏架构详解
游戏·架构·harmonyos
见山是山-见水是水10 小时前
鸿蒙flutter第三方库适配 - 读书笔记
flutter·华为·harmonyos
Utopia^11 小时前
鸿蒙flutter第三方库适配 - 图片压缩工具
flutter·华为·harmonyos
SoraLuna12 小时前
「鸿蒙智能体实战记录 11」年俗文化展示卡片开发与多段内容结构化呈现实现
华为·harmonyos
Ww.xh12 小时前
OpenHarmony API8升API9:权限与接口变更实战指南
harmonyos
梁山好汉(Ls_man)14 小时前
鸿蒙_自定义组件包含多个引用自定义构建函数@BuilderParam时的用法
华为·harmonyos·鸿蒙·arkui
见山是山-见水是水14 小时前
鸿蒙flutter第三方库适配 - 车辆管理
flutter·华为·harmonyos
Utopia^15 小时前
鸿蒙flutter第三方库适配 - 番茄钟专注
flutter·华为·harmonyos