HarmonyOS中开发高德地图第四篇:地图标记Marker

45 阅读5分钟

第四篇:地图标记Marker

本篇教程将学习如何在地图上添加各种标记,包括默认标记、自定义图标、自定义View标记,以及标记的交互功能。

学习目标

  • 添加默认样式标记
  • 使用自定义图标
  • 创建自定义View标记
  • 处理标记点击和拖拽事件
  • 标记动画效果

1. Marker基础

Marker(标记)是地图上最常用的覆盖物,用于标注特定位置。

1.1 MarkerOptions 常用属性

属性方法说明
位置setPosition(LatLng)标记的地理坐标
标题setTitle(string)点击显示的标题
描述setSnippet(string)点击显示的描述
图标setIcon(BitmapDescriptor)自定义图标
锚点setAnchor(u, v)图标锚点位置(0-1)
可拖拽setDraggable(boolean)是否可拖拽
可点击setClickable(boolean)是否可点击
层级setZIndex(number)覆盖物层级
可见性setVisible(boolean)是否可见
旋转setRotateAngle(angle)旋转角度

2. 完整代码示例

创建文件 entry/src/main/ets/pages/Demo03_Marker.ets

import {
  AMap,
  MapView,
  MapViewComponent,
  MapViewManager,
  MapViewCreateCallback,
  LatLng,
  Marker,
  MarkerOptions,
  BitmapDescriptor,
  BitmapDescriptorFactory,
  OnMarkerDragListener,
  CameraUpdateFactory
} from '@amap/amap_lbs_map3d';

const MAP_VIEW_NAME = 'MarkerDemo';

// 获取应用上下文
let globalContext: Context;

/**
 * 标记信息接口
 */
interface MarkerInfo {
  name: string;
  lat: number;
  lng: number;
  description: string;
}

@Entry
@Component
struct Demo03_Marker {
  private mapView: MapView | undefined = undefined;
  private aMap: AMap | undefined = undefined;
  
  // 存储所有标记
  private markers: Marker[] = [];
  
  @State isMapReady: boolean = false;
  @State markerInfo: string = '点击标记查看信息';
  @State dragInfo: string = '';
  
  // 示例地点数据
  private places: MarkerInfo[] = [
    { name: '天安门', lat: 39.909187, lng: 116.397451, description: '北京市中心地标' },
    { name: '故宫', lat: 39.916345, lng: 116.397155, description: '世界文化遗产' },
    { name: '王府井', lat: 39.914539, lng: 116.410276, description: '著名商业街' },
    { name: '北海公园', lat: 39.924091, lng: 116.389472, description: '皇家园林' }
  ];

  private mapViewCreateCallback: MapViewCreateCallback = 
    (mapview: MapView | undefined, mapViewName: string | undefined) => {
      if (!mapview || mapViewName !== MAP_VIEW_NAME) return;

      this.mapView = mapview;
      this.mapView.onCreate();
      
      this.mapView.getMapAsync((map: AMap) => {
        this.aMap = map;
        this.isMapReady = true;
        
        // 移动到北京
        const beijing = new LatLng(39.916345, 116.397155);
        map.moveCamera(CameraUpdateFactory.newLatLngZoom(beijing, 14));
        
        // 启用缩放控件
        map.getUiSettings()?.setZoomControlsEnabled(true);
        
        // 设置标记事件监听
        this.setupMarkerListeners();
      });
    };

  /**
   * 设置标记事件监听
   */
  private setupMarkerListeners(): void {
    if (!this.aMap) return;
    
    // 1. 标记点击事件
    this.aMap.setOnMarkerClickListener((marker: Marker): boolean => {
      console.info('[Marker] Clicked:', marker.getTitle());
      
      const title = marker.getTitle() || '未命名';
      const snippet = marker.getSnippet() || '';
      const position = marker.getPosition();
      
      this.markerInfo = `${title}\n${snippet}\n坐标: ${position.latitude.toFixed(4)}, ${position.longitude.toFixed(4)}`;
      
      // 显示InfoWindow
      marker.showInfoWindow();
      
      return true; // 返回true表示已处理,不执行默认行为
    });
    
    // 2. 标记拖拽事件
    this.aMap.setOnMarkerDragListener(new OnMarkerDragListener(
      // 开始拖拽
      (marker: Marker) => {
        console.info('[Marker] Drag start:', marker.getTitle());
        this.dragInfo = `开始拖拽: ${marker.getTitle()}`;
      },
      // 拖拽中
      (marker: Marker) => {
        const pos = marker.getPosition();
        this.dragInfo = `拖拽中: ${pos.latitude.toFixed(4)}, ${pos.longitude.toFixed(4)}`;
      },
      // 拖拽结束
      (marker: Marker) => {
        const pos = marker.getPosition();
        console.info('[Marker] Drag end:', pos.latitude, pos.longitude);
        this.dragInfo = `拖拽结束: ${pos.latitude.toFixed(4)}, ${pos.longitude.toFixed(4)}`;
      }
    ));
    
    // 3. InfoWindow点击事件
    this.aMap.setOnInfoWindowClickListener((marker: Marker) => {
      console.info('[Marker] InfoWindow clicked:', marker.getTitle());
      marker.hideInfoWindow();
    });
  }

