点标记 Marker

点标记(Marker)是地图上最常用的覆盖物,用于在指定经纬度位置标注一个图标,支持自定义图标、旋转、拖拽、信息窗(InfoWindow)等。

本文档介绍 Marker 的创建、属性配置、交互事件以及声明式更新机制。



创建 Marker

Marker(
  id: 'm1',
  position: const LatLng(39.9087, 116.3975),
  icon: BitmapDescriptor.defaultMarker,
  infoWindow: const InfoWindow(
    title: '天安门',
    snippet: '北京市东城区',
  ),
  onTap: () {
    print('点击了 Marker');
  },
)

将 Marker 添加到地图:

TencentMap(
  markers: {marker},
  ...
)

Marker 参数

参数 类型 默认值 说明
id String? 自动生成 Marker 唯一标识
position LatLng 必填 Marker 位置
icon BitmapDescriptor defaultMarker Marker 图标
anchor Offset (0.5, 1.0) 锚点比例(0.0~1.0)
alpha double 1.0 透明度(0.0~1.0)
rotation double 0.0 旋转角度(0~360)
clockwise bool true 旋转方向(true=顺时针)
draggable bool false 是否可拖拽
visible bool true 是否可见
clickable bool true 是否可点击
zIndex double 0.0 层级(值越大越在上层)
level OverlayLevel aboveLabels 显示层级
scale Offset? null 缩放比例(sx, sy)
infoWindow InfoWindow noText 信息窗配置
infoWindowEnable bool true 是否启用信息窗
fixedToScreen bool false 是否固定在屏幕位置
fixedScreenPoint Offset? null 固定屏幕坐标
collisionTypes Set<MarkerCollisionType> {} 碰撞类型集合
collisionRelation MarkerCollisionRelation alone 碰撞关系
onTap VoidCallback? null 点击回调(无参数)
onDragStart ValueChanged<LatLng>? null 开始拖拽回调
onDrag ValueChanged<LatLng>? null 拖拽中回调
onDragEnd ValueChanged<LatLng>? null 拖拽结束回调
onCollisionStatusChanged void Function(String, bool)? null 碰撞状态变化回调

图标

Marker 支持三种图标来源:

// 1. 默认图标(红色水滴)
icon: BitmapDescriptor.defaultMarker

// 2. 从 Asset 加载(原始尺寸)
icon: BitmapDescriptor.fromAsset('assets/marker.png')

// 3. 从 Asset 加载(指定逻辑像素尺寸)
icon: BitmapDescriptor.fromAsset('assets/marker.png', width: 24, height: 32)

// 4. 从 Asset 加载(按像素密度适配)
icon: BitmapDescriptor.fromAsset(
  'assets/marker.png',
  imagePixelRatio: MediaQuery.of(context).devicePixelRatio,
)

// 5. 从字节数据加载(如 Widget 转图片)
icon: BitmapDescriptor.fromBytes(uint8List)

详细的 BitmapDescriptor 用法请参考自定义纹理与 BitmapDescriptor


锚点

锚点定义图标中哪个位置对齐到经纬度坐标:

Marker(
  position: const LatLng(39.9, 116.4),
  anchor: const Offset(0.5, 1.0), // 底部中心(默认)
  // anchor: const Offset(0.5, 0.5), // 图标中心
  // anchor: const Offset(0.0, 0.0), // 左上角
  ...
)

旋转与透明度

Marker(
  rotation: 45,    // 顺时针旋转 45 度
  clockwise: true, // 顺时针方向(false 为逆时针)
  alpha: 0.7,      // 70% 不透明
  ...
)

拖拽

Marker(
  draggable: true,
  onDragStart: (latLng) => print('开始拖拽: $latLng'),
  onDrag: (latLng) => print('拖拽中: $latLng'),
  onDragEnd: (latLng) => print('拖拽结束: $latLng'),
  ...
)

InfoWindow 信息窗

Marker(
  infoWindow: const InfoWindow(
    title: '标题',
    snippet: '副标题',
    onTap: null, // 点击信息窗的回调
  ),
  infoWindowEnable: true, // 默认 true
  ...
)

通过 Controller 控制信息窗显示/隐藏:

// 显示信息窗
await controller.showMarkerInfoWindow('m1');

// 隐藏信息窗
await controller.hideMarkerInfoWindow('m1');

注意:同一时间只能显示一个信息窗。点击新的 Marker 会自动隐藏前一个。


显示层级

Marker(
  level: OverlayLevel.aboveLabels,  // 在 POI 标注之上(默认)
  // level: OverlayLevel.aboveRoads,   // 在道路之上(最低层)
  zIndex: 10, // 同层级内的排序
  ...
)

固定屏幕位置

将 Marker 固定在屏幕坐标上,不随地图移动:

Marker(
  fixedToScreen: true,
  fixedScreenPoint: const Offset(100, 200), // 屏幕坐标(逻辑像素)
  ...
)

碰撞控制

Marker 碰撞控制用于管理多个 Marker 之间、以及 Marker 与 POI 之间的避让关系,避免图标重叠。

