前言

地图上那些能点击的图标叫做 Marker(标记),是地图应用最核心的 UI 元素之一。在附近加油站里,每个加油站在地图上都有一个自定义图标(加油站 SVG 图案),点击后标记会放大,底部列表也会出现。

这篇文章详细讲 Marker 的添加、自定义图标配置、信息窗口显示以及点击事件处理。

项目预览

一、addMapMaker 方法解析

// MapUtil.ets
async addMapMaker(latitude: number, longitude: number, mapController: map.MapComponentController): Promise<void> {
  let markerOptions: mapCommon.MarkerOptions = {
    position: {
      latitude: latitude,
      longitude: longitude
    },
    rotation: 0,                           // 旋转角度(0度,即不旋转)
    visible: true,                         // 是否可见
    zIndex: 0,                             // 层叠顺序
    alpha: 1,                              // 透明度(1=完全不透明)
    anchorU: 0.5,                          // 锚点X(0-1,0.5=水平居中)
    anchorV: 1,                            // 锚点Y(0-1,1=底部对齐到坐标点)
    clickable: true,                       // 是否可点击
    flat: true,                            // 是否贴地(随地图倾斜)
    icon: 'station.svg',                   // 自定义图标(SVG文件名)
    collisionRule: mapCommon.CollisionRule.NAME, // 碰撞规则
  };

  // 添加 Marker
  await mapController.addMarker(markerOptions);
}

二、MarkerOptions 每个参数详解

2.1 position:坐标位置

position: {
  latitude: latitude,
  longitude: longitude
}

Marker 在地图上显示的位置,使用 GCJ02 坐标(中国境内)。

2.2 anchor:锚点

anchorU: 0.5,   // 水平方向锚点(0=左边, 0.5=中间, 1=右边)
anchorV: 1,     // 垂直方向锚点(0=顶部, 0.5=中间, 1=底部)

锚点决定 Marker 图标的哪个点对应地图坐标:

anchorU=0.5, anchorV=1 的效果:
┌────────────┐
│            │
│  [图标]    │
│            │
└─────●──────┘
      ↑
   图标底部中央对准坐标点(像图钉一样)

对于加油站这种图标,anchorV=1(底部对齐)让图标的底部尖端精确指向坐标点,视觉上更准确。

2.3 icon:自定义图标

icon: 'station.svg',  // 使用 SVG 文件作为图标

icon 指定图标文件名,文件放在 resources/base/media/ 目录下。支持 PNG、SVG 等格式。

为什么选 SVG?

格式 优点 缺点
PNG 支持复杂图案 放大会模糊,需要提供@2x/@3x多分辨率
SVG 矢量,任意放大不失真,文件小 不支持过于复杂的效果

地图标记需要在不同缩放级别下保持清晰,SVG 是更好的选择。

2.4 flat:贴地模式

flat: true,
  • flat: true:标记贴在地图表面,地图倾斜时标记也跟着倾斜(更立体的感觉)
  • flat: false(默认):标记始终面向用户(始终垂直于屏幕)

2.5 zIndex:层叠顺序

zIndex: 0,

当多个 Marker 重叠时,zIndex 越大的显示在上层。默认 0,如果你希望某个重要标记始终在最上面,给它设置更大的 zIndex。

2.6 alpha:透明度

alpha: 1,  // 1=完全不透明,0=完全透明,0.5=半透明

2.7 collisionRule:碰撞规则

collisionRule: mapCommon.CollisionRule.NAME,

当多个 Marker 在屏幕上位置太近时,如何处理碰撞:

碰撞规则 说明
NAME 按名称决定显示优先级
NONE 不处理碰撞(都显示)

三、批量添加 Marker

// GasStationPage.ets - callback 里
this.stationInfoList.forEach(async (stationItem: StationData) => {
  await mapUtil.addMapMaker(
    stationItem.latitude,
    stationItem.longitude,
    this.mapController as map.MapComponentController
  );
});

遍历加油站列表,为每个加油站在对应坐标添加一个 Marker。注意这里用的是 async forEach——虽然语法上没问题,但有一个潜在问题:forEach 不能正确等待 async 回调完成

