首页 > 建站教程 > APP开发,混合APP >  HarmonyOS 6实战:鸿蒙三方SDK对接-手把手教你接入高德定位SDK正文

HarmonyOS 6实战:鸿蒙三方SDK对接-手把手教你接入高德定位SDK

前言

大家好,我是木斯佳,还记得之前做那个地图出行应用时,我面临过一个选择:是用系统自带的定位能力,还是接入高德的定位SDK?

很多小伙伴会想,这事儿很简单——鸿蒙系统本身就有位置服务API,几行代码就能拿到经纬度,何必再多接一个SDK?

鸿蒙三方SDK对接-手把手教你对接调试高德定位SDK


但现实需求往往不是“拿到经纬度”那么简单。比如需要逆地理编码判断用户是否在某个区域内,需要处理不同坐标系之间的转换(GPS的WGS-84、高德的GCJ-02、百度的BD-09)。如果后续还要深度去做地图和导航,那工程量就太大了。特别是跨端开发者,同一套选型方案往往可以避免很多不必要的麻烦。

鸿蒙星河版定位SDK是高德开放的一个鸿蒙侧专业的定位SDK,开发者可以使用ArkTS接口开发应用集成定位功能,组件使用ArkUI原生组件,兼容方舟UI框架。代码全面适配鸿蒙NEXTSDK,所有系统接口均使用鸿蒙NEXTAPI。

但话说回来,三方SDK对接,坑永远比想象的多。

权限怎么配?隐私合规怎么处理?单次定位和连续定位的参数有什么区别?为什么有时候定位失败有时候又能成功?这些问题在我第一次接入时都踩过一遍。

所以就有了这篇文章。我想把自己踩过的坑、总结的经验整理出来,从一个真实的地图应用场景出发,带你一步步完成高德定位SDK的接入,实现连续定位这个最基础也最常用的功能。


一、准备工作

1.1 项目添加依赖

{
  "dependencies": {
    "@amap/amap_lbs_common": ">=1.2.2",      // 高德定位通用库
    "@amap/amap_lbs_location": ">=1.2.2",   // 高德定位SDK
    "@amap/amap_lbs_map3d": ">=2.2.3"       // 高德地图3D SDK
  }
}


在oh-package.json5里加上这三行,然后点一下Sync Now,等着就行。


1.2 申请API Key(劝退的地方)

去高德开放平台注册应用,选择“鸿蒙”平台,包名填你的应用包名。

鸿蒙三方SDK对接-手把手教你对接调试高德定位SDK


这里有一个小坑点,你申请APPID要获取如何获取AppID

官方给了示例代码,我一开始没搞明白,我建议你按这个步骤来,

1、华为官网配置应用签名

2、工程中正确的引用签名文件,加上下方的示例代码(打印到页面上)。

3、然后打出带签名的hap包,把签名出来的包,拖模拟器运行

4、页面上复制出来appid。好,结束(第一次我差不多弄了半下午)。

鸿蒙三方SDK对接-手把手教你对接调试高德定位SDK


请在当前应用的Ability中使用如下代码获取,我建议你自己加上在页面打印出来。

let flag = bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_SIGNATURE_INFO;
let bundleInfo = bundleManager.getBundleInfoForSelfSync(flag)
let appId = bundleInfo.signatureInfo.appId;


注意事项 1. 获取AppID之前需要先配置应用的签名信息,应用的签名信息配置请参考华为官网的配置应用签名信息指南

2.为了确保鉴权通过,请确保真机调试时使用的key是基于真机获取的appid申请的,而云真机调试时则应使用云真机appid对应的key,正确的appId形式为:“包名_签名信息”, 例如:com.amap.demo_BGtGgVB3ASqU7ar1nHkwX4s0nIexDbEwqNrVoatUDs17GrClWC7V2/zhoYh6tFQHAd5DASWVTEAgvZfzrEGljjs=

拿到Key之后存好,后面每个页面都要用。(你能走到申请完key,我觉得这个入门篇你已经完成一半了)


1.3 权限配置

定位功能绕不开权限。鸿蒙的权限管理比较严格,需要在module.json5里把会用到的权限都声明一遍。

鸿蒙三方SDK对接-手把手教你对接调试高德定位SDK


module.json5 权限声明

