在鸿蒙生态跨平台开发中,地图功能是出行、物流、本地生活类应用的核心模块。Flutter 凭借其跨平台一致性和高性能渲染优势,成为鸿蒙应用开发的优选框架。本文将从实战角度出发,详细讲解如何在鸿蒙 Flutter 应用中集成高德 / 百度地图 SDK,实现基础地图展示、POI 搜索与标注、轨迹绘制等进阶功能,并提供完整可复用的代码示例和避坑指南,帮助开发者快速落地地图相关需求。

一、前置准备:开发环境与核心依赖

1.1 开发环境配置

  • 基础环境

    • 鸿蒙 SDK:API Version 9+(需支持 Flutter 跨平台能力,推荐 API 10 稳定版)
    • Flutter 版本:3.10+(确保与鸿蒙 Flutter 插件兼容,参考 Flutter 鸿蒙适配指南
    • DevEco Studio:4.0+(鸿蒙应用开发 IDE,集成 Flutter 开发插件)
    • 地图平台账号:高德地图开放平台、百度地图开放平台(需申请开发者账号并创建应用,获取 API Key)
  • 环境验证:运行以下命令确认 Flutter 与鸿蒙环境适配:

    bash

    运行

    flutter doctor --harmony
    

    若输出 “HarmonyOS toolchain - develop for HarmonyOS” 且无错误,说明环境配置成功。

1.2 核心依赖库选择

鸿蒙 Flutter 地图开发需依赖支持鸿蒙系统的地图插件,以下是经过实战验证的稳定插件:

地图平台 推荐插件 插件地址 核心优势
高德地图 amap_flutter_map pub.dev 地址 鸿蒙适配完善,支持自定义图层、定位、POI 搜索
百度地图 baidu_map_flutter GitHub 地址 支持离线地图,轨迹绘制功能丰富

依赖配置步骤

  1. 在 pubspec.yaml 中添加插件依赖:

    yaml

    dependencies:
      flutter:
        sdk: flutter
      # 高德地图依赖(二选一)
      amap_flutter_map: ^3.0.0  # 需与鸿蒙 SDK 版本匹配
      amap_flutter_location: ^5.0.0  # 定位辅助插件
      # 百度地图依赖(二选一)
      baidu_map_flutter: ^2.1.0
      baidu_map_flutter_location: ^1.0.0
      # 其他辅助依赖
      permission_handler: ^10.2.0  # 权限申请
      dio: ^5.3.0  # 网络请求(POI 搜索接口调用)
      json_serializable: ^6.7.0  # JSON 解析
    
  2. 执行 flutter pub get 安装依赖,若出现兼容性问题,可通过 flutter pub upgrade 更新插件版本。

1.3 地图 API Key 申请

(1)高德地图 API Key 申请
  1. 访问 高德地图开放平台,注册并创建应用(选择 “鸿蒙应用” 类型)。
  2. 进入应用管理,添加 “地图 SDK” 和 “POI 搜索” 服务,获取 API Key(需配置应用包名和签名信息)。
  3. 参考 高德地图鸿蒙 SDK 配置文档 完成权限配置。
(2)百度地图 API Key 申请
  1. 访问 百度地图开放平台,注册并创建应用(选择 “鸿蒙应用”)。
  2. 申请 “地图 SDK” 和 “POI 检索” 权限,获取 API Key(需填写应用包名和签名)。
  3. 配置参考 百度地图鸿蒙 SDK 快速入门

二、基础地图集成:高德 / 百度地图快速接入

2.1 高德地图集成步骤

(1)权限配置

在鸿蒙应用的 config.json 中添加必要权限:

json

{
  "module": {
    "reqPermissions": [
      {
        "name": "ohos.permission.LOCATION",
        "reason": "获取位置信息用于地图定位",
        "usedScene": {
          "ability": ["com.example.map_demo.MainAbility"],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.INTERNET",
        "reason": "访问网络加载地图数据",
        "usedScene": {
          "ability": ["com.example.map_demo.MainAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}
(2)初始化地图 SDK

在应用入口 main.dart 中初始化高德地图 SDK:

dart

import 'package:amap_flutter_map/amap_flutter_map.dart';
import 'package:amap_flutter_location/amap_flutter_location.dart';
import 'package:flutter/material.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // 初始化高德地图 SDK(替换为你的 API Key)
  await AMapFlutterMap.initialize("你的高德地图 API Key");
  // 初始化定位 SDK
  await AMapFlutterLocation().initialize();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "鸿蒙 Flutter 地图 Demo",
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const MapHomePage(),
    );
  }
}
(3)基础地图展示

创建 MapHomePage 组件,实现地图加载和基础交互:

dart

class MapHomePage extends StatefulWidget {
  const MapHomePage({super.key});

  @override
  State<MapHomePage> createState() => _MapHomePageState();
}

class _MapHomePageState extends State<MapHomePage> {
  // 地图控制器(用于操作地图缩放、平移等)
  late AMapController _mapController;
  // 初始中心点坐标(北京天安门)
  final LatLng _initialCenter = const LatLng(39.9042, 116.4074);
  // 初始缩放级别(3-20,数值越大越详细)
  final double _initialZoom = 14.0;

  // 地图创建完成回调
  void _onMapCreated(AMapController controller) {
    _mapController = controller;
    // 设置地图样式(可选:标准、卫星、黑夜模式)
    _mapController.setMapStyle(AMapMapStyle.standard);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("高德地图基础展示")),
      body: AMapWidget(
        initialCameraPosition: CameraPosition(
          target: _initialCenter,
          zoom: _initialZoom,
        ),
        onMapCreated: _onMapCreated,
        // 启用定位功能
        myLocationStyleOptions: MyLocationStyleOptions(
          enabled: true,
          myLocationType: MyLocationType.show,
          icon: BitmapDescriptor.fromAssetImage(
            const ImageConfiguration(size: Size(30, 30)),
            "images/location_marker.png", // 自定义定位图标
          ),
        ),
        // 启用地图交互(缩放、平移、旋转)
        interactive: true,
        zoomGesturesEnabled: true,
        scrollGesturesEnabled: true,
        rotateGesturesEnabled: true,
      ),
    );
  }

  @override
  void dispose() {
    // 释放地图资源
    _mapController.dispose();
    AMapFlutterLocation().dispose();
    super.dispose();
  }
}

2.2 百度地图集成步骤

(1)权限配置

与高德地图一致,需在 config.json 中添加定位和网络权限。

(2)初始化百度地图 SDK

dart

import 'package:baidu_map_flutter/baidu_map_flutter.dart';
import 'package:flutter/material.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // 初始化百度地图 SDK(替换为你的 API Key)
  await BaiduMapSdk.initSdk("你的百度地图 API Key");
  runApp(const MyApp());
}
(3)基础地图展示

dart

class BaiduMapHomePage extends StatefulWidget {
  const BaiduMapHomePage({super.key});

  @override
  State<BaiduMapHomePage> createState() => _BaiduMapHomePageState();
}

class _BaiduMapHomePageState extends State<BaiduMapHomePage> {
  late BaiduMapController _mapController;
  final LatLng _initialCenter = const LatLng(39.9042, 116.4074);
  final double _initialZoom = 14.0;

  void _onMapCreated(BaiduMapController controller) {
    _mapController = controller;
    // 设置地图类型(普通、卫星、交通)
    _mapController.setMapType(MapType.normal);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("百度地图基础展示")),
      body: BaiduMap(
        initialCameraPosition: CameraPosition(
          target: _initialCenter,
          zoom: _initialZoom,
        ),
        onMapCreated: _onMapCreated,
        // 定位配置
        locationEnabled: true,
        myLocationConfig: MyLocationConfig(
          myLocationType: MyLocationType.locationAndRotate,
          icon: BitmapDescriptor.fromAsset("images/location_marker.png"),
        ),
        // 交互配置
        zoomGesturesEnabled: true,
        scrollGesturesEnabled: true,
      ),
    );
  }

  @override
  void dispose() {
    _mapController.dispose();
    super.dispose();
  }
}