碰撞类型

枚举值 说明
MarkerCollisionType.poi 与地图 POI 标注碰撞
MarkerCollisionType.marker 与其他 Marker 碰撞

一个 Marker 可以同时设置多种碰撞类型:

Marker(
  collisionTypes: {
    MarkerCollisionType.poi,
    MarkerCollisionType.marker,
  },
  ...
)

碰撞关系

枚举值 说明
MarkerCollisionRelation.alone 独立碰撞(默认),Marker 各自参与碰撞
MarkerCollisionRelation.together 同组联动,一组 Marker 中任一被碰撞则整组隐藏
Marker(
  collisionRelation: MarkerCollisionRelation.together,
  ...
)

配置示例

// 两个 Marker 设置为同组碰撞
final markers = {
  Marker(
    id: 'm1',
    position: const LatLng(39.9087, 116.3975),
    collisionTypes: {MarkerCollisionType.marker},
    collisionRelation: MarkerCollisionRelation.together,
  ),
  Marker(
    id: 'm2',
    position: const LatLng(39.9088, 116.3976), // 与 m1 距离很近
    collisionTypes: {MarkerCollisionType.marker},
    collisionRelation: MarkerCollisionRelation.together,
  ),
};

当 m1 和 m2 位置重叠时,它们会作为一个整体参与碰撞——要么都显示,要么都隐藏。

碰撞状态回调

当 Marker 因碰撞而被隐藏或重新显示时,会触发回调:

Marker(
  id: 'm1',
  position: const LatLng(39.9087, 116.3975),
  collisionTypes: {MarkerCollisionType.marker},
  onCollisionStatusChanged: (markerId, isHidden) {
    if (isHidden) {
      print('Marker $markerId 被碰撞隐藏');
    } else {
      print('Marker $markerId 重新显示');
    }
  },
  ...
)
参数 类型 说明
markerId String 被碰撞的 Marker ID
isHidden bool true 表示被碰撞隐藏,false 表示重新显示

注意事项

  • zIndex 影响zIndex 值大的 Marker 优先显示,zIndex 小的被碰撞隐藏。

声明式更新

基本原理

Marker 通过 Set<Marker> 以声明式方式管理。插件在 Widget 重建时自动 diff 新旧两个 Set,计算出增/删/改三类变更并推送到原生层:

  • diff 按 id 索引:内部用 Map<String, Marker> 索引,不依赖 Set 的 hashCode
  • 属性比较用 ==:Marker 重写了 ==,比较 position/icon/rotation/visible 等全部业务属性(⚠️ 不比较 onTap/onDrag* 回调)
  • 对象不可变:Marker 是 @immutable,修改属性必须用 copyWith 创建新对象

推荐用法:Map 管理 + Set 传入

final Map<String, Marker> _markers = {};

// 添加
_markers['m1'] = Marker(
  id: 'm1',
  position: const LatLng(39.9087, 116.3975),
);

// 更新(必须 copyWith,不能原地改字段)
_markers['m1'] = _markers['m1']!.copyWith(rotation: 90);

// 删除
_markers.remove('m1');

// 传入 Widget(每次 setState 都转一次 Set)
TencentMap(markers: Set<Marker>.of(_markers.values));

也支持直接用 Set 字面量:

Set<Marker> _markers = {};

// 添加
setState(() {
  _markers = {..._markers, newMarker};
});

// 更新(遍历替换为新对象)
setState(() {
  _markers = {
    for (final m in _markers)
      if (m.id == 'm1') m.copyWith(rotation: 90) else m,
  };
});

// 删除
setState(() {
  _markers = _markers.where((m) => m.id != 'm1').toSet();
});

哪些情况会导致更新不触发

错误写法 原因 正确写法
直接改 List 字段:marker.someList.add(x) Marker 是 @immutable,字段为 final,编译期就阻止;但若持有外部 List 引用并原地改,对象引用不变 → == 短路 identical → 判定无变化 marker.copyWith(someList: [...marker.someList, x])
在同一 Set 上 add copyWith 后的对象:_markers.add(m.copyWith(rotation: 90)) hashCode 仅基于 id(相同),但 == 为 false(属性不同)→ Set 同时保留新旧两个对象 → diff 按 id 索引只取到最后一个,行为不可预测 创建全新 Set:_markers = {..._markers.where((m) => m.id != 'm1'), m.copyWith(rotation: 90)};或用 Map 管理
两个 Marker 用同一个 id diff 按 id 索引成 Map 时后者覆盖前者 → 变更丢失 确保每个 Marker 有唯一 id(不传 id 时自动生成)
只改了 onTap/onDragStart/onDrag/onDragEnd 回调 Marker 的 == 不比较回调(回调无法按值比较)→ 判定无变化 → 不触发更新 同时改一个视觉属性(如 alpha 微调),或通过 controller 方法控制行为
修改了 State 变量但没调 setState Widget 不重建 → didUpdateWidget 不触发 → diff 不执行 必须在 setState 内修改变量
本页内容