"requestPermissions": [
  {
    "name": "ohos.permission.APPROXIMATELY_LOCATION",  // 模糊定位权限
    "reason": "$string:Harmony_location_permission_reason",
    "usedScene": {
      "abilities": ["Harmony_location_demoAbility"],
      "when": "always"
    }
  },
  {
    "name": "ohos.permission.LOCATION",  // 精确定位权限
    "reason": "$string:Harmony_location_permission_reason",
    "usedScene": {
      "abilities": ["Harmony_location_demoAbility"],
      "when": "always"
    }
  },
  {
    "name": "ohos.permission.LOCATION_IN_BACKGROUND",  // 后台定位权限
    "reason": "$string:Harmony_location_permission_reason",
    "usedScene": {
      "abilities": ["Harmony_location_demoAbility"],
      "when": "always"
    }
  },
  {
    "name": "ohos.permission.INTERNET",  // 网络权限
    "reason": "$string:Harmony_location_permission_reason",
    "usedScene": {
      "abilities": ["Harmony_location_demoAbility"],
      "when": "always"
    }
  },
  {
    "name": "ohos.permission.KEEP_BACKGROUND_RUNNING",  // 后台运行权限
    "reason": "$string:Harmony_location_permission_reason",
    "usedScene": {
      "abilities": ["Harmony_location_demoAbility"],
      "when": "always"
    }
  }
]


如果你需要应用退到后台还能继续定位,还得在abilities里加一行:

"abilities": [
  {
    "name": "Harmony_location_demoAbility",
    "backgroundModes": ["location"],  // 支持后台定位
    // ... 其他配置
  }
]


这一步容易漏,后台定位跑不起来的时候,可以先检查这里。


二、参考代码上手搭建一个简易Demo

我这里因为有我自己的业务代码,我就给大家分享我之前上手用的社区的教程代码二改一下。

2.1 常量定义(CommonConstants.ets)

import { Permissions } from '@ohos.abilityAccessCtrl';
export class CommonConstants {
  // 需要请求的权限列表
  static readonly REQUEST_PERMISSIONS: Array<Permissions> = [
    'ohos.permission.APPROXIMATELY_LOCATION',  // 模糊定位
    'ohos.permission.LOCATION',                // 精确定位
  ];
  // 初始化值
  static readonly INITIALIZATION_VALUE: string = '0';
}


把权限列表抽成常量,方便多个页面复用。后面请求权限的时候直接拿这个数组就行。


2.2 全局上下文管理(GlobalThis.ets)

import { common } from '@kit.AbilityKit';
// 单例模式管理全局上下文
export class GlobalThis {
  private constructor() {}
  
  private static instance: GlobalThis;
  private _uiContexts = new Map<string, common.UIAbilityContext>();
  // 获取单例实例
  public static getInstance(): GlobalThis {
    if (!GlobalThis.instance) {
      GlobalThis.instance = new GlobalThis();
    }
    return GlobalThis.instance;
  }
  // 获取上下文
  getContext(key: string): common.UIAbilityContext | undefined {
    return this._uiContexts.get(key);
  }
  // 设置上下文
  setContext(key: string, value: common.UIAbilityContext): void {
    this._uiContexts.set(key, value);
  }
}


鸿蒙很多API需要传入Context。这个单例类就是用来在Ability创建时保存上下文,然后在任意页面里都能拿到。后面你会看到它频繁出现。


2.3 应用入口(Harmony_location_demoAbility.ets)

import UIAbility from '@ohos.app.ability.UIAbility';
import { AppInfo } from '@amap/amap_lbs_common';
import { GlobalThis } from './GlobalThis';
export default class Harmony_location_demoAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // 保存全局上下文,供其他页面使用
    GlobalThis.getInstance().setContext('context', this.context);
  }
  onWindowStageCreate(windowStage: window.WindowStage): void {
    // 加载主页面
    windowStage.loadContent('pages/Index', (err, data) => {
      if (err.code) {
        hilog.error(0x0000, 'testTag', 'Failed to load the content.');
        return;
      }
      hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.');
    });
    
    // 获取应用包名
    let res = AppInfo.getPackageName(getContext(this));
    hilog.info(0x0000, 'testTag', '%{public}s', res);
  }
}


入口文件做了两件事:保存上下文、加载主页面。AppInfo.getPackageName是高德SDK提供的方法,可以用来验证SDK是否集成成功。


2.3 主页面导航(Index.ets)

这里你可以根据实际情况自己定义。你可以放几个button去跑一下官网的API Demo