2.3 基础功能对比与选型建议

功能点 高德地图 百度地图
鸿蒙适配度 ★★★★★ ★★★★☆
地图加载速度 较快
定位精度 高(支持 GPS + 网络定位) 高(支持北斗定位)
离线地图 支持 支持(文档更详细)
自定义样式 丰富(支持自定义地图主题) 一般(基础样式可配置)

选型建议

  • 若需丰富的自定义地图样式和 POI 搜索功能,优先选择高德地图;
  • 若需离线地图和北斗定位支持,优先选择百度地图;
  • 若应用需同时兼容多平台(鸿蒙 + Android+iOS),建议封装统一地图接口,根据平台动态切换 SDK。

三、进阶功能:POI 搜索与标注

POI(Point of Interest)即兴趣点,是地图应用中常用的功能(如搜索餐厅、加油站、公交站等)。本节将讲解如何通过高德 / 百度地图 SDK 实现 POI 搜索,并在地图上标注结果。

3.1 高德地图 POI 搜索实现

(1)POI 搜索 API 调用

使用 amap_flutter_search 插件或直接调用高德地图 Web API 实现搜索。以下是 Web API 方式(无需额外插件,灵活性更高):

dart

import 'package:dio/dio.dart';
import 'dart:convert';

// POI 搜索模型
class PoiSearchResult {
  final String name; // 名称
  final LatLng location; // 坐标
  final String address; // 地址
  final String type; // 类型

  PoiSearchResult({
    required this.name,
    required this.location,
    required this.address,
    required this.type,
  });

  factory PoiSearchResult.fromJson(Map<String, dynamic> json) {
    return PoiSearchResult(
      name: json["name"],
      location: LatLng(
        double.parse(json["location"].split(",")[1]),
        double.parse(json["location"].split(",")[0]),
      ),
      address: json["address"],
      type: json["type"],
    );
  }
}

class AMapPoiSearch {
  static const String _apiKey = "你的高德地图 API Key";
  static const String _searchUrl = "https://restapi.amap.com/v3/place/text";

