前言

如果一个开发者想要开发一个与地址、地理信息等相关的软件(如地图软件、打车软件等),在软件中嵌入地图是一个必须的环节。
通常来说,现在的原生鸿蒙软件嵌入地图有两种方式:Web嵌套和原生组件嵌套。本文将专注于原生地图组件嵌套,力求每一个读者在看完本文都可以独立实现一个简单的包含地图显示和定位的小型demo,以帮助从未接触过的开发者可以直接理解这一开发流程。

为保证软件使用的信息、权限安全,鸿蒙系统对敏感权限的使用作了诸多限制,本文所展示的地图和定位就是其中一种,所以,还请开发者耐心看完,跟着流程一步步完成。

经实测,DevEco Studio 5.0.4不能正常使用该demo(由于第三方组件配置的问题)。测试时,务必保证调试证书、权限配置完好,设备位置定位功能打开,应用位置权限授权正常。

鸿蒙开发者应已完成实名注册和开发者认证。

项目开发

权限申请

使用相关功能需要在如下位置申请权限:

"requestPermissions": [
      {
        "name": "ohos.permission.LOCATION",
        "reason": "$string:location_reason",
        "usedScene": {
          "abilities": [ "EntryAbility" ],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.APPROXIMATELY_LOCATION",
        "reason": "$string:location_reason",
        "usedScene": {
          "abilities": [ "EntryAbility" ],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.INTERNET",
        "reason": "$string:internet_reason",
        "usedScene": {
          "abilities": [ "EntryAbility" ],
          "when": "inuse"
        }
      }

    ],

前两个是定位权限(定位相关),后一个是网络权限(地图相关)。由于现在权限申请需要更加规范,故而需要声明多种属性(自测时发现不写reasen等会报错,可能是我的api版本较高的原因)。如果不需要,也可以省略此处除name以外的字段。reason的字段需要在如下位置声明:

    {
      "name": "location_reason",
      "value": "获取位置"
    },
    {
      "name": "internet_reason",
      "value": "获取网络"
    },
    {
      "name": "approximately_location_reason",
      "value": "用于获取模糊定位"
    }

关于权限的问题还有一个疑问,文档中心 此处写,目前定位需要依靠@ohos.geoLocationManager (位置服务),且这个服务需要发邮件申请,审核通过方可使用。但是,我不理解这个机制,我申请了但是并没有反馈,并且似乎对我们后面的操作没有影响。

引入第三方库组件

OpenHarmony三方库中心仓 ,此处是第三方组件链接。
@pura/harmony-utils(V1.3.6)这个第三方库可以帮助我们简化代码流程,这里不再赘述第三方库组件的安装方式。

我们需要使用的是其中LocationUtil类。

这个组件需要进行初始化,请在如下位置写入代码:

import { AppUtil } from '@pura/harmony-utils';

//onCreate中

AppUtil.init(this.context);

用户授权

应用想要获取设备的位置权限需要用户手动授权位置信息。

我们在Index页面的aboutToAppear生命周期函数中写下:

import { LocationUtil } from '@pura/harmony-utils';     //需要导入

LocationUtil.requestLocationPermissions();

编写代码

进入基础Index页面,开始编写。

初始化地图

import { MapComponent, mapCommon, map } from '@kit.MapKit' // 地图组件与类型定义
import { AsyncCallback } from '@kit.BasicServicesKit'

@Entry
@Component
struct Index {
  // 地图控制器,用于操作地图(缩放、添加 marker 等)
  private mapController?: map.MapComponentController

  // 地图初始化参数(设置中心位置、缩放等级等)
  private mapOptions?: mapCommon.MapOptions

  // 地图组件的初始化回调,用于拿到 mapController
  private mapCallback?: AsyncCallback<map.MapComponentController>

  // 地图事件管理器(注册点击事件等)
  private mapEventManager?: map.MapEventManager

  // 用于防止重复注册 mapClick 事件
  private mapClickRegistered: boolean = false

  aboutToAppear(): void {
    // 设置地图初始化参数(初始中心点:北京天安门)
    this.mapOptions = {
      position: {
        target: {
          latitude: 39.9,
          longitude: 116.4
        },
        zoom: 10
      }
    }

    // 地图控件初始化完成后的回调
    this.mapCallback = async (err, mapController) => {
      if (!err) {
        // 绑定 controller
        this.mapController = mapController

        // 获取地图事件管理器
        this.mapEventManager = mapController.getEventManager()

        // 仅注册一次 mapClick 事件
        if (!this.mapClickRegistered) {
          this.mapEventManager?.on('mapClick', async (position) => {
            //备用
          })

          this.mapClickRegistered = true
        }
      } else {
        console.error('地图初始化失败', JSON.stringify(err))
      }
    }
  }

  build() {
    // 页面布局结构:整页展示地图
    Column() {
      MapComponent({
        mapOptions: this.mapOptions,
        mapCallback: this.mapCallback
      })
        .width('100%')
        .height('100%')
    }
    .width('100%')
    .height('100%')
  }
}

这时,我们就可以看到完整的地图了:(样式可能不同,因为我这里还设置了沉浸式)
 

添加定位 

import { LocationUtil } from '@pura/harmony-utils' // 封装好的定位工具库
import { MapComponent, mapCommon, map } from '@kit.MapKit' // 地图组件与类型定义
import { AsyncCallback } from '@kit.BasicServicesKit'

@Entry
@Component
struct Index {
  // 地图控制器,用于操作地图(缩放、添加 marker 等)
  private mapController?: map.MapComponentController

  // 地图初始化参数(设置中心位置、缩放等级等)
  private mapOptions?: mapCommon.MapOptions

  // 地图组件的初始化回调,用于拿到 mapController
  private mapCallback?: AsyncCallback<map.MapComponentController>

  // 地图事件管理器(注册点击事件等)
  private mapEventManager?: map.MapEventManager

  // 用于防止重复注册 mapClick 事件
  private mapClickRegistered: boolean = false

  aboutToAppear(): void {
    // 请求定位权限(必要,否则 getCurrentLocation 会失败)
    LocationUtil.requestLocationPermissions()

    // 判断设备当前是否开启了定位服务
    const isEnabled = LocationUtil.isLocationEnabled()
    console.log('获取定位情况', isEnabled)

    // 设置地图初始化参数(初始中心点:北京天安门)
    this.mapOptions = {
      position: {
        target: {
          latitude: 39.9,
          longitude: 116.4
        },
        zoom: 10
      }
    }

    // 地图控件初始化完成后的回调
    this.mapCallback = async (err, mapController) => {
      if (!err) {
        // 绑定 controller
        this.mapController = mapController

        // 获取地图事件管理器
        this.mapEventManager = mapController.getEventManager()

        // 开启系统定位图层(蓝点)
        this.mapController.setMyLocationEnabled(true)

        // 获取当前位置(异步)
        const location = await LocationUtil.getCurrentLocationEasy()

        // 创建一个相机位置对象(跳转至当前位置)
        const target: mapCommon.LatLng = {
          latitude: location.latitude,
          longitude: location.longitude
        }
        const cameraPosition: mapCommon.CameraPosition = {
          target: target,
          zoom: 10
        }
        const cameraUpdate: map.CameraUpdate = map.newCameraPosition(cameraPosition)

        // 执行视角移动动画
        this.mapController?.animateCamera(cameraUpdate, 500)

        // 开启右下角定位按钮
        this.mapController?.setMyLocationControlsEnabled(true)

        // 仅注册一次 mapClick 事件
        if (!this.mapClickRegistered) {
          this.mapEventManager?.on('mapClick', async (position) => {
            //备用
            
          })

          this.mapClickRegistered = true
        }
      } else {
        console.error('地图初始化失败', JSON.stringify(err))
      }
    }
  }

  build() {
    // 页面布局结构:整页展示地图
    Column() {
      MapComponent({
        mapOptions: this.mapOptions,
        mapCallback: this.mapCallback
      })
        .width('100%')
        .height('100%')
    }
    .width('100%')
    .height('100%')
  }
}

此时,如果点击右下角定位图标或是初始打开应用,均会定位到你的设备的位置了。(图示为虚拟地址),并且,在第一次打开时还会邀你授权权限(似乎真机才有)。
初始打开应用定位是依靠第三方组件,点击定位图标实现定位依靠地图组件。

附加功能

添加一个点击地点,进行标记的功能:

//在点击事件里面插入即可
// 构建点击位置的 marker
   const marker: mapCommon.MarkerOptions = {
       position,
    title: '点击了',
    clickable: true
    }
    // 动画视角跳转至点击位置
    const target: mapCommon.LatLng = {
        latitude: position.latitude,
        longitude: position.longitude
     }
     const cameraPosition: mapCommon.CameraPosition = {
         target: target,
         zoom: 10
      }
     const cameraUpdate: map.CameraUpdate = map.newCameraPosition(cameraPosition)
     this.mapController?.animateCamera(cameraUpdate, 500)
     // 清除旧 marker(如果有),添加新 marker标志
     await this.mapController?.clear()
     await this.mapController?.addMarker(marker)

 总的代码如下:

import { LocationUtil } from '@pura/harmony-utils' // 封装好的定位工具库
import { MapComponent, mapCommon, map } from '@kit.MapKit' // 地图组件与类型定义
import { AsyncCallback } from '@kit.BasicServicesKit'

@Entry
@Component
struct Index {
  // 地图控制器,用于操作地图(缩放、添加 marker 等)
  private mapController?: map.MapComponentController

  // 地图初始化参数(设置中心位置、缩放等级等)
  private mapOptions?: mapCommon.MapOptions

  // 地图组件的初始化回调,用于拿到 mapController
  private mapCallback?: AsyncCallback<map.MapComponentController>

  // 地图事件管理器(注册点击事件等)
  private mapEventManager?: map.MapEventManager

  // 用于防止重复注册 mapClick 事件
  private mapClickRegistered: boolean = false

  aboutToAppear(): void {
    // 请求定位权限(必要,否则 getCurrentLocation 会失败)
    LocationUtil.requestLocationPermissions()

    // 判断设备当前是否开启了定位服务
    const isEnabled = LocationUtil.isLocationEnabled()
    console.log('获取定位情况', isEnabled)

    // 设置地图初始化参数(初始中心点:北京天安门)
    this.mapOptions = {
      position: {
        target: {
          latitude: 39.9,
          longitude: 116.4
        },
        zoom: 10
      }
    }

    // 地图控件初始化完成后的回调
    this.mapCallback = async (err, mapController) => {
      if (!err) {
        // 绑定 controller
        this.mapController = mapController

        // 获取地图事件管理器
        this.mapEventManager = mapController.getEventManager()

        // 开启系统定位图层(蓝点)
        this.mapController.setMyLocationEnabled(true)

        try {         //异步操作,用于位移操作,如果不加try-catch可能会导致定位显示地图不显示
          const location = await LocationUtil.getCurrentLocationEasy()
          console.log('获取位置成功:', JSON.stringify(location))

          const target: mapCommon.LatLng = {
            latitude: location.latitude,
            longitude: location.longitude
          }
          const cameraPosition: mapCommon.CameraPosition = { target, zoom: 10 }
          const cameraUpdate: map.CameraUpdate = map.newCameraPosition(cameraPosition)
          // 动画移动地图
          this.mapController?.animateCamera(cameraUpdate, 500)
        } catch (err) {
          console.error('获取定位或设置视角失败:', JSON.stringify(err))
        }

        // 开启右下角定位按钮
        this.mapController?.setMyLocationControlsEnabled(true)

        // 仅注册一次 mapClick 事件
        if (!this.mapClickRegistered) {
          this.mapEventManager?.on('mapClick', async (position) => {
            // 构建点击位置的 marker
            const marker: mapCommon.MarkerOptions = {
              position,
              title: '点击了',
              clickable: true
            }

            // 动画视角跳转至点击位置
            const target: mapCommon.LatLng = {
              latitude: position.latitude,
              longitude: position.longitude
            }
            const cameraPosition: mapCommon.CameraPosition = {
              target: target,
              zoom: 10
            }
            const cameraUpdate: map.CameraUpdate = map.newCameraPosition(cameraPosition)
            this.mapController?.animateCamera(cameraUpdate, 500)

            // 清除旧 marker(如果有),添加新 marker标志
            await this.mapController?.clear()
            await this.mapController?.addMarker(marker)
          })

          this.mapClickRegistered = true
        }
      } else {
        console.error('地图初始化失败', JSON.stringify(err))
      }
    }
  }

  build() {
    // 页面布局结构:整页展示地图
    Column() {
      MapComponent({
        mapOptions: this.mapOptions,
        mapCallback: this.mapCallback
      })
        .width('100%')
        .height('100%')
    }
    .width('100%')
    .height('100%')
  }
}

权限申请和配置

AGC建立项目和配置项目

打开AppGallery Connect官网。如下:

点击“开发与服务”,进入相关页面,并新建项目。开发与服务页面如下:

根据流程完成项目新建(设置项目名称,此处我的设置是“地图DEMO”,名字并不重要):

接着进入项目配置页面,按照图示,至少勾选以下三种服务(重要):

到此ACG配置告一段落。

DevEco Studio配置

新建项目

API选择12以上即可,包名可以使用默认或者自行配置。包名是安装在设备上的路径和标识,后续有需要,还请记住这个包名。

以后如果需要再次查看,可以在此处查看:
 

生成所需私钥和证书

见此文:鸿蒙开发-获取项目所需的私钥和证书-CSDN博客 

至此,我们应获取了如下四种文件:

配置开发项目证书

在完成了各种私钥和证书之后,我们需要将这些文件与开发项目绑定。 

 保存之后,在此处可以看到信息:

 除此之外,还需要在entry->src->main->module.json5中添加如下代码:

    "metadata": [
      {
        "name": "client_id",
        "value": "************"          //这里放进去你的client_id,查阅地方如下图所示
      }
    ]

如图所示:

 还需要配置以下信息,即添加公钥指纹。

至此,我们完成了所有需要的配置 。

总结

地图和定位功能是一种常见的需求,但是实际上开发并不简单,在鸿蒙当中去开发它主要的难点就是各种权限,稍不注意就会出错,所以我希望这篇文章能给读者一个有效的帮助。
其实这里有个重要的先后顺序,即需要你完成了代码的编写(主要是服务的申请),才进行项目权限的申请和配置,否则可能会不生效。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