@Entry
@Component
struct Index {
  pageInfos: NavPathStack = new NavPathStack();
  build() {
    Navigation(this.pageInfos) {
      Scroll() {
        Column() {
          // 定位功能入口
          Button('定位')
            .ButtonCommonStyle()
            .onClick(() => {
              this.pageInfos.pushPath({ name: 'LocationPage' });
            })
          Divider()
          // 地理围栏功能入口
      
          // 距离计算功能入口
     
          // 坐标转换功能入口
      
          // 区域判断功能入口
  
        }
      }
    }
  }
}
// 按钮通用样式扩展
@Extend(Button)
function ButtonCommonStyle() {
  .stateEffect(true)
  .type(ButtonType.Normal)
  .width('100%')
  .backgroundColor(Color.Transparent)
  .align(Alignment.Start)
  .fontColor(Color.Black)
  .padding({ left:32, top:16, bottom:16, right:16 })
  .backgroundColor(Color.White)
}


简单实现个导航菜单,点不同的按钮跳到对应的功能页面。样式统一用ButtonCommonStyle,保证所有按钮看起来一致。


三、定位功能详解(LocationPage.ets)

3.1 SDK 初始化

const key = "此处替换官网申请的Key";  // 替换为你的高德 API Key
aboutToAppear() {
  // 设置 API Key
  AMapLocationManagerImpl.setApiKey(key);
  
  // 更新隐私合规状态
  AMapLocationManagerImpl.updatePrivacyShow(
    AMapPrivacyShowStatus.DidShow,      // 已展示隐私政策
    AMapPrivacyInfoStatus.DidContain,   // 已包含隐私信息
    getContext(this)
  );
  
  // 同意隐私政策
  AMapLocationManagerImpl.updatePrivacyAgree(
    AMapPrivacyAgreeStatus.DidAgree, 
    getContext(this)
  );
  
  // 创建定位管理器实例
  if (this.locationManger === undefined) {
    this.locationManger = new AMapLocationManagerImpl(this.context);
  }
}


高德SDK有个硬性要求:必须先调隐私合规方法,才能初始化定位管理器。顺序是:setApiKey → updatePrivacyShow → updatePrivacyAgree。少一步或者顺序错了,定位都会失败。


3.2 权限检查与请求

鸿蒙三方SDK对接-手把手教你对接调试高德定位SDK

// 检查定位权限
async checkPermissions(): Promise<boolean> {
  // 1. 检查系统定位开关是否开启
  let locationEnabled = geoLocationManager.isLocationEnabled();
  if (!locationEnabled) {
    this.dialogConLocationEnable.open();  // 提示用户开启定位
    return false;
  }
  
  // 2. 检查应用权限授权状态
  const permissions: Array<Permissions> = ['ohos.permission.APPROXIMATELY_LOCATION'];
  let grantStatus: abilityAccessCtrl.GrantStatus = await this.checkAccessToken(permissions[0]);
  if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
    return true;  // 已授权
  } else {
    // 请求权限
    return await this.reqPermissionsFromUser(CommonConstants.REQUEST_PERMISSIONS);
  }
}
// 请求用户授权
async reqPermissionsFromUser(permissions: Array<Permissions>): Promise<boolean> {
  let context: Context = getContext(this) as common.UIAbilityContext;
  let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
  
  // 弹出权限请求对话框
  let permissionRequestResult = await atManager.requestPermissionsFromUser(context, permissions);
  let grantStatus: Array<number> = permissionRequestResult.authResults;
  
  // 检查是否所有权限都已授权
  let haspermissions: boolean = false;
  for (let i = 0; i < grantStatus.length; i++) {
    if (grantStatus[i] === 0) {
      haspermissions = true;
    }
  }
  
  if (!haspermissions) {
    this.dialogOpenPermissions.open();  // 引导用户到设置页面
    return false;
  }
  return true;
}

鸿蒙三方SDK对接-手把手教你对接调试高德定位SDK

权限检查分两步:先看系统定位开没开,再看应用权限给没给。系统定位没开的话,代码里没法直接打开,只能弹窗提示用户去设置里开。

鸿蒙三方SDK对接-手把手教你对接调试高德定位SDK


3.3 单次定位-初始化加载或精准POI等场景

鸿蒙三方SDK对接-手把手教你对接调试高德定位SDK

