amap_map2 1.0.4
amap_map2: ^1.0.4 copied to clipboard
Amap SDK Flutter plugin for integrating AMapSDK in iOS and Android applications.
amap_map2 #
基于高德开放平台地图 SDK的 Flutter 地图插件,支持 Android 和 iOS。
| Android | iOS | |
|---|---|---|
| AMapSDK | 11.2.000_loc11.2.000_sea9.8.0 | Map 11.2.000 / Location 2.12.0 / Search 9.8.0 |
| Support | minSdk 21+ | 12.0+ |
功能 #
- 显示高德 3D 地图
- 地图类型、路况、建筑物、文字标注、语言、Logo、手势等配置
- 地图点击、长按、POI 点击、相机移动、定位回调
- Marker、Polyline、Polygon 覆盖物
- Marker 点击、拖拽、自定义图标、自定义 InfoWindow
- Polyline 点击、纹理、虚线、线头、连接点样式
- Polygon 点击、边框、填充、显隐、点集更新
- 运行时通过
AMapController动态增删改覆盖物 - 经纬度和屏幕坐标互转、截图、清理缓存
- 独立单次/连续定位、正向和逆向地理编码
安装 #
flutter pub add amap_map2
需要同时引入 x_amap_base 中的基础类型:
import 'package:amap_map2/amap_map2.dart';
import 'package:x_amap_base/x_amap_base.dart';
准备工作 #
登录高德开放平台申请 Key:
高德 SDK 需要先完成隐私合规授权。建议在展示 AMapWidget 前调用:
class ConstConfig {
static const AMapApiKey amapApiKeys = AMapApiKey(
androidKey: '你的 Android Key',
iosKey: '你的 iOS Key',
);
static const AMapPrivacyStatement amapPrivacyStatement =
AMapPrivacyStatement(
hasContains: true,
hasShow: true,
hasAgree: true,
);
}
@override
Widget build(BuildContext context) {
AMapInitializer.init(context, apiKey: ConstConfig.amapApiKeys);
AMapInitializer.updatePrivacyAgree(ConstConfig.amapPrivacyStatement);
return const MaterialApp(home: MapPage());
}
高德 SDK 合规方案请参考:高德开放平台合规使用说明。
定位与地理编码 #
定位和地理编码都可以脱离 AMapWidget 独立使用。调用前仍需执行上面的
AMapInitializer.init 和 AMapInitializer.updatePrivacyAgree。隐私参数中的三个字段必须全部为
true;用户撤回同意后,插件会停止正在运行的定位,并取消尚未完成的定位和地理编码请求。
定位由宿主应用申请系统权限,插件不会主动弹出权限请求。正向和逆向地理编码不需要系统定位权限。
iOS 宿主应用还需在 Info.plist 中配置 NSLocationWhenInUseUsageDescription。Android 所需权限和
APSService 已由插件清单合并,应用只需在运行时申请定位权限。
单次定位:
final AMapLocation location =
await AMapLocationClient.instance.getCurrentLocation(
options: const AMapLocationOptions(
accuracy: AMapLocationAccuracy.high,
timeout: Duration(seconds: 10),
),
);
连续定位:
final StreamSubscription<AMapLocation> subscription =
AMapLocationClient.instance.locations.listen((AMapLocation location) {
debugPrint('location: ${location.latLng}');
});
await AMapLocationClient.instance.startLocation(
options: const AMapLocationOptions(interval: Duration(seconds: 2)),
);
// 页面销毁时:
await AMapLocationClient.instance.stopLocation();
await subscription.cancel();
正向地理编码:
final List<AMapGeocodeResult> results =
await AMapGeocodingClient.instance.geocode(
address: '天安门广场',
city: '北京',
);
final LatLng coordinate = results.first.location;
地址可能匹配多个结果,geocode 按 SDK 顺序返回 List<AMapGeocodeResult>;没有匹配结果时返回空列表。
结果包含 location、formattedAddress、省市区、乡镇、社区、建筑、行政区划代码和匹配等级等信息。
用户选点后查询地点名:
Future<void> selectPoint(LatLng selectedLatLng) async {
final AMapReverseGeocodeResult result =
await AMapGeocodingClient.instance.reverseGeocode(
location: selectedLatLng,
radius: 1000,
);
debugPrint('地点:${result.displayName}');
debugPrint('地址:${result.formattedAddress}');
}
AMapWidget(
initialCameraPosition: const CameraPosition(
target: LatLng(39.909187, 116.397451),
zoom: 15,
),
onTap: selectPoint,
);
radius 为查询半径,单位是米,允许范围为 0 到 3000,默认值为 1000。
AMapReverseGeocodeResult 提供以下常用数据:
| 字段 | 含义 |
|---|---|
displayName |
推荐直接显示的地点名,依次从最近 POI、建筑、社区、街道和完整地址中选择 |
placeName |
查询点附近距离最近的 POI 名称,可能为空 |
formattedAddress |
SDK 返回的完整格式化地址 |
street / number |
街道和门牌号 |
province / city / district |
省、市、区县 |
adCode / cityCode / townCode |
行政区划、城市和乡镇编码 |
定位和地理编码接口只暴露高德坐标语义,面向中国境内 GCJ-02 场景,不提供坐标系切换。传给地图、定位和
reverseGeocode 的坐标应保持同一坐标系。
参数不合法时接口抛出 ArgumentError。原生服务失败时抛出 PlatformException,常见错误码包括:
| 错误码 | 含义 |
|---|---|
privacy_not_agreed |
尚未完成隐私合规授权,或用户已经撤回授权 |
permission_denied |
宿主应用尚未取得系统定位权限 |
location_service_disabled |
系统定位服务未开启 |
location_busy / location_timeout / location_failed |
单次定位冲突、超时或 SDK 定位失败 |
geocode_failed / reverse_geocode_failed |
正向或逆向地理编码请求失败 |
基础地图 #
class MapPage extends StatefulWidget {
const MapPage({super.key});
@override
State<MapPage> createState() => _MapPageState();
}
class _MapPageState extends State<MapPage> {
AMapController? _controller;
static const CameraPosition _initialPosition = CameraPosition(
target: LatLng(39.909187, 116.397451),
zoom: 12,
);
@override
Widget build(BuildContext context) {
return Scaffold(
body: AMapWidget(
initialCameraPosition: _initialPosition,
trafficEnabled: false,
buildingsEnabled: true,
labelsEnabled: true,
onMapCreated: (AMapController controller) {
_controller = controller;
},
onTap: (LatLng position) {
debugPrint('map tapped: $position');
},
onLongPress: (LatLng position) {
debugPrint('map long pressed: $position');
},
),
);
}
}
地图配置 #
常用配置可以直接传给 AMapWidget:
AMapWidget(
initialCameraPosition: const CameraPosition(
target: LatLng(39.909187, 116.397451),
zoom: 12,
),
mapType: MapType.normal,
trafficEnabled: false,
buildingsEnabled: true,
labelsEnabled: true,
compassEnabled: true,
scaleEnabled: true,
touchPoiEnabled: true,
zoomGesturesEnabled: true,
scrollGesturesEnabled: true,
rotateGesturesEnabled: true,
tiltGesturesEnabled: true,
mapLanguage: MapLanguage.chinese,
)
支持的地图类型:
MapType.normalMapType.satelliteMapType.nightMapType.naviMapType.bus
自定义地图样式 #
从高德开放平台下载自定义地图样式文件后,可以传入 style.data 和 style_extra.data:
final ByteData styleData = await rootBundle.load('assets/style.data');
final ByteData styleExtraData = await rootBundle.load('assets/style_extra.data');
final CustomStyleOptions customStyleOptions = CustomStyleOptions(
true,
styleData: styleData.buffer.asUint8List(),
styleExtraData: styleExtraData.buffer.asUint8List(),
);
AMapWidget(
customStyleOptions: customStyleOptions,
)
Marker #
final Marker marker = Marker(
position: const LatLng(39.909187, 116.397451),
draggable: true,
draggingEventFrequency: 20,
infoWindowEnable: true,
infoWindow: const InfoWindow(
title: '天安门',
snippet: '北京市东城区',
),
onTap: (String markerId) {
debugPrint('marker tapped: $markerId');
},
onDragStart: (String markerId, LatLng position) {
debugPrint('marker drag start: $markerId, $position');
},
onDrag: (String markerId, LatLng position) {
debugPrint('marker dragging: $markerId, $position');
},
onDragEnd: (String markerId, LatLng position) {
debugPrint('marker drag end: $markerId, $position');
},
);
AMapWidget(
markers: <Marker>{marker},
)
Marker 支持:
positioniconalphaanchorclickabledraggabledraggingEventFrequency,iOS 拖拽中事件采样频率,默认30 FPS,取值范围1~120infoWindowinfoWindowEnable,默认为falserotationvisiblezIndexonTaponDragStartonDragonDragEnd
声明式 JSON Marker 图标 #
BitmapDescriptor.fromJsonIcon 支持用一棵声明式、强类型的 Dart 节点树描述 Marker 图标。Flutter/Dart 将平台无关的图标结构传给原生侧:
- Android 渲染为原生
View,再通过BitmapDescriptorFactory.fromView(view)生成 Marker 图标。 - iOS 用同一份 JSON schema 渲染为
UIImage。
相关文件:
lib/src/types/amap_marker_icon_json.dart:Dart 侧强类型构建 API。android/src/main/java/com/amap/flutter/map/overlays/marker/MarkerIconJsonRenderer.java:Android 递归渲染row、column、stack、text、image、space。android/src/main/java/com/amap/flutter/map/overlays/marker/MarkerIconDescriptorFactory.java:把原生 View 转为BitmapDescriptor。ios/Classes/OverlayController/MarkerIcon/AMapMarkerIconJsonRenderer.m:iOS 递归渲染同一份 schema 到UIImage。
BitmapDescriptor.fromJsonIcon 使用普通 Marker 的 icon 字段。Android 在 ConvertUtil 中处理 descriptor,iOS 在 AMapConvertUtil 中处理 descriptor。
Dart 用法
final MarkerRenderIcon iconJson = MarkerRender.icon(
view: MarkerRender.container(
style: const MarkerRenderContainerStyle(
alignment: MarkerRenderAlignment.center,
),
child: MarkerRender.column(
style: const MarkerRenderColumnStyle(
alignment: MarkerRenderAlignment.center,
),
children: <MarkerRenderNode>[
MarkerRender.text(
'12',
style: const MarkerRenderTextStyle(
textColor: Color(0xFFFFFFFF),
textSize: 12,
textStyle: MarkerRenderTextStyleValue.bold,
backgroundColor: Color(0xFFE53935),
radius: 10,
padding: MarkerRenderInsets.fromLTRB(8, 2, 8, 2),
alignment: MarkerRenderAlignment.center,
includeFontPadding: false,
),
),
MarkerRender.space(height: 2),
MarkerRender.image(
const MarkerRenderAssetSource('assets/marker_icon.png'),
style: const MarkerRenderImageStyle(
width: 48,
height: 48,
),
),
],
),
),
);
final Marker marker = Marker(
position: const LatLng(31.2304, 121.4737),
anchor: const Offset(0.5, 1.0),
icon: BitmapDescriptor.fromJsonIcon(iconJson),
);
当标签变化时,重新构建图标 JSON 并更新 Marker:
final MarkerRenderIcon updatedIconJson = MarkerRender.icon(
view: MarkerRender.container(
style: const MarkerRenderContainerStyle(
alignment: MarkerRenderAlignment.center,
),
child: MarkerRender.column(
children: <MarkerRenderNode>[
MarkerRender.text(
'18',
style: const MarkerRenderTextStyle(
textColor: Color(0xFFFFFFFF),
textSize: 12,
textStyle: MarkerRenderTextStyleValue.bold,
backgroundColor: Color(0xFFE53935),
radius: 10,
padding: MarkerRenderInsets.fromLTRB(8, 2, 8, 2),
alignment: MarkerRenderAlignment.center,
includeFontPadding: false,
),
),
MarkerRender.space(height: 2),
MarkerRender.image(
const MarkerRenderAssetSource('assets/marker_icon.png'),
style: const MarkerRenderImageStyle(
width: 48,
height: 48,
),
),
],
),
),
);
final Marker updatedMarker = marker.copyWith(
icon: BitmapDescriptor.fromJsonIcon(updatedIconJson),
);
JSON 结构
{
"view": {
"type": "container",
"style": {
"alignment": "center"
},
"child": {
"type": "column",
"children": [
{
"type": "text",
"text": "12",
"style": {
"textColor": "#FFFFFFFF",
"textSize": 12,
"backgroundColor": "#FFE53935",
"radius": 10,
"padding": [8, 2, 8, 2],
"alignment": "center"
}
},
{
"type": "image",
"src": ["fromAsset", "assets/marker_icon.png"],
"style": {
"width": 48,
"height": 48
}
}
]
}
}
}
支持的节点类型
container:AndroidFrameLayout;适合作为固定尺寸或 wrap-content 的根包装节点。row:AndroidLinearLayouthorizontal。column:AndroidLinearLayoutvertical。stack:AndroidFrameLayout。text:AndroidTextView。image:AndroidImageView。space:AndroidSpace。
根 view 只支持布局类型:container、row、column、stack。叶子节点 text、image、space 只能作为子节点。
container 只支持单个 child 字段,会忽略 children。row、column、stack 使用 children。
后续增加新节点类型时,需要在 MarkerIconJsonRenderer.createView 中新增一个 case,并从节点根字段或 props 中读取新增属性。
样式字段
所有显式尺寸都是无单位数字。原生渲染器会把数值转换为平台显示单位。
container、text、space 可以省略 width 或 height,从内容自动计算尺寸。stack 必须提供固定 width 和 height。图片必须提供固定 width 和 height。
渲染后的 ARGB bitmap 在平台缩放后限制为 2 MiB,超过限制会使用空白 fallback 图标。
颜色只支持完整十六进制字符串:
#RRGGBB#AARRGGBB
不支持 red、transparent 这类颜色名。
支持的 style 类型:
MarkerRenderContainerStyle:width、height、padding、margin、alignment、backgroundColor、radius、borderColor、borderWidthMarkerRenderRowStyle:margin、alignmentMarkerRenderColumnStyle:margin、alignmentMarkerRenderStackStyle:必填width、必填height、margin、alignmentMarkerRenderTextStyle:width、height、padding、margin、alignment、backgroundColor、radius,以及文本样式字段MarkerRenderImageStyle:固定width、固定height、margin
alignment 是 Flutter 侧的高级对齐字段。Android 映射为原生 gravity,iOS 在手动 UIKit 布局阶段映射。
对于 container、row、column、stack,alignment 属于父节点,作用于直接子节点。row 和 column 保持主轴顺序,alignment 控制交叉轴。子节点的 margin 会参与对齐计算。
支持的对齐值:
centertopLefttopRightbottomLeftbottomRighttopCenterbottomCentercenterLeftcenterRight
原生样式应用是节点专属的:
container:背景、边框、圆角、paddingstack:背景、边框、圆角;忽略 paddingtext:背景、边框、圆角、paddingrow/column:子节点对齐image/space:尺寸和 margin
文本样式字段
textColortextSize:Android 为 sp,iOS 为 point sizetextStyle:bold、italic、boldItalicfontWeight:bold或"700"这类数值字符串maxLinesminLinessingleLineellipsize:start、middle、endincludeFontPadding:仅 AndroidTextView生效;false会移除 Android 额外字体上下 padding,UIKit 无等价设置
图片源格式
["fromAsset", "assets/marker_icon.png"]
["fromAsset", "assets/marker_icon.png", "package_name"]
图片以 contain 方式渲染到固定尺寸图片盒中,不会从 row 或 column 继承宽高。
原生架构
两端都消费 MarkerRender 生成的同一份 JSON:
- Android:
ConvertUtil校验["fromJsonIcon", payload],MarkerIconJsonRenderer构建原生View,MarkerIconDescriptorFactory通过BitmapDescriptorFactory.fromView截图。 - iOS:
AMapConvertUtil校验["fromJsonIcon", payload],AMapMarkerIconJsonRenderer手动布局UIView、UILabel、UIImageView和空UIView,最终渲染成UIImage。
这样 Flutter 负责平台无关 schema,Android 和 iOS 保持原生渲染。
注意事项
- 新 Dart 代码建议使用强类型
MarkerRenderAPI,不建议手写 raw map。 BitmapDescriptor.fromJsonIcon接收MarkerRenderIcon。- Android 的
BitmapDescriptorFactory.fromView(view)是快照机制。标签变化时,需要重新构建 icon JSON 并更新 Marker。
Polyline #
final Polyline polyline = Polyline(
points: const <LatLng>[
LatLng(39.938698, 116.275177),
LatLng(39.966069, 116.289253),
LatLng(39.944226, 116.306076),
],
width: 8,
color: const Color(0xFF1677FF),
onTap: (String polylineId) {
debugPrint('polyline tapped: $polylineId');
},
);
AMapWidget(
polylines: <Polyline>{polyline},
)
如果线太细导致难点击,可以额外叠加一条更宽、几乎透明的 Polyline 作为点击热区。
Polygon #
final Polygon polygon = Polygon(
points: const <LatLng>[
LatLng(39.835334, 116.3710069),
LatLng(39.843082, 116.3709830),
LatLng(39.845932, 116.3642213),
LatLng(39.841562, 116.3455680),
],
strokeWidth: 4,
strokeColor: const Color(0xFF1677FF),
fillColor: const Color(0x331677FF),
onTap: (String polygonId) {
debugPrint('polygon tapped: $polygonId');
},
);
AMapWidget(
polygons: <Polygon>{polygon},
)
Polygon.onTap 在 Flutter 层实现:地图点击后使用 turf.booleanPointInPolygon 判断点击点是否落在 Polygon 内。高德国内地图默认使用 GCJ-02,只要点击点和 Polygon 点集使用同一坐标系即可。多个 Polygon 重叠时,后添加的 Polygon 优先响应。
动态更新覆盖物 #
通过 AMapController 可以在地图创建后动态增删改覆盖物:
AMapController? controller;
AMapWidget(
onMapCreated: (AMapController value) {
controller = value;
},
)
await controller?.setMarker(marker);
await controller?.removeMarker(marker.id);
await controller?.setPolyline(polyline);
await controller?.removePolyline(polyline.id);
await controller?.setPolygon(polygon);
await controller?.removePolygon(polygon.id);
控制地图 #
区域截图 #
可按左上、右上坐标截取指定区域,输出为指定物理像素尺寸的 PNG,且不会改变当前地图视角:
final Uint8List pngBytes = await controller.takeRegionSnapshot(
topLeft: const LatLng(39.95, 116.30),
topRight: const LatLng(39.95, 116.50),
width: 1200,
height: 800,
timeout: const Duration(seconds: 30),
);
topLeft 与 topRight 必须位于同一纬度,且左侧经度小于右侧经度;暂不支持跨日期变更线。同一地图实例同时只执行一个区域截图任务,实际可用尺寸取决于原生地图 SDK 和设备内存。
await controller?.moveCamera(
CameraUpdate.newLatLngZoom(
const LatLng(39.909187, 116.397451),
15,
),
);
final Uint8List? image = await controller?.takeSnapshot();
final ScreenCoordinate screen =
await controller!.toScreenCoordinate(const LatLng(39.909187, 116.397451));
final LatLng position = await controller!.fromScreenCoordinate(screen);
常用控制器 API:
moveCameratakeSnapshotclearDisktoScreenCoordinatefromScreenCoordinatesetMapOptionssetMarker/removeMarkersetPolyline/removePolylinesetPolygon/removePolygongetMapContentApprovalNumbergetSatelliteImageApprovalNumber
自定义 InfoWindow #
可以通过 infoWindowAdapter 自定义 Marker 的气泡 Widget:
AMapWidget(
markers: markers,
infoWindowAdapter: InfoWindowAdapter(
getInfoWindow: (BuildContext context, Marker marker) {
return Positioned(
left: 16,
top: 16,
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(6),
),
child: Padding(
padding: const EdgeInsets.all(8),
child: Text(marker.infoWindow.title ?? ''),
),
),
);
},
),
)
坐标系说明 #
高德国内地图使用 GCJ-02 坐标系。地图点击回调、Marker、Polyline、Polygon 的坐标应保持同一坐标系。不要混用 WGS84、GCJ-02、BD-09,否则覆盖物位置和点击判断会出现偏移。
常见问题 #
Android targetSdkVersion >= 30 返回地图页闪退 #
可以在 Android AndroidManifest.xml 的 application 中增加:
<application android:allowNativeHeapPointerTagging="false">
...
</application>
模拟器 OpenGL 崩溃 #
如果模拟器运行遇到类似:
com.amap.api.col.3sl.dl$b.createContext(GlesUtility.java:73)
可尝试将模拟器图像加速模式切换为 Software。
示例 #
完整示例请查看 example 目录。