  // 关键词搜索 POI
  static Future<List<PoiSearchResult>> searchByKeyword({
    required String keyword,
    required LatLng center,
    int radius = 5000, // 搜索半径(米)
    int pageSize = 20,
    int pageNum = 1,
  }) async {
    try {
      final response = await Dio().get(_searchUrl, queryParameters: {
        "key": _apiKey,
        "keywords": keyword,
        "location": "${center.longitude},${center.latitude}",
        "radius": radius,
        "page_size": pageSize,
        "page_num": pageNum,
        "output": "json",
      });

      if (response.statusCode == 200) {
        final data = json.decode(response.data);
        if (data["status"] == "1") {
          final List<dynamic> pois = data["pois"];
          return pois.map((poi) => PoiSearchResult.fromJson(poi)).toList();
        } else {
          throw Exception("POI 搜索失败:${data["info"]}");
        }
      } else {
        throw Exception("网络请求失败:${response.statusCode}");
      }
    } catch (e) {
      debugPrint("POI 搜索异常:$e");
      return [];
    }
  }
}
(2)POI 结果标注在地图上

修改 MapHomePage,添加搜索框和 POI 标注:

dart

class _MapHomePageState extends State<MapHomePage> {
  final TextEditingController _searchController = TextEditingController();
  List<PoiSearchResult> _poiResults = [];
  // 存储 POI 标注点
  final List<Marker> _poiMarkers = [];