  /**
   * 添加默认样式标记
   */
  private async addDefaultMarkers(): Promise<void> {
    if (!this.aMap) return;
    
    // 清除之前的标记
    this.clearMarkers();
    
    for (const place of this.places) {
      const options = new MarkerOptions();
      options.setPosition(new LatLng(place.lat, place.lng));
      options.setTitle(place.name);
      options.setSnippet(place.description);
      options.setClickable(true);
      options.setDraggable(false);
      
      const marker = this.aMap.addMarker(options);
      if (marker) {
        this.markers.push(marker);
      }
    }
    
    this.markerInfo = `已添加 ${this.markers.length} 个默认标记`;
  }

  /**
   * 添加不同颜色的标记
   * 注意:必须使用 defaultMarkerASync 异步方法创建彩色图标
   */
  private async addColoredMarkers(): Promise<void> {
    if (!this.aMap) return;
    
    this.clearMarkers();
    
    // 使用异步方法创建彩色标记图标
    const colors: number[] = [
      BitmapDescriptorFactory.HUE_GREEN,
      BitmapDescriptorFactory.HUE_YELLOW,
      BitmapDescriptorFactory.HUE_CYAN,
      BitmapDescriptorFactory.HUE_VIOLET
    ];
    const colorNames: string[] = ['绿色', '黄色', '青色', '紫色'];
    
    for (let i = 0; i < this.places.length && i < colors.length; i++) {
      const place = this.places[i];
      
      const options = new MarkerOptions();
      options.setPosition(new LatLng(place.lat, place.lng));
      options.setTitle(`${place.name}(${colorNames[i]})`);
      options.setClickable(true);
      
      // 重要:使用 defaultMarkerASync 异步方法创建彩色图标
      const icon = await BitmapDescriptorFactory.defaultMarkerASync(globalContext, colors[i]);
      if (icon) {
        options.setIcon(icon);
      }
      
      const marker = this.aMap.addMarker(options);
      if (marker) {
        this.markers.push(marker);
      }
    }
    
    this.markerInfo = `已添加 ${this.markers.length} 个彩色标记`;
  }

  /**
   * 添加自定义图标标记
   */
  private async addCustomIconMarkers(): Promise<void> {
    if (!this.aMap) return;
    
    this.clearMarkers();
    
    for (const place of this.places) {
      const options = new MarkerOptions();
      options.setPosition(new LatLng(place.lat, place.lng));
      options.setTitle(place.name);
      options.setSnippet(place.description);
      
      // 从rawfile加载自定义图标
      // 需要在entry/src/main/resources/rawfile/目录下放置图片
      try {
        const icon = await BitmapDescriptorFactory.fromRawfilePath(
          globalContext, 
          'marker_custom.png'
        );
        if (icon) {
          options.setIcon(icon);
        }
      } catch (e) {
        console.warn('[Marker] Custom icon not found, using default');
      }
      
      // 设置锚点为底部中心
      options.setAnchor(0.5, 1.0);
      
      const marker = this.aMap.addMarker(options);
      if (marker) {
        this.markers.push(marker);
      }
    }
    
    this.markerInfo = `已添加 ${this.markers.length} 个自定义图标标记`;
  }

  /**
   * 添加可拖拽标记
   */
  private async addDraggableMarkers(): Promise<void> {
    if (!this.aMap) return;
    
    this.clearMarkers();
    
    for (const place of this.places) {
      const options = new MarkerOptions();
      options.setPosition(new LatLng(place.lat, place.lng));
      options.setTitle(place.name);
      options.setSnippet('可拖拽');
      options.setDraggable(true);  // 允许拖拽
      options.setClickable(true);
      
      // 使用橙色表示可拖拽
      const icon = BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_ORANGE);
      options.setIcon(icon);
      
      const marker = this.aMap.addMarker(options);
      if (marker) {
        this.markers.push(marker);
      }
    }
    
