点标记(Marker)是地图上最常用的覆盖物,用于在指定经纬度位置标注一个图标,支持自定义图标、旋转、拖拽、信息窗(InfoWindow)等。
本文档介绍 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},
...
)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
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'),
...
)
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 值大的 Marker 优先显示,zIndex 小的被碰撞隐藏。Marker 通过 Set<Marker> 以声明式方式管理。插件在 Widget 重建时自动 diff 新旧两个 Set,计算出增/删/改三类变更并推送到原生层:
id 索引:内部用 Map<String, Marker> 索引,不依赖 Set 的 hashCode==:Marker 重写了 ==,比较 position/icon/rotation/visible 等全部业务属性(⚠️ 不比较 onTap/onDrag* 回调)@immutable,修改属性必须用 copyWith 创建新对象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 内修改变量 |
有帮助
没帮助