  // 搜索 POI 并更新标注
  Future<void> _searchPoi(String keyword) async {
    if (keyword.isEmpty) return;
    // 获取当前地图中心点
    final CameraPosition currentCamera = await _mapController.getCameraPosition();
    final List<PoiSearchResult> results = await AMapPoiSearch.searchByKeyword(
      keyword: keyword,
      center: currentCamera.target,
    );

    setState(() {
      _poiResults = results;
      // 清除旧标注
      _poiMarkers.clear();
      // 添加新标注
      for (int i = 0; i < results.length; i++) {
        final poi = results[i];
        _poiMarkers.add(
          Marker(
            markerId: MarkerId("poi_$i"),
            position: poi.location,
            icon: BitmapDescriptor.fromAssetImage(
              const ImageConfiguration(size: Size(30, 30)),
              "images/poi_marker.png", // 自定义 POI 图标
            ),
            infoWindow: InfoWindow(
              title: poi.name,
              snippet: poi.address,
              onTap: () {
                // 点击信息窗回调
                debugPrint("点击了 ${poi.name}");
              },
            ),
            onTap: () {
              // 点击标注点显示信息窗
              _mapController.showInfoWindow(MarkerId("poi_$i"));
            },
          ),
        );
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("高德地图 POI 搜索")),
      body: Stack(
        children: [
          AMapWidget(
            initialCameraPosition: CameraPosition(
              target: _initialCenter,
              zoom: _initialZoom,
            ),
            onMapCreated: _onMapCreated,
            myLocationStyleOptions: MyLocationStyleOptions(enabled: true),
            markers: Set<Marker>.of(_poiMarkers), // 添加 POI 标注
          ),
          // 搜索框
          Positioned(
            top: 20,
            left: 20,
            right: 20,
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _searchController,
                    decoration: InputDecoration(
                      hintText: "搜索餐厅、加油站、公交站...",
                      fillColor: Colors.white,
                      filled: true,
                      border: OutlineInputBorder(
                        borderRadius: BorderRadius.circular(8),
                        borderSide: BorderSide.none,
                      ),
                      contentPadding: const EdgeInsets.symmetric(horizontal: 16),
                    ),
                  ),
                ),
                const SizedBox(width: 10),
                ElevatedButton(
                  onPressed: () => _searchPoi(_searchController.text),
                  child: const Text("搜索"),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

3.2 百度地图 POI 搜索实现

百度地图 POI 搜索类似,以下是核心代码:

dart

// 百度地图 POI 搜索 API
class BaiduPoiSearch {
  static const String _apiKey = "你的百度地图 API Key";
  static const String _searchUrl = "https://api.map.baidu.com/place/v2/search";

  static Future<List<PoiSearchResult>> searchByKeyword({
    required String keyword,
    required LatLng center,
    int radius = 5000,
  }) async {
    try {
      final response = await Dio().get(_searchUrl, queryParameters: {
        "ak": _apiKey,
        "query": keyword,
        "location": "${center.latitude},${center.longitude}",
        "radius": radius,
        "output": "json",
        "scope": 2, // 返回详细信息
      });

      if (response.statusCode == 200) {
        final data = json.decode(response.data);
        if (data["status"] == 0) {
          final List<dynamic> results = data["results"];
          return results.map((poi) => PoiSearchResult(
            name: poi["name"],
            location: LatLng(
              poi["location"]["lat"],
              poi["location"]["lng"],
            ),
            address: poi["address"],
            type: poi["detail_info"]["type"],
          )).toList();
        } else {
          throw Exception("POI 搜索失败:${data["message"]}");
        }
      } else {
        throw Exception("网络请求失败");
      }
    } catch (e) {
      debugPrint("POI 搜索异常:$e");
      return [];
    }
  }
}

// 百度地图 POI 标注
class _BaiduMapHomePageState extends State<BaiduMapHomePage> {
  final TextEditingController _searchController = TextEditingController();
  List<PoiSearchResult> _poiResults = [];
  final List<Marker> _poiMarkers = [];

  Future<void> _searchPoi(String keyword) async {
    if (keyword.isEmpty) return;
    final CameraPosition currentCamera = await _mapController.getCameraPosition();
    final List<PoiSearchResult> results = await BaiduPoiSearch.searchByKeyword(
      keyword: keyword,
      center: currentCamera.target,
    );

    setState(() {
      _poiResults = results;
      _poiMarkers.clear();
      for (int i = 0; i < results.length; i++) {
        final poi = results[i];
        _poiMarkers.add(
          Marker(
            markerId: MarkerId("poi_$i"),
            position: poi.location,
            icon: BitmapDescriptor.fromAsset("images/poi_marker.png"),
            infoWindow: InfoWindow(
              title: poi.name,
              snippet: poi.address,
            ),
          ),
        );
      }
    });
  }

  // 构建 UI 部分与高德地图类似,替换 AMapWidget 为 BaiduMap 即可
}

3.3 POI 搜索优化技巧

  1. 搜索半径动态调整:根据地图缩放级别调整搜索半径(缩放级别越高,半径越小)。
  2. 防抖处理:使用 Debouncer 避免频繁搜索(输入完成后延迟 500ms 再发起请求):

    dart

    class Debouncer {
      final int milliseconds;
      VoidCallback? _action;
      Timer? _timer;
    
      Debouncer({this.milliseconds = 500});
    
      run(VoidCallback action) {
        _timer?.cancel();
        _timer = Timer(Duration(milliseconds: milliseconds), action);
      }
    }
    
    // 使用示例
    final Debouncer _debouncer = Debouncer();
    TextField(
      onChanged: (value) {
        _debouncer.run(() => _searchPoi(value));
      },
    );
    
  3. 缓存搜索结果:使用 shared_preferences 缓存历史搜索结果,减少重复网络请求。
  4. 权限判断:搜索前检查网络权限,无网络时提示用户。

四、高级功能:自定义轨迹图层绘制

轨迹绘制是运动、物流类应用的核心需求(如跑步轨迹、车辆行驶轨迹)。本节将讲解如何在地图上绘制自定义轨迹,支持轨迹颜色、宽度、透明度配置,以及轨迹动画效果。

4.1 高德地图轨迹绘制

(1)轨迹数据模型

dart

// 轨迹点模型
class TrackPoint {
  final LatLng location; // 坐标
  final double speed; // 速度(km/h)
  final int timestamp; // 时间戳(毫秒)

  TrackPoint({
    required this.location,
    required this.speed,
    required this.timestamp,
  });
}

// 轨迹线模型
class TrackLine {
  final List<TrackPoint> points; // 轨迹点列表
  final Color color; // 轨迹颜色
  final double width; // 轨迹宽度(像素)
  final double opacity; // 透明度(0-1)

  TrackLine({
    required this.points,
    this.color = Colors.blue,
    this.width = 5.0,
    this.opacity = 1.0,
  });
}
(2)轨迹绘制实现

通过 Polyline 组件绘制轨迹线,支持动态添加轨迹点:

dart

class _MapHomePageState extends State<MapHomePage> {
  // 模拟轨迹数据(北京二环部分路段)
  final List<TrackPoint> _mockTrackPoints = [
    TrackPoint(
      location: const LatLng(39.9042, 116.4074),
      speed: 30.0,
      timestamp: 1690000000000,
    ),
    TrackPoint(
      location: const LatLng(39.9142, 116.4174),
      speed: 35.0,
      timestamp: 1690000010000,
    ),
    TrackPoint(
      location: const LatLng(39.9242, 116.4274),
      speed: 40.0,
      timestamp: 1690000020000,
    ),
    // 更多轨迹点...
  ];

  // 轨迹线
  late Polyline _trackPolyline;

  @override
  void initState() {
    super.initState();
    // 初始化轨迹线
    _trackPolyline = Polyline(
      polylineId: const PolylineId("track_line"),
      points: _mockTrackPoints.map((p) => p.location).toList(),
      color: Colors.red.withOpacity(0.8),
      width: 6.0,
      jointType: JointType.round, // 线条拐点圆角
      capType: CapType.round, // 线条端点圆角
    );
  }

  // 动态添加轨迹点
  void _addTrackPoint(TrackPoint point) {
    setState(() {
      _mockTrackPoints.add(point);
      _trackPolyline = _trackPolyline.copyWith(
        pointsParam: _mockTrackPoints.map((p) => p.location).toList(),
      );
    });
    // 移动地图视角到最新轨迹点
    _mapController.moveCamera(
      CameraUpdate.newLatLng(point.location),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("高德地图轨迹绘制")),
      body: Stack(
        children: [
          AMapWidget(
            initialCameraPosition: CameraPosition(
              target: _initialCenter,
              zoom: _initialZoom,
            ),
            onMapCreated: _onMapCreated,
            polylines: {_trackPolyline}, // 添加轨迹线
            markers: {
              // 轨迹起点标注
              Marker(
                markerId: const MarkerId("start_point"),
                position: _mockTrackPoints.first.location,
                icon: BitmapDescriptor.fromAssetImage(
                  const ImageConfiguration(size: Size(30, 30)),
                  "images/start_marker.png",
                ),
                infoWindow: const InfoWindow(title: "起点"),
              ),
              // 轨迹终点标注
              Marker(
                markerId: const MarkerId("end_point"),
                position: _mockTrackPoints.last.location,
                icon: BitmapDescriptor.fromAssetImage(
                  const ImageConfiguration(size: Size(30, 30)),
                  "images/end_marker.png",
                ),
                infoWindow: const InfoWindow(title: "终点"),
              ),
            },
          ),
          // 控制按钮
          Positioned(
            bottom: 20,
            left: 20,
            child: ElevatedButton(
              onPressed: () {
                // 模拟添加新轨迹点(每 1 秒添加一个)
                Timer.periodic(const Duration(seconds: 1), (timer) {
                  if (_mockTrackPoints.length >= 20) {
                    timer.cancel();
                    return;
                  }
                  _addTrackPoint(
                    TrackPoint(
                      location: LatLng(
                        _mockTrackPoints.last.location.latitude + 0.001,
                        _mockTrackPoints.last.location.longitude + 0.001,
                      ),
                      speed: 45.0,
                      timestamp: DateTime.now().millisecondsSinceEpoch,
                    ),
                  );
                });
              },
              child: const Text("开始绘制轨迹"),
            ),
          ),
        ],
      ),
    );
  }
}

4.2 百度地图轨迹绘制

百度地图轨迹绘制与高德地图类似,使用 Polyline 组件:

dart

class _BaiduMapHomePageState extends State<BaiduMapHomePage> {
  late Polyline _trackPolyline;
  final List<TrackPoint> _mockTrackPoints = [/* 模拟轨迹点 */];

  @override
  void initState() {
    super.initState();
    _trackPolyline = Polyline(
      polylineId: const PolylineId("track_line"),
      points: _mockTrackPoints.map((p) => p.location).toList(),
      color: Colors.green.withOpacity(0.8),
      width: 6,
      jointType: JointType.round,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("百度地图轨迹绘制")),
      body: BaiduMap(
        initialCameraPosition: CameraPosition(
          target: _initialCenter,
          zoom: _initialZoom,
        ),
        onMapCreated: _onMapCreated,
        polylines: {_trackPolyline},
        markers: {
          Marker(
            markerId: const MarkerId("start_point"),
            position: _mockTrackPoints.first.location,
            icon: BitmapDescriptor.fromAsset("images/start_marker.png"),
          ),
          Marker(
            markerId: const MarkerId("end_point"),
            position: _mockTrackPoints.last.location,
            icon: BitmapDescriptor.fromAsset("images/end_marker.png"),
          ),
        },
      ),
    );
  }
}

4.3 轨迹绘制进阶优化

  1. 轨迹平滑处理:使用抽稀算法(如道格拉斯 - 普克算法)减少轨迹点数量,提升绘制性能:

    dart

    // 道格拉斯-普克算法抽稀轨迹点
    List<TrackPoint> simplifyTrack(List<TrackPoint> points, double tolerance) {
      if (points.length <= 2) return points;
    
      double maxDistance = 0;
      int index = 0;
      final LatLng start = points.first.location;
      final LatLng end = points.last.location;
    
      for (int i = 1; i < points.length - 1; i++) {
        final double distance = _distanceToLine(
          points[i].location,
          start,
          end,
        );
        if (distance > maxDistance) {
          maxDistance = distance;
          index = i;
        }
      }
    
      if (maxDistance > tolerance) {
        final List<TrackPoint> left = simplifyTrack(points.sublist(0, index + 1), tolerance);
        final List<TrackPoint> right = simplifyTrack(points.sublist(index), tolerance);
        return [...left.sublist(0, left.length - 1), ...right];
      } else {
        return [points.first, points.last];
      }
    }
    
    // 计算点到直线的距离(米)
    double _distanceToLine(LatLng point, LatLng start, LatLng end) {
      // 实现距离计算公式(参考高德/百度地图 SDK 提供的距离计算工具)
      final double a = start.latitude - end.latitude;
      final double b = end.longitude - start.longitude;
      final double c = start.longitude * end.latitude - end.longitude * start.latitude;
      return (a * point.latitude + b * point.longitude + c).abs() / sqrt(a * a + b * b);
    }
    
  2. 轨迹颜色渐变:根据速度动态调整轨迹颜色(如速度越快颜色越红):

    dart

    Color _getTrackColor(double speed) {
      if (speed < 30) return Colors.green;
      if (speed < 60) return Colors.yellow;
      return Colors.red;
    }
    
    // 分段绘制渐变轨迹
    List<Polyline> buildGradientTrack(List<TrackPoint> points) {
      final List<Polyline> polylines = [];
      for (int i = 0; i < points.length - 1; i++) {
        final start = points[i];
        final end = points[i + 1];
        polylines.add(
          Polyline(
            polylineId: PolylineId("track_$i"),
            points: [start.location, end.location],
            color: _getTrackColor((start.speed + end.speed) / 2),
            width: 6.0,
          ),
        );
      }
      return polylines;
    }
    
  3. 轨迹动画效果:使用 AnimationController 实现轨迹动态绘制:

    dart

    late AnimationController _animationController;
    late Animation<int> _animation;
    List<LatLng> _animatedPoints = [];
    
    @override
    void initState() {
      super.initState();
      _animationController = AnimationController(
        vsync: this,
        duration: const Duration(seconds: 5),
      );
      _animation = IntTween(begin: 0, end: _mockTrackPoints.length - 1).animate(
        CurvedAnimation(parent: _animationController, curve: Curves.linear),
      )..addListener(() {
          setState(() {
            _animatedPoints = _mockTrackPoints.sublist(0, _animation.value + 1).map((p) => p.location).toList();
            _trackPolyline = _trackPolyline.copyWith(pointsParam: _animatedPoints);
          });
        });
    }
    
    // 启动轨迹动画
    void _startTrackAnimation() {
      _animationController.forward(from: 0);
    }
    

五、实战案例:完整地图应用整合

5.1 项目结构设计

plaintext

map_demo/
├── lib/
│   ├── main.dart          # 应用入口
│   ├── pages/
│   │   ├── map_home_page.dart  # 地图主页面(集成高德/百度地图)
│   ├── models/
│   │   ├── poi_model.dart      # POI 数据模型
│   │   ├── track_model.dart     # 轨迹数据模型
│   ├── services/
│   │   ├── amap_service.dart    # 高德地图服务(POI 搜索、定位)
│   │   ├── baidu_service.dart   # 百度地图服务
│   ├── utils/
│   │   ├── debouncer.dart       # 防抖工具
│   │   ├── track_simplify.dart  # 轨迹抽稀工具
│   ├── res/
│   │   ├── images/              # 地图图标资源
├── config.json             # 鸿蒙应用配置(权限、包名等)
├── pubspec.yaml            # 依赖配置

5.2 核心功能整合代码

以下是整合了地图展示、POI 搜索、轨迹绘制的完整页面代码(以高德地图为例):

dart

import 'package:flutter/material.dart';
import 'package:amap_flutter_map/amap_flutter_map.dart';
import 'package:amap_flutter_location/amap_flutter_location.dart';
import 'package:dio/dio.dart';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:permission_handler/permission_handler.dart';

// 导入自定义模型和工具
import '../models/poi_model.dart';
import '../models/track_model.dart';
import '../utils/debouncer.dart';
import '../utils/track_simplify.dart';

class MapIntegratePage extends StatefulWidget {
  const MapIntegratePage({super.key});

  @override
  State<MapIntegratePage> createState() => _MapIntegratePageState();
}

class _MapIntegratePageState extends State<MapIntegratePage> with SingleTickerProviderStateMixin {
  late AMapController _mapController;
  final LatLng _initialCenter = const LatLng(39.9042, 116.4074);
  final double _initialZoom = 14.0;

  // POI 搜索相关
  final TextEditingController _searchController = TextEditingController();
  final Debouncer _debouncer = Debouncer(milliseconds: 500);
  List<PoiSearchResult> _poiResults = [];
  final List<Marker> _poiMarkers = [];

  // 轨迹绘制相关
  late AnimationController _animationController;
  late Animation<int> _animation;
  List<TrackPoint> _mockTrackPoints = [/* 模拟轨迹点 */];
  List<LatLng> _animatedPoints = [];
  late Polyline _trackPolyline;
  bool _isTracking = false;

  // 权限状态
  bool _hasLocationPermission = false;

  @override
  void initState() {
    super.initState();
    // 检查定位权限
    _checkLocationPermission();
    // 初始化轨迹
    _initTrack();
    // 初始化轨迹动画
    _initTrackAnimation();
  }

  // 检查定位权限
  Future<void> _checkLocationPermission() async {
    final status = await Permission.location.request();
    setState(() {
      _hasLocationPermission = status == PermissionStatus.granted;
    });
  }

  // 初始化轨迹
  void _initTrack() {
    // 模拟轨迹数据
    _mockTrackPoints = List.generate(30, (index) {
      return TrackPoint(
        location: LatLng(
          39.9042 + index * 0.001,
          116.4074 + index * 0.001,
        ),
        speed: 30 + index * 1.5,
        timestamp: DateTime.now().millisecondsSinceEpoch - (30 - index) * 1000,
      );
    });
    // 抽稀轨迹点
    final simplifiedPoints = simplifyTrack(_mockTrackPoints, 10); // 抽稀 tolerance 为 10 米
    _mockTrackPoints = simplifiedPoints;
    // 初始化轨迹线
    _trackPolyline = Polyline(
      polylineId: const PolylineId("track_line"),
      points: _animatedPoints,
      color: Colors.red.withOpacity(0.8),
      width: 6.0,
      jointType: JointType.round,
    );
  }

  // 初始化轨迹动画
  void _initTrackAnimation() {
    _animationController = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 8),
    );
    _animation = IntTween(begin: 0, end: _mockTrackPoints.length - 1).animate(
      CurvedAnimation(parent: _animationController, curve: Curves.linear),
    )..addListener(() {
        setState(() {
          _animatedPoints = _mockTrackPoints.sublist(0, _animation.value + 1).map((p) => p.location).toList();
          _trackPolyline = _trackPolyline.copyWith(pointsParam: _animatedPoints);
          // 移动地图视角到最新轨迹点
          if (_animatedPoints.isNotEmpty) {
            _mapController.moveCamera(
              CameraUpdate.newLatLng(_animatedPoints.last),
            );
          }
        });
      });
  }

  // 地图创建完成回调
  void _onMapCreated(AMapController controller) {
    _mapController = controller;
    _mapController.setMapStyle(AMapMapStyle.standard);
    // 若有定位权限,启用定位
    if (_hasLocationPermission) {
      _mapController.setMyLocationEnabled(true);
    }
  }

  // POI 搜索
  Future<void> _searchPoi(String keyword) async {
    if (keyword.isEmpty) return;
    final CameraPosition currentCamera = await _mapController.getCameraPosition();
    final List<PoiSearchResult> results = await AMapPoiSearch.searchByKeyword(
      keyword: keyword,
      center: currentCamera.target,
    );

    setState(() {
      _poiResults = results;
      _poiMarkers.clear();
      for (int i = 0; i < results.length; i++) {
        final poi = results[i];
        _poiMarkers.add(
          Marker(
            markerId: MarkerId("poi_$i"),
            position: poi.location,
            icon: BitmapDescriptor.fromAssetImage(
              const ImageConfiguration(size: Size(30, 30)),
              "images/poi_marker.png",
            ),
            infoWindow: InfoWindow(
              title: poi.name,
              snippet: poi.address,
            ),
            onTap: () => _mapController.showInfoWindow(MarkerId("poi_$i")),
          ),
        );
      }
    });
  }

  // 启动/暂停轨迹绘制
  void _toggleTrack() {
    setState(() {
      _isTracking = !_isTracking;
      if (_isTracking) {
        _animationController.forward(from: 0);
      } else {
        _animationController.stop();
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("鸿蒙 Flutter 地图综合 Demo"),
        actions: [
          IconButton(
            icon: const Icon(Icons.layers),
            onPressed: () {
              // 切换地图样式
              _mapController.setMapStyle(
                _mapController.mapStyle == AMapMapStyle.standard
                    ? AMapMapStyle.dark
                    : AMapMapStyle.standard,
              );
            },
          ),
        ],
      ),
      body: Stack(
        children: [
          AMapWidget(
            initialCameraPosition: CameraPosition(
              target: _initialCenter,
              zoom: _initialZoom,
            ),
            onMapCreated: _onMapCreated,
            myLocationStyleOptions: MyLocationStyleOptions(
              enabled: _hasLocationPermission,
              icon: BitmapDescriptor.fromAssetImage(
                const ImageConfiguration(size: Size(30, 30)),
                "images/location_marker.png",
              ),
            ),
            markers: Set<Marker>.of([
              ..._poiMarkers,
              // 轨迹起点和终点
              if (_animatedPoints.isNotEmpty)
                Marker(
                  markerId: const MarkerId("start_point"),
                  position: _animatedPoints.first,
                  icon: BitmapDescriptor.fromAssetImage(
                    const ImageConfiguration(size: Size(30, 30)),
                    "images/start_marker.png",
                  ),
                ),
              if (_animatedPoints.isNotEmpty && _animatedPoints.length == _mockTrackPoints.length)
                Marker(
                  markerId: const MarkerId("end_point"),
                  position: _animatedPoints.last,
                  icon: BitmapDescriptor.fromAssetImage(
                    const ImageConfiguration(size: Size(30, 30)),
                    "images/end_marker.png",
                  ),
                ),
            ]),
            polylines: {_trackPolyline},
            interactive: true,
          ),
          // 搜索框
          Positioned(
            top: 20,
            left: 20,
            right: 20,
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _searchController,
                    decoration: InputDecoration(
                      hintText: "搜索兴趣点...",
                      fillColor: Colors.white,
                      filled: true,
                      border: OutlineInputBorder(
                        borderRadius: BorderRadius.circular(8),
                        borderSide: BorderSide.none,
                      ),
                      contentPadding: const EdgeInsets.symmetric(horizontal: 16),
                    ),
                    onChanged: (value) => _debouncer.run(() => _searchPoi(value)),
                  ),
                ),
                const SizedBox(width: 10),
                ElevatedButton(
                  onPressed: () => _searchPoi(_searchController.text),
                  child: const Text("搜索"),
                ),
              ],
            ),
          ),
          // 轨迹控制按钮
          Positioned(
            bottom: 20,
            left: 20,
            child: ElevatedButton(
              onPressed: _toggleTrack,
              style: ElevatedButton.styleFrom(
                backgroundColor: _isTracking ? Colors.red : Colors.blue,
              ),
              child: Text(_isTracking ? "暂停轨迹" : "开始轨迹"),
            ),
          ),
          // 定位按钮
          Positioned(
            bottom: 20,
            right: 20,
            child: FloatingActionButton(
              onPressed: () {
                if (_hasLocationPermission) {
                  _mapController.moveCamera(CameraUpdate.newLatLngZoom(
                    _mapController.myLocation?.position ?? _initialCenter,
                    16.0,
                  ));
                } else {
                  _checkLocationPermission();
                }
              },
              child: const Icon(Icons.my_location),
            ),
          ),
        ],
      ),
    );
  }

  @override
  void dispose() {
    _searchController.dispose();
    _debouncer._timer?.cancel();
    _animationController.dispose();
    _mapController.dispose();
    AMapFlutterLocation().dispose();
    super.dispose();
  }
}