Button("单次定位")
  .onClick(async () => {
    // 1. 检查权限
    let haspermissions = await this.checkPermissions();
    if (haspermissions) {
      // 2. 定义定位监听器
      let listener: IAMapLocationListener = {
        onLocationChanged: (location) => {
          // 定位成功回调
          systemDateTime.getCurrentTime(false).then((time) => {
            const date = new Date(time);
            this.response = " 时间:" + date.toLocaleTimeString() + "\n"
              + " 经度:" + location.longitude + "\n"
              + " 纬度:" + location.latitude + "\n"
              + " 海拔:" + location.altitude + "\n"
              + " 精度:" + location.accuracy + "\n"
              + " 速度:" + location.speed + "\n"
              + " UTC时间:" + location.timeStamp + "\n"
              + " 方向:" + location.direction + "\n"
              + " 自启动以来时间:" + location.timeSinceBoot + "\n"
              + " 附加信息:" + location.additions + "\n"
              + " 附加信息size:" + location.additionSize + "\n"
              + " 逆地理:" + location.reGeo?.desc;
          });
        }, 
        onLocationError: (error) => {
          // 定位失败回调
          console.error('error  ' + JSON.stringify(error));
        }
      };
      // 3. 配置定位参数
      let options: AMapLocationOption = {
        priority: geoLocationManager.LocationRequestPriority.FIRST_FIX,  // 优先级:快速定位
        scenario: geoLocationManager.LocationRequestScenario.UNSET,      // 场景:未设置
        maxAccuracy: 0,                      // 最大精度(0表示不限制)
        singleLocationTimeout: 3000,         // 单次定位超时时间(毫秒)
        allowsBackgroundLocationUpdates: false,  // 不允许后台定位
        locatingWithReGeocode: false,        // 不进行逆地理编码
        reGeocodeLanguage: AMapLocationReGeocodeLanguage.Chinese,  // 逆地理语言
        isOffset: true                       // 是否进行坐标偏移(国测局加密)
      };
      // 4. 设置监听器和参数
      this.locationManger?.setLocationListener(AMapLocationType.Single, listener);
      this.locationManger?.setLocationOption(AMapLocationType.Single, options);
      
      // 5. 发起单次定位请求
      this.locationManger?.requestSingleLocation();
    }
  })


单次定位是最常用的功能,点一下按钮,定位一次,返回结果。listener里定义了成功和失败的回调,options里配置定位参数,最后调用requestSingleLocation()发起请求。


3.4 连续定位-用户设备持续更新定位

鸿蒙三方SDK对接-手把手教你对接调试高德定位SDK

Button("开始连续定位")
  .onClick(async () => {
    let haspermissions = await this.checkPermissions();
    if (haspermissions) {
      // 定义定位监听器
      let listener: IAMapLocationListener = {
        onLocationChanged: (location) => {
          // 持续接收定位信息
          systemDateTime.getCurrentTime(false).then((time) => {
            const date = new Date(time);
            this.response = " 时间:" + date.toLocaleTimeString() + "\n"
              + " 经度:" + location.longitude + "\n"
              + " 纬度:" + location.latitude + "\n"
              // ... 其他信息
          });
        }, 
        onLocationError: (error) => {
          console.error('error  ' + JSON.stringify(error));
        }
      };
      // 配置连续定位参数
      let options: AMapLocationOption = {
        priority: geoLocationManager.LocationRequestPriority.FIRST_FIX,
        scenario: geoLocationManager.LocationRequestScenario.UNSET,
        timeInterval: 2,           // 定位时间间隔(秒)
        distanceInterval: 0,       // 定位距离间隔(米,0表示不限制)
        maxAccuracy: 20,           // 最大精度(米)
        allowsBackgroundLocationUpdates: false,
        singleLocationTimeout: 5000,
        locatingWithReGeocode: true,   // 进行逆地理编码
        reGeocodeLanguage: AMapLocationReGeocodeLanguage.Chinese,
        isOffset: true
      };
      this.locationManger?.setLocationListener(AMapLocationType.Updating, listener);
      this.locationManger?.setLocationOption(AMapLocationType.Updating, options);
      
      // 开始连续定位
      this.locationManger?.startUpdatingLocation();
    }
  })
// 停止连续定位
Button("停止连续定位")
  .onClick(() => {
    this.locationManger?.stopUpdatingLocation();
  })


连续定位适合运动轨迹、导航等场景。区别主要在options里多了timeInterval,表示每隔几秒更新一次位置。记得提供停止按钮,不然会一直定下去,耗电很快。