    this.markerInfo = `已添加 ${this.markers.length} 个可拖拽标记\n长按标记可拖动`;
    this.dragInfo = '长按标记开始拖拽';
  }

  /**
   * 添加自定义View标记
   */
  private async addCustomViewMarkers(): Promise<void> {
    if (!this.aMap) return;
    
    this.clearMarkers();
    
    for (let i = 0; i < this.places.length; i++) {
      const place = this.places[i];
      const options = new MarkerOptions();
      options.setPosition(new LatLng(place.lat, place.lng));
      options.setTitle(place.name);
      
      // 使用Builder创建自定义View
      const icon = await BitmapDescriptorFactory.fromView(() => {
        this.CustomMarkerView(i + 1, place.name);
      });
      
      if (icon) {
        options.setIcon(icon);
      }
      
      options.setAnchor(0.5, 1.0);
      
      const marker = this.aMap.addMarker(options);
      if (marker) {
        this.markers.push(marker);
      }
    }
    
    this.markerInfo = `已添加 ${this.markers.length} 个自定义View标记`;
  }

  /**
   * 自定义标记View
   */
  @Builder
  CustomMarkerView(index: number, name: string) {
    Column() {
      // 编号气泡
      Column() {
        Text(index.toString())
          .fontSize(14)
          .fontColor(Color.White)
          .fontWeight(FontWeight.Bold)
      }
      .width(28)
      .height(28)
      .borderRadius(14)
      .backgroundColor('#FF5722')
      .justifyContent(FlexAlign.Center)
      .alignItems(HorizontalAlign.Center)
      
      // 名称标签
      Text(name)
        .fontSize(10)
        .fontColor('#333')
        .backgroundColor('rgba(255,255,255,0.9)')
        .padding({ left: 4, right: 4, top: 2, bottom: 2 })
        .borderRadius(4)
        .margin({ top: 2 })
    }
    .alignItems(HorizontalAlign.Center)
  }

  /**
   * 清除所有标记
   */
  private clearMarkers(): void {
    if (!this.aMap) return;
    
    // 方法1:逐个移除
    // for (const marker of this.markers) {
    //   marker.remove();
    // }
    
    // 方法2:清除所有覆盖物
    this.aMap.clear(true);
    this.markers = [];
    this.markerInfo = '已清除所有标记';
    this.dragInfo = '';
  }

  aboutToAppear(): void {
    globalContext = getContext(this).getApplicationContext();
    MapViewManager.getInstance()
      .registerMapViewCreatedCallback(this.mapViewCreateCallback);
  }

  aboutToDisappear(): void {
    MapViewManager.getInstance()
      .unregisterMapViewCreatedCallback(this.mapViewCreateCallback);
    
    if (this.mapView) {
      this.mapView.onDestroy();
      this.mapView = undefined;
      this.aMap = undefined;
    }
  }

  build() {
    Column() {
      // 标题栏
      Row() {
        Text('地图标记 Marker')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.White)
      }
      .width('100%')
      .height(50)
      .padding({ left: 16 })
      .backgroundColor('#FF5722')

      // 地图区域
      Stack() {
        MapViewComponent({ mapViewName: MAP_VIEW_NAME })
          .width('100%')
          .height('100%')
        
        // 信息面板
        Column() {
          Text(this.markerInfo)
            .fontSize(12)
            .fontColor('#333')
          if (this.dragInfo) {
            Text(this.dragInfo)
              .fontSize(11)
              .fontColor('#666')
              .margin({ top: 4 })
          }
        }
        .padding(10)
        .backgroundColor('rgba(255,255,255,0.95)')
        .borderRadius(8)
        .position({ x: 10, y: 10 })
      }
      .width('100%')
      .layoutWeight(1)

      // 操作按钮
      Column() {
        Row() {
          Button('默认标记')
            .fontSize(12)
            .height(36)
            .layoutWeight(1)
            .margin({ right: 8 })
            .onClick(() => this.addDefaultMarkers())
          
          Button('彩色标记')
            .fontSize(12)
            .height(36)
            .layoutWeight(1)
            .margin({ right: 8 })
            .onClick(() => this.addColoredMarkers())
          
          Button('可拖拽')
            .fontSize(12)
            .height(36)
            .layoutWeight(1)
            .onClick(() => this.addDraggableMarkers())
        }
        .width('100%')
        .margin({ bottom: 8 })
        
        Row() {
          Button('自定义View')
            .fontSize(12)
            .height(36)
            .layoutWeight(1)
            .margin({ right: 8 })
            .onClick(() => this.addCustomViewMarkers())
          
          Button('清除全部')
            .fontSize(12)
            .height(36)
            .layoutWeight(1)
            .backgroundColor('#F44336')
            .onClick(() => this.clearMarkers())
        }
        .width('100%')
      }
      .padding(12)
      .backgroundColor('#f5f5f5')
    }
    .width('100%')
    .height('100%')
  }
}