// POI 搜索服务(复用前文代码)
class AMapPoiSearch {
  static const String _apiKey = "你的高德地图 API Key";
  static const String _searchUrl = "https://restapi.amap.com/v3/place/text";

  static Future<List<PoiSearchResult>> searchByKeyword({
    required String keyword,
    required LatLng center,
    int radius = 5000,
    int pageSize = 20,
    int pageNum = 1,
  }) async {
    // 实现代码复用前文...
  }
}

5.3 鸿蒙应用配置补充

在 config.json 中配置应用包名、签名信息和权限:

json

{
  "app": {
    "bundleName": "com.example.map_demo",
    "vendor": "example",
    "version": {
      "code": 1000000,
      "name": "1.0.0"
    },
    "apiVersion": {
      "compatible": 9,
      "target": 10
    }
  },
  "module": {
    "package": "com.example.map_demo",
    "name": ".MapDemo",
    "mainAbility": "com.example.map_demo.MainAbility",
    "deviceType": ["phone", "tablet"],
    "reqPermissions": [
      {
        "name": "ohos.permission.LOCATION",
        "reason": "用于地图定位和轨迹记录",
        "usedScene": {
          "ability": ["com.example.map_demo.MainAbility"],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.INTERNET",
        "reason": "用于加载地图数据和 POI 搜索",
        "usedScene": {
          "ability": ["com.example.map_demo.MainAbility"],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.WRITE_EXTERNAL_STORAGE",
        "reason": "用于存储离线地图数据",
        "usedScene": {
          "ability": ["com.example.map_demo.MainAbility"],
          "when": "inuse"
        }
      }
    ],
    "distro": {
      "deliveryWithInstall": true,
      "moduleName": "map_demo",
      "moduleType": "entry",
      "installationFree": false
    }
  }
}

六、问题排查与性能优化

6.1 常见问题及解决方案

问题现象 可能原因 解决方案
地图加载空白 1. API Key 错误或未配置;2. 网络权限未申请;3. 插件版本不兼容 1. 检查 API Key 是否正确,确保已启用对应服务;2. 确认 config.json 中添加了网络权限;3. 升级 Flutter 和插件版本,参考插件文档的兼容说明
定位失败 1. 定位权限未申请;2. 设备定位功能未开启;3. 室内定位信号弱 1. 动态申请定位权限,引导用户授权;2. 提示用户开启设备定位功能;3. 结合网络定位提升精度
POI 搜索无结果 1. 关键词错误;2. 搜索半径过小;3. API Key 无 POI 搜索权限 1. 检查关键词格式;2. 扩大搜索半径;3. 在地图开放平台启用 POI 搜索服务
轨迹绘制卡顿 1. 轨迹点数量过多;2. 频繁 setState 刷新界面 1. 使用抽稀算法减少轨迹点;2. 优化状态管理,避免不必要的刷新

6.2 性能优化技巧

  1. 地图资源管理

    • 按需加载地图:不在前台时暂停地图渲染,释放资源;
    • 优化图标资源:使用矢量图(SVG)替代位图,减少内存占用;
    • 及时释放控制器:在 dispose 方法中释放地图控制器和定位资源。
  2. 渲染性能优化

    • 减少标注点数量:批量展示 POI 时,根据地图缩放级别动态加载(缩放级别低时不显示密集标注);
    • 避免重叠标注:使用聚类算法(如 flutter_map_marker_cluster 插件)合并密集标注点;
    • 优化轨迹绘制:抽稀轨迹点,避免一次性绘制大量线段。
  3. 网络优化

    • 缓存地图瓦片:启用离线地图功能,减少网络请求;
    • 压缩 POI 搜索结果:只返回必要字段,减少数据传输量;
    • 批量请求:合并多个 POI 搜索请求,减少接口调用次数。

七、总结与学习资源推荐

7.1 核心知识点总结

本文围绕鸿蒙 Flutter 地图开发,重点讲解了:

  1. 高德 / 百度地图 SDK 在鸿蒙 Flutter 应用中的集成步骤(权限配置、SDK 初始化、基础地图展示);
  2. POI 搜索与标注功能实现(API 调用、结果展示、搜索优化);
  3. 自定义轨迹图层绘制(轨迹数据模型、动态绘制、动画效果、性能优化);
  4. 完整项目整合与常见问题排查。

通过本文的实战代码,开发者可以快速落地地图相关功能,并根据实际需求扩展离线地图、地图测距、地理编码等进阶功能。

7.2 学习资源推荐

7.3 后续学习方向

  1. 离线地图功能集成(支持离线瓦片下载、离线 POI 搜索);
  2. 地图交互高级功能(测距、测面积、地理围栏);
  3. 多地图平台适配(封装统一接口,支持高德 / 百度 / 腾讯地图动态切换);
  4. 地图与鸿蒙原生功能结合(如鸿蒙位置服务、导航服务)。
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