更严谨的写法:

// 更严谨:用 for...of + await 确保顺序添加
for (let stationItem of this.stationInfoList) {
  await mapUtil.addMapMaker(
    stationItem.latitude,
    stationItem.longitude,
    this.mapController as map.MapComponentController
  );
}

对于这个场景,并发添加还是顺序添加差别不大,forEach 写法更简洁,但要知道它不是真正的顺序等待。

四、Marker 点击事件

在 GasStationPage 的 callback 里监听 Marker 点击:

this.mapController.on('markerClick', (marker) => {
  // 1. 显示底部加油站列表
  this.isShow = true;

  // 2. 显示标记的信息窗口(弹出小气泡)
  marker.setInfoWindowVisible(true);

  // 3. 记录当前点击的标记(用于后续重置)
  this.curMarker = marker;

  // 4. 设置放大比例并播放动画
  this.imageScale = 1.5;
  mapUtil.imageAnimation(marker, this.imageScale);

  // 5. 移动地图到该标记位置
  mapUtil.moveToCurrentPosition(
    marker.getPosition().latitude,
    marker.getPosition().longitude,
    mapController
  );
});

marker 对象的常用方法

方法 作用
marker.getPosition() 获取标记的经纬度
marker.setInfoWindowVisible(true) 显示信息窗口(小气泡)
marker.setAnimation(animation) 设置动画
marker.startAnimation() 启动动画
marker.setVisible(true) 设置可见性
marker.setIcon('newIcon.png') 动态更换图标

五、信息窗口(InfoWindow)

marker.setInfoWindowVisible(true);  // 显示信息窗口
marker.setInfoWindowVisible(false); // 隐藏信息窗口

信息窗口是点击 Marker 后弹出的小气泡,默认显示 Marker 的 title 和 snippet:

let markerOptions: mapCommon.MarkerOptions = {
  // ...
  title: '中国石化AA站',        // 信息窗口标题
  snippet: 'N市J区XX大街587号', // 信息窗口副标题
};

项目里没有设置 titlesnippet,所以信息窗口是空的。调用 setInfoWindowVisible(true) 只是触发一个空白气泡,实际信息展示在底部的 bindSheet 里。

六、底部弹窗关闭时重置动画

当底部弹窗关闭时,需要把放大的 Marker 恢复原始大小:

// GasStationPage.ets - bindSheet 的 onWillDismiss 回调
.bindSheet($$this.isShow, this.bindBuilder(), {
  // ...
  onWillDismiss: ((dismissSheetAction: DismissSheetAction) => {
    if (this.curMarker) {
      this.imageScale = 1;                              // 恢复原始大小
      mapUtil.imageAnimation(this.curMarker, this.imageScale); // 播放缩小动画
    }
    dismissSheetAction.dismiss();  // 确认关闭弹窗
  })
});

七、完整的 Marker 生命周期

页面进入 → init()
  └─> addMapMaker() × 4   (添加4个加油站标记)

用户点击标记:
  └─> on('markerClick')
       ├─> setInfoWindowVisible(true)  (显示气泡)
       ├─> imageAnimation(scale=1.5)   (放大动画)
       ├─> moveToCurrentPosition()     (移动镜头)
       └─> isShow = true               (显示底部列表)

用户关闭底部列表:
  └─> onWillDismiss
       └─> imageAnimation(scale=1)     (缩小恢复)

总结

Marker 是地图的核心 UI 元素:

  1. 创建:构建 MarkerOptions(坐标、图标、锚点、可见性等)
  2. 添加mapController.addMarker(markerOptions),注意是 async 的
  3. 监听点击mapController.on('markerClick', callback),callback 接收 marker 对象
  4. 动态操作:通过 marker 对象的方法控制显示/隐藏/动画

自定义图标使用 SVG,锚点 anchorV=1 让图标底部对准坐标点,体验更准确。

下一篇讲相机动画——地图镜头移动是怎么实现流畅动画效果的。

Logo

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

更多推荐