3. BitmapDescriptorFactory 方法

3.1 创建彩色图标(推荐使用异步方法)

// ⚠️ 重要:必须使用 defaultMarkerASync 异步方法创建彩色图标
// 同步方法 defaultMarker() 在鸿蒙SDK中可能无法正确显示颜色

// 需要先获取 context
let globalContext: Context;
// 在 aboutToAppear 中初始化
globalContext = getContext(this).getApplicationContext();

// 使用异步方法创建彩色图标
const icon = await BitmapDescriptorFactory.defaultMarkerASync(globalContext, BitmapDescriptorFactory.HUE_GREEN);

// 可用颜色常量
BitmapDescriptorFactory.HUE_RED       // 红色 (0)
BitmapDescriptorFactory.HUE_ORANGE    // 橙色 (30)
BitmapDescriptorFactory.HUE_YELLOW    // 黄色 (60)
BitmapDescriptorFactory.HUE_GREEN     // 绿色 (120)
BitmapDescriptorFactory.HUE_CYAN      // 青色 (180)
BitmapDescriptorFactory.HUE_AZURE     // 天蓝色 (210)
BitmapDescriptorFactory.HUE_BLUE      // 蓝色 (240)
BitmapDescriptorFactory.HUE_VIOLET    // 紫色 (270)
BitmapDescriptorFactory.HUE_MAGENTA   // 洋红色 (300)
BitmapDescriptorFactory.HUE_ROSE      // 玫红色 (330)

3.2 从rawfile加载图片

const icon = await BitmapDescriptorFactory.fromRawfilePath(
  context, 
  'path/to/image.png'
);

3.3 从View创建

const icon = await BitmapDescriptorFactory.fromView(() => {
  // 在这里构建UI组件
  this.YourCustomBuilder();
});

4. Marker常用操作

4.1 更新标记属性

// 更新位置
marker.setPosition(new LatLng(39.9, 116.4));

// 更新标题
marker.setTitle('新标题');

// 更新图标
marker.setIcon(newIcon);

// 设置可见性
marker.setVisible(false);

// 设置旋转角度
marker.setRotateAngle(45);

4.2 InfoWindow操作

// 显示信息窗口
marker.showInfoWindow();

// 隐藏信息窗口
marker.hideInfoWindow();

// 检查是否显示
const isShowing = marker.isInfoWindowShown();

4.3 获取标记信息

const id = marker.getId();
const position = marker.getPosition();
const title = marker.getTitle();
const snippet = marker.getSnippet();
const isVisible = marker.isVisible();
const isDraggable = marker.isDraggable();

5. 锚点(Anchor)说明

锚点决定了图标哪个位置对应标记的地理坐标。

options.setAnchor(u, v);
// u: 水平方向 (0=左, 0.5=中, 1=右)
// v: 垂直方向 (0=上, 0.5=中, 1=下)

// 常用设置:
options.setAnchor(0.5, 1.0);  // 底部中心(适合气泡样式)
options.setAnchor(0.5, 0.5);  // 正中心(适合圆形图标)

6. 实用技巧

6.1 批量添加标记优化

// 添加多个标记时,建议关闭动画提升性能
for (const item of dataList) {
  const options = new MarkerOptions();
  options.setPosition(new LatLng(item.lat, item.lng));
  // ... 其他设置
  aMap.addMarker(options);
}

6.2 标记聚合

当标记数量较多时,可以考虑根据缩放级别显示/隐藏部分标记:

aMap.setOnCameraChangeListener(new OnCameraChangeListener(
  () => {},
  (position: CameraPosition) => {
    const zoom = position.zoom;
    // 根据缩放级别控制标记显示
    this.markers.forEach((marker, index) => {
      if (zoom < 10) {
        // 缩放较小时只显示部分标记
        marker.setVisible(index % 3 === 0);
      } else {
        marker.setVisible(true);
      }
    });
  }
));

本篇小结

本篇教程我们学习了:

  • ✅ 添加默认样式和彩色标记
  • ✅ 使用自定义图标
  • ✅ 创建自定义View标记
  • ✅ 标记点击和拖拽事件处理
  • ✅ InfoWindow的显示与隐藏
  • ✅ 锚点设置与标记属性更新

下一篇我们将学习定位蓝点功能。 班级 developer.huawei.com/consumer/cn…

源码地址 gitcode.com/daleishen/g…