3.5 后台定位-省电或保活场景

// 开启后台定位
Button("开启后台定位")
  .onClick(async () => {
    let haspermissions = await this.checkPermissions();
    if (haspermissions) {
      // 启动后台长时任务
      this.locationManger?.startContinuousTask();
    }
  })
// 停止后台定位
Button("停止后台定位")
  .onClick(() => {
    // 停止后台长时任务
    this.locationManger?.stopContinuousTask();
  })


后台定位需要配合前面module.json5里的backgroundModes配置。startContinuousTask()是告诉系统:我要在后台持续定位,你别把我杀了。


3.6 获取最后定位-APP缓存、二次打开场景

鸿蒙三方SDK对接-手把手教你对接调试高德定位SDK

Button("最后定位")
  .onClick(() => {
    // 定义监听器
    let listener: IAMapLocationListener = {
      onLocationChanged: (location) => {
        // 获取到最后一次定位信息
        systemDateTime.getCurrentTime(false).then((time) => {
          const date = new Date(time);
          this.response = " 时间:" + date.toLocaleTimeString() + "\n"
            + " 经度:" + location.longitude + "\n"
            + " 纬度:" + location.latitude + "\n"
            // ... 其他信息
        });
      }, 
      onLocationError: (error) => {
        console.error('error  ' + JSON.stringify(error));
      }
    };
    // 配置参数
    let options: AMapLocationOption = {
      allowsBackgroundLocationUpdates: true,
      locatingWithReGeocode: true,
      reGeocodeLanguage: AMapLocationReGeocodeLanguage.Chinese,
      isOffset: false
    };
    
    this.locationManger?.setLocationOption(AMapLocationType.Last, options);
    this.locationManger?.setLocationListener(AMapLocationType.Last, listener);
    
    // 请求最后一次定位
    this.locationManger?.requestLastLocation();
  })


有时候我们不需要重新定位,用缓存的上次位置就够了。这个接口返回的是SDK内存里保存的上次定位结果,不重新发起定位请求,速度快,省电。


四、参数详解:搞懂这些才能调优

interface AMapLocationOption {
  priority?: LocationRequestPriority;           // 定位优先级
  scenario?: LocationRequestScenario;           // 定位场景
  timeInterval?: number;                        // 定位时间间隔(秒)
  distanceInterval?: number;                    // 定位距离间隔(米)
  maxAccuracy?: number;                         // 最大精度(米)
  singleLocationTimeout?: number;               // 单次定位超时(毫秒)
  allowsBackgroundLocationUpdates?: boolean;    // 是否允许后台定位
  locatingWithReGeocode?: boolean;              // 是否进行逆地理编码
  reGeocodeLanguage?: AMapLocationReGeocodeLanguage;  // 逆地理语言
  isOffset?: boolean;                           // 是否进行坐标偏移
}


4.1 定位优先级

// 快速定位:优先使用网络和缓存,首次定位快但精度低
geoLocationManager.LocationRequestPriority.FIRST_FIX
// 高精度定位:优先使用GPS,精度高但耗时
geoLocationManager.LocationRequestPriority.ACCURACY
// 低功耗定位:只使用网络和基站,省电
geoLocationManager.LocationRequestPriority.LOW_POWER


选择建议:

  • 地图首次打开用 FIRST_FIX

  • 运动记录用 ACCURACY

  • 后台用 LOW_POWER


4.2 定位场景

// 导航场景:连续定位,高频率
LocationRequestScenario.NAVIGATION
// 轨迹追踪:次高频,平衡精度和功耗
LocationRequestScenario.TRAJECTORY_TRACKING
// 打车场景:单次定位为主
LocationRequestScenario.CAR_HAILING
// 日常服务:低频,省电
LocationRequestScenario.DAILY_LIFE_SERVICE


4.3 常用参数配置

参数作用推荐值
timeInterval定位时间间隔(秒)2-10
distanceInterval定位距离间隔(米)0(不限制)或10
maxAccuracy最大精度要求(米)20-100
singleLocationTimeout单次定位超时(毫秒)3000-10000
allowsBackgroundLocationUpdates是否允许后台定位按需开启


根据这篇实操教学后,我们已经掌握了:

✅ 项目结构和依赖配置

✅ 权限配置和动态申请

✅ SDK 初始化流程

✅ 单次定位、连续定位、后台定位的实现