csp_amap_flutter_map
基于高德开放平台地图SDK的 Flutter 插件,支持 Android、iOS、HarmonyOS 三大平台。
✨ 特性
- 🌍 三平台支持: Android、iOS、HarmonyOS 全平台覆盖
- 🗺️ 丰富的地图功能: 多种地图类型、标记、路径绘制、定位等
- 🎨 自定义样式: 支持自定义地图样式和标记图标
- 📱 响应式设计: 支持各种屏幕尺寸和分辨率
- 🔧 易于集成: 简单的 API 设计,快速集成到项目中
- 📋 合规处理: 内置隐私合规处理方案
📋 目录
🚀 准备工作
1. 申请 API Key
登录高德开放平台官网申请 API Key:
- Android: 获取 Android Key
- iOS: 获取 iOS Key
- HarmonyOS: 获取 HarmonyOS Key
2. 引入高德地图 SDK
- Android: Android Studio 配置工程
- iOS: iOS 安装地图 SDK
- HarmonyOS: HarmonyOS 配置
3. 平台特殊配置
iOS 配置
在 iOS 工程的 info.plist
中添加以下配置(Flutter 1.22.0 之前版本需要):
<key>io.flutter.embedded_views_preview</key>
<string>YES</string>
HarmonyOS 配置
- 确保项目使用 HarmonyOS Next 版本
- 在
ohos/oh-package.json5
中配置高德地图 SDK 依赖 - 在
ohos/src/main/module.json5
中配置所需权限和 API Key - 获得定位权限后使用
📝 注意: 详细的 HarmonyOS 配置请参考 HARMONYOS_SETUP.md
📦 安装使用
pub.flutter-io.cn 版本(Android + iOS)
dependencies:
csp_amap_flutter_map: ^1.0.0
完整版本(Android + iOS + HarmonyOS)
如需要 HarmonyOS 支持,请使用 Gitee 仓库版本:
dependencies:
csp_amap_flutter_map:
git:
url: https://gitee.com/chenshipeng0914/csp_amap_flutter_map.git
ref: develop
📝 说明: pub.flutter-io.cn 版本为了兼容性考虑,仅包含 Android 和 iOS 平台支持。完整的 HarmonyOS 支持请使用 Gitee 仓库版本。
1. 安装依赖
# 使用 FVM(推荐)
fvm flutter pub get
# 或者直接使用 Flutter
flutter pub get
📝 推荐使用 FVM: 本项目使用 FVM 管理 Flutter 版本,支持 HarmonyOS 的定制 Flutter 版本 3.22.1-ohos-0.1.1
2. 导入包
import 'package:csp_amap_flutter_map/csp_amap_flutter_map.dart';
import 'package:csp_amap_flutter_base/csp_amap_flutter_base.dart';
🚀 快速开始
1. 初始化 SDK
// 在 main.dart 中初始化
AMapInitializer.init(
context,
apiKey: AMapApiKey(
androidKey: 'your_android_key',
iosKey: 'your_ios_key',
ohosKey: 'your_harmonyos_key', // HarmonyOS 支持
),
);
2. 合规处理
根据高德 SDK 合规使用方案,需要进行授权交互:
// 在用户同意隐私协议后调用
AMapInitializer.updatePrivacyAgree(
AMapPrivacyStatement(
hasContains: true, // 是否包含高德隐私政策
hasShow: true, // 是否已展示隐私政策
hasAgree: true, // 是否同意隐私政策
),
);
3. 基本使用
import 'package:flutter/material.dart';
import 'package:csp_amap_flutter_map/csp_amap_flutter_map.dart';
import 'package:csp_amap_flutter_base/csp_amap_flutter_base.dart';
class BasicMapPage extends StatefulWidget {
@override
_BasicMapPageState createState() => _BasicMapPageState();
}
class _BasicMapPageState extends State<BasicMapPage> {
static const CameraPosition _initialPosition = CameraPosition(
target: LatLng(39.909187, 116.397451), // 北京天安门
zoom: 10.0,
);
AMapController? _mapController;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('高德地图示例')),
body: AMapWidget(
initialCameraPosition: _initialPosition,
onMapCreated: (AMapController controller) {
_mapController = controller;
},
onTap: (LatLng latLng) {
print('点击位置: $latLng');
},
),
);
}
}
📋 API 文档
📱 示例代码
基础地图显示
import 'package:amap_flutter_map_example/base_page.dart';
import 'package:flutter/material.dart';
import 'package:amap_flutter_map/amap_flutter_map.dart';
import 'package:st_amap_flutter_base/st_amap_flutter_base.dart';
class ShowMapPage extends BasePage {
ShowMapPage(String title, String subTitle) : super(title, subTitle);
@override
Widget build(BuildContext context) {
return _ShowMapPageBody();
}
}
class _ShowMapPageBody extends StatefulWidget {
@override
State<StatefulWidget> createState() => _ShowMapPageState();
}
class _ShowMapPageState extends State<_ShowMapPageBody> {
static final CameraPosition _kInitialPosition = const CameraPosition(
target: LatLng(39.909187, 116.397451),
zoom: 10.0,
);
List<Widget> _approvalNumberWidget = List<Widget>();
@override
Widget build(BuildContext context) {
final AMapWidget map = AMapWidget(
initialCameraPosition: _kInitialPosition,
onMapCreated: onMapCreated,
);
return ConstrainedBox(
constraints: BoxConstraints.expand(),
child: Stack(
alignment: Alignment.center,
children: [
Container(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: map,
),
Positioned(
right: 10,
bottom: 15,
child: Container(
alignment: Alignment.centerLeft,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: _approvalNumberWidget),
))
],
),
);
}
AMapController _mapController;
void onMapCreated(AMapController controller) {
setState(() {
_mapController = controller;
getApprovalNumber();
});
}
/// 获取审图号
void getApprovalNumber() async {
//普通地图审图号
String mapContentApprovalNumber =
await _mapController?.getMapContentApprovalNumber();
//卫星地图审图号
String satelliteImageApprovalNumber =
await _mapController?.getSatelliteImageApprovalNumber();
setState(() {
if (null != mapContentApprovalNumber) {
_approvalNumberWidget.add(Text(mapContentApprovalNumber));
}
if (null != satelliteImageApprovalNumber) {
_approvalNumberWidget.add(Text(satelliteImageApprovalNumber));
}
});
print('地图审图号(普通地图): $mapContentApprovalNumber');
print('地图审图号(卫星地图): $satelliteImageApprovalNumber');
}
}
添加自定义Widget的Marker
// 添加一个预览图片的方法
void _addCustomMarkerImage() async {
// 创建一个 GlobalKey 来获取 Widget 的渲染对象
final GlobalKey repaintKey = GlobalKey();
// 创建一个自定义的 Widget
final Widget customWidget = RepaintBoundary(
key: repaintKey,
child: Material(
color: Colors.transparent,
child: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(25),
),
child: const Center(
child: Text(
'自定义',
style: TextStyle(color: Colors.white, fontSize: 12),
),
),
),
),
);
// 首先将 Widget 添加到 Overlay 中进行渲染
final OverlayState? overlayState = Overlay.of(context);
final OverlayEntry entry = OverlayEntry(
builder: (context) => Positioned(
left: -1000, // 放在屏幕外
top: -1000,
child: customWidget,
),
);
overlayState?.insert(entry);
// 等待下一帧完成渲染
await Future.delayed(const Duration(milliseconds: 20));
// 获取渲染对象并转换为图片
final RenderRepaintBoundary? boundary =
repaintKey.currentContext?.findRenderObject() as RenderRepaintBoundary?;
if (boundary == null) {
entry.remove();
return;
}
// 渲染为图片
final ui.Image image = await boundary.toImage(pixelRatio: 3.0);
final ByteData? byteData =
await image.toByteData(format: ui.ImageByteFormat.png);
// 使用完后移除 Overlay
entry.remove();
final Uint8List? bytes = byteData?.buffer.asUint8List();
if (bytes != null) {
final markerPosition =
LatLng(_currentLatLng.latitude, _currentLatLng.longitude + 2 / 1000);
final BitmapDescriptor bitmapDescriptor =
BitmapDescriptor.fromBytes(bytes);
final Marker marker = Marker(
position: _currentLatLng,
icon: bitmapDescriptor,
);
//调用setState触发AMapWidget的更新,从而完成marker的添加
setState(() {
_currentLatLng = markerPosition;
//将新的marker添加到map里
_markers[marker.id] = marker;
});
}
}
添加Marker和Polyline实例
int colorsIndex = 0;
List<Color> colors = <Color>[
Colors.purple,
Colors.red,
Colors.green,
Colors.pink,
];
final Map<String, Polyline> _polylines = <String, Polyline>{};
String? selectedPolylineId;
LatLng mapCenter = const LatLng(36.811483, 118.497235);
final Map<String, Marker> _initMarkerMap = <String, Marker>{};
final BitmapDescriptor _markerIcon =
BitmapDescriptor.fromIconPath('assets/start.png');
final BitmapDescriptor _markerIcon1 =
BitmapDescriptor.fromIconPath('assets/end.png');
void _onMapCreated(AMapController controller) {}
List<LatLng> points = <LatLng>[];
List<LatLng> _createPoints() {
final List<LatLng> points = <LatLng>[];
points.add(const LatLng(39.90403, 116.407525));
points.add(const LatLng(31.238068, 121.501654));
points.add(const LatLng(30.679879, 104.064855));
return points;
}
/// 获取polyline数据
void readJsonFileToMap() async {
String jsonString =
await DefaultAssetBundle.of(context).loadString("assets/map.json");
Map<String, dynamic> jsonMap = jsonDecode(jsonString);
String polylinesPositions = jsonMap['data']['polyline'];
List<String> pointsString = polylinesPositions.split(';');
for (String point in pointsString) {
List<String> latLng = point.split(',');
if (latLng.length == 2) {
points.add(LatLng(double.parse(latLng[0]), double.parse(latLng[1])));
}
}
final Polyline polyline = Polyline(
color: Colors.red, width: 5, points: points, onTap: _onPolylineTapped);
LatLng position = points[0];
LatLng endPosition = points.last;
LatLng centerPosition = points[points.length ~/ 2];
Marker startMarker = Marker(
position: position,
alpha: 1,
icon: _markerIcon,
zIndex: 1,
infoWindow: const InfoWindow(title: '起点', snippet: '这里是起点'),
);
Marker endMarker = Marker(
position: endPosition,
alpha: 1,
icon: _markerIcon1,
zIndex: 1,
infoWindow: const InfoWindow(title: '终点', snippet: '这里是终点'),
);
setState(() {
_polylines[polyline.id] = polyline;
mapCenter = centerPosition;
_initMarkerMap[startMarker.id] = startMarker;
_initMarkerMap[endMarker.id] = endMarker;
});
}
void _add() {
final Polyline polyline = Polyline(
color: Colors.red,
width: 10,
points: _createPoints(),
onTap: _onPolylineTapped);
print('Polyline: ${polyline.toMap().toString()} 被添加了');
setState(() {
_polylines[polyline.id] = polyline;
});
}
@override
Widget build(BuildContext context) {
final AMapWidget map = AMapWidget(
onMapCreated: _onMapCreated,
initialCameraPosition: CameraPosition(
target: mapCenter,
zoom: 5,
),
markers: Set<Marker>.of(_initMarkerMap.values),
polylines: Set<Polyline>.of(_polylines.values),
);
return SizedBox(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(
height: MediaQuery.of(context).size.height * 0.6,
width: MediaQuery.of(context).size.width,
child: map,
),
ElevatedButton(
onPressed: _add,
child: const Text('添加Polyline'),
),
],
),
);
}
地图视图
///用于展示高德地图的Widget
class AMapWidget extends StatefulWidget {
/// 初始化时的地图中心点
final CameraPosition initialCameraPosition;
///地图类型
final MapType mapType;
///自定义地图样式
final CustomStyleOptions? customStyleOptions;
///定位小蓝点
final MyLocationStyleOptions? myLocationStyleOptions;
///缩放级别范围
final MinMaxZoomPreference? minMaxZoomPreference;
///地图显示范围
final LatLngBounds? limitBounds;
///显示路况开关
final bool trafficEnabled;
/// 地图poi是否允许点击
final bool touchPoiEnabled;
///是否显示3D建筑物
final bool buildingsEnabled;
///是否显示底图文字标注
final bool labelsEnabled;
///是否显示指南针
final bool compassEnabled;
///是否显示比例尺
final bool scaleEnabled;
///是否支持缩放手势
final bool zoomGesturesEnabled;
///是否支持滑动手势
final bool scrollGesturesEnabled;
///是否支持旋转手势
final bool rotateGesturesEnabled;
///是否支持倾斜手势
final bool tiltGesturesEnabled;
/// logo 位置,此字段高德只支持Android,本插件iOS借用logoCenter做了实现
final LogoPosition? logoPosition;
/// logo 底部间距(px),此字段高德只支持Android,本插件iOS借用logoCenter做了实现
final int? logoBottomMargin;
/// logo 靠左间距(px),此字段高德只支持Android,本插件iOS借用logoCenter做了实现
final int? logoLeftMargin;
/// 地图上显示的Marker
final Set<Marker> markers;
/// 地图上显示的polyline
final Set<Polyline> polylines;
/// 地图上显示的polygon
final Set<Polygon> polygons;
/// 地图创建成功的回调, 收到此回调之后才可以操作地图
final MapCreatedCallback? onMapCreated;
/// 相机视角持续移动的回调
final ArgumentCallback<CameraPosition>? onCameraMove;
/// 相机视角移动结束的回调
final ArgumentCallback<CameraPosition>? onCameraMoveEnd;
/// 地图单击事件的回调
final ArgumentCallback<LatLng>? onTap;
/// 地图长按事件的回调
final ArgumentCallback<LatLng>? onLongPress;
/// 地图POI的点击回调,需要`touchPoiEnabled`true,才能回调
final ArgumentCallback<AMapPoi>? onPoiTouched;
///位置回调
final ArgumentCallback<AMapLocation>? onLocationChanged;
///需要应用到地图上的手势集合
final Set<Factory<OneSequenceGestureRecognizer>> gestureRecognizers;
/// 设置地图语言
final MapLanguage? mapLanguage;
/// Marker InfoWindow 适配器
final InfoWindowAdapter? infoWindowAdapter;
}
地图控制器
class AMapController {
///改变地图视角
///
///通过[CameraUpdate]对象设置新的中心点、缩放比例、放大缩小、显示区域等内容
///
///(注意:iOS端设置显示区域时,不支持duration参数,动画时长使用iOS地图默认值350毫秒)
///
///可选属性[animated]用于控制是否执行动画移动
///
///可选属性[duration]用于控制执行动画的时长,默认250毫秒,单位:毫秒
Future<void> moveCamera(CameraUpdate cameraUpdate,
{bool animated = true, int duration = 250});
///地图截屏
Future<Uint8List?> takeSnapshot();
/// 清空缓存
Future<void> clearDisk();
/// 经纬度转屏幕坐标
Future<ScreenCoordinate> toScreenCoordinate(LatLng latLng);
/// 屏幕坐标转经纬度
Future<LatLng> fromScreenCoordinate(ScreenCoordinate screenCoordinate);
}
⚙️ 平台配置
Android 平台
-
最低版本要求: Android API 21+
-
targetSDKVersion >= 30 问题修复: 在
android/app/src/main/AndroidManifest.xml
中添加:<application android:allowNativeHeapPointerTagging="false"> <!-- 其他配置 --> </application>
-
权限配置:
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
iOS 平台
- 最低版本要求: iOS 11.0+
- Info.plist 配置:
<key>NSLocationWhenInUseUsageDescription</key> <string>本应用需要在使用期间访问位置服务来显示您的定位</string> <!-- Flutter 1.22.0 之前版本需要 --> <key>io.flutter.embedded_views_preview</key> <string>YES</string>
HarmonyOS 平台
- 系统要求: HarmonyOS Next 5.0+
- 依赖配置: 请参考 HARMONYOS_SETUP.md
- 权限配置: 在
module.json5
中配置定位权限
📄 AMapController API
地图控制器提供了丰富的地图控制功能:
class AMapController {
// 相机控制
Future<void> moveCamera(CameraUpdate cameraUpdate, {
bool animated = true,
int duration = 250,
});
// 地图截屏
Future<Uint8List?> takeSnapshot();
// 坐标转换
Future<ScreenCoordinate> toScreenCoordinate(LatLng latLng);
Future<LatLng> fromScreenCoordinate(ScreenCoordinate screenCoordinate);
// 其他功能
Future<void> clearDisk(); // 清空缓存
Future<String?> getMapContentApprovalNumber(); // 获取审图号
Future<String?> getSatelliteImageApprovalNumber(); // 获取卫星审图号
}
⚠️ 常见问题
1. iOS 地图销毁时 Main Thread Checker 报警
问题: Flutter 插件在 iOS 端,MapView 销毁时一定概率触发 Main Thread Checker 的报警。
原因: 经过对比测试确认是 Flutter 的 bug 所致。
解决方案:
- 依赖 Flutter 升级修复
- 建议使用 Flutter 1.24.0+ 版本
- 参考: Flutter Issue #68490
2. Android targetSDKVersion >= 30 地图页返回闪退
问题: 当 app 的 targetSDKVersion >= 30 时,地图页返回可能闪退。
解决方案: 在 AndroidManifest.xml 中添加配置:
<application android:allowNativeHeapPointerTagging="false">
<!-- 其他配置 -->
</application>
参考: Google 官方说明
3. HarmonyOS 平台问题
问题: HarmonyOS 平台编译或运行错误。
解决方案:
- 确保使用 FVM 和支持 HarmonyOS 的 Flutter 版本
- 检查 oh-package.json5 中的依赖配置
- 参考 HARMONYOS_SETUP.md 进行配置
4. API Key 相关问题
问题: 地图不显示或显示错误信息。
解决方案:
- 检查 API Key 是否正确配置
- 确认 API Key 的平台和包名匹配
- 检查 API Key 是否在高德控制台启用
- 确认已调用合规初始化方法
5. Android 构建问题
问题: Android 构建失败,出现 "Java heap space" 或 SDK 版本相关错误。
解决方案:
- 确保 compileSdkVersion 设置为 36 或更高版本
- 在 gradle.properties 中增加内存分配:
org.gradle.jvmargs=-Xmx4096M
- 移除弃用的配置项如
android.enableR8
- 清理 Gradle 缓存后重新构建
📦 更新日志
v1.0.1 (2025-09-04)
- 🐛 Android 构建修复: 解决了 Android 平台上的构建问题
- 升级 compileSdkVersion 从 35 到 36
- 增加 Gradle 内存分配到 -Xmx4096M
- 移除弃用的 android.enableR8 配置
- 🛠️ Gradle 配置优化: 更新了 Gradle 配置以提高兼容性和性能
v1.0.0 (2024-12-19)
- ✨ 新增 HarmonyOS 平台支持
- 🔧 从 st_amap_flutter_map 重构为 csp_amap_flutter_map
- 🐛 修复所有编译错误和 linting 问题
- 📋 添加完整的文档和示例
- 🎨 优化代码结构和质量
- 🔧 支持 FVM 版本管理
🤝 贡献指南
欢迎贡献代码和建议!
开发环境
-
克隆仓库:
git clone https://gitee.com/chenshipeng0914/csp_amap_flutter_map.git cd csp_amap_flutter_map
-
安装 FVM:
# 安装 FVM dart pub global activate fvm # 使用项目指定的 Flutter 版本 fvm use custom_3.22.0
-
安装依赖:
fvm flutter pub get cd example && fvm flutter pub get
贡献流程
- Fork 本仓库
- 创建特性分支 (
git checkout -b feature/amazing-feature
) - 提交修改 (
git commit -m 'Add amazing feature'
) - 推送分支 (
git push origin feature/amazing-feature
) - 创建 Pull Request
代码规范
- 遵循 Flutter 和 Dart 官方编码规范
- 使用
fvm flutter analyze
检查代码质量 - 添加必要的注释和文档
- 确保所有平台都能正常编译和运行
📞 联系支持
- 问题报告: Gitee Issues
- 功能建议: Gitee Issues
- 讨论交流: Gitee 讨论区
📜 许可证
本项目采用 Apache 2.0 许可证。
🙏 致谢
- 高德开放平台 提供的优秀 SDK
- Flutter 社区 的技术支持
- 所有为本项目贡献代码和建议的开发者