如何在 Flutter 中为 Google Maps 添加自定义标记?一份详尽的实战指南

你是否曾经在开发应用时,觉得默认的红色大头针标记无法完美融入你的精心设计的 UI?或者,你需要在地图上直观地展示不同类型的地点(如餐厅、加油站或公交站),而不仅仅是一个通用的位置点?如果是这样,那么你来对地方了。在今天的文章中,我们将一起深入探索如何在 Flutter 应用中为 Google Maps 实现自定义标记。我们将从环境搭建开始,一步步构建一个功能完整、视觉精美的地图应用,展示如何将普通的图片转化为地图上的图标,并分享一些开发过程中的最佳实践。

通过阅读这篇文章,你将学会以下核心技能:

  • 配置与集成:完整掌握 Flutter 项目与 Google Maps SDK 的配置流程。
  • 资源管理:学会正确地在 Flutter 中加载和使用图片资源。
  • 标记自定义:深入理解如何将自定义图片转化为 Google Maps 可识别的 Marker 对象。
  • 状态管理:掌握如何在地图状态中高效地管理多个标记点。
  • 性能优化:了解处理地图图片资源时的内存管理技巧。

项目准备与环境搭建

在开始编写代码之前,我们需要确保开发环境已经就绪。虽然 Flutter 的开发体验非常流畅,但配置地图功能涉及到 Android 和 iOS 的原生设置,这一步至关重要,不可马虎。

首先,请确保你的电脑上已经安装了 Flutter SDK,并且 Android Studio(或 VS Code)配置好了相应的插件。如果你还不知道如何创建一个新的 Flutter 项目,可以参考 Flutter 官方文档中的“Getting Started”部分,或者直接在你的终端运行以下命令来快速初始化项目:

flutter create custom_maps_app

进入项目目录后,我们将使用 VS Code 或 Android Studio 打开它。接下来,我们需要获取一把“金钥匙”——Google Maps API Key。没有这把钥匙,我们的应用将无法显示地图数据。

获取 Google Maps API 密钥

Google Maps 并不是免费无限使用的,我们需要通过 API Key 来验证我们的请求。获取这个密钥的过程其实非常简单,但需要你拥有一个 Google 账号以及一个 Google Cloud 控制台项目。

  • 前往 Google Cloud Console。
  • 创建一个新项目,或者选择一个现有的项目。
  • 在搜索栏中搜索“Maps SDK for Android”并启用它。
  • 接着,前往“凭据”页面,点击“创建凭据”,选择“API 密钥”。

> 💡 开发者小贴士:在创建 API 密钥时,为了安全起见,建议你对该密钥进行限制。例如,你可以限制该密钥只能被特定的 Android 应用包名调用,或者限制每日的请求额度。这样可以防止密钥被滥用而导致产生意外费用。

创建好密钥后,先把它复制到一个安全的地方,我们马上就要用到它了。

添加依赖项

为了让我们的 Flutter 应用能够控制 Google Maps,我们需要在 INLINECODE4181a026 文件中添加官方提供的 INLINECODE9c75663e 插件。这是一个非常强大且维护良好的插件。

打开你的 INLINECODE806c9715 文件,找到 INLINECODE7898f01d 部分,添加如下代码:

dependencies:
  flutter:
    sdk: flutter
  # 添加这一行
google_maps_flutter: ^2.5.0

这里建议使用最新的稳定版本。保存文件后,别忘了在终端运行 flutter pub get 来将这些依赖拉取到你的项目中。

Android 原生配置

由于地图组件涉及到原生的渲染,我们需要对 Android 项目进行一些必要的调整。请打开 android/app/build.gradle 文件。

1. 设置 SDK 版本

Google Maps SDK 对 Android 的版本有一定要求。请确保你的 INLINECODE1ee27118 至少为 31,INLINECODE466b188f 至少为 20。建议配置如下:

android {
    compileSdkVersion 31 // 确保编译 SDK 版本足够新

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    defaultConfig {
        applicationId "com.example.custom_maps_app"
        minSdkVersion 20 // Maps SDK 要求的最小版本
        targetSdkVersion 31
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }
}

2. 配置 Manifest 权限与 API Key

接下来是至关重要的一步:配置 AndroidManifest.xml。我们需要在这里声明网络权限(因为地图需要加载数据),并填入刚才申请的 API Key。

打开 INLINECODE02bb8d91 文件。在 INLINECODE7bc5b89c 标签内,添加以下 标签:


同时,确保你的应用拥有访问网络的权限。通常在 标签下添加(尽管 AndroidManifest 默认可能有网络权限,但明确检查一下总是好的):


准备自定义图标资源

现在让我们回到 Flutter 的世界。为了让地图看起来与众不同,我们需要准备一些自定义的图标。在真实的业务场景中,这些可能是品牌的 Logo、具体的车型图标或者分类图标。

在你的项目根目录下创建一个 assets/images 文件夹(如果没有的话)。然后,找几张 PNG 格式的透明背景图片放进去。为了让演示更生动,我假设我们正在开发一个交通监控应用,所以我们准备了以下图片:

  • bus.png (公交车)
  • car.png (汽车)
  • travelling.png (旅游)
  • bicycle.png (自行车)
  • food-delivery.png (外卖配送)

> ⚠️ 注意:Flutter 默认不会自动加载资源文件。你必须在 INLINECODEbc21f448 中显式声明它们。请打开 INLINECODE6aec77d7,找到 INLINECODE7407ab15 部分,按如下格式配置 INLINECODE3edb221e:

flutter:
  uses-material-design: true
  assets:
    - assets/images/bus.png
    - assets/images/car.png
    - assets/images/travelling.png
    - assets/images/bicycle.png
    - assets/images/food-delivery.png

核心实现:编写 Dart 代码

配置工作终于完成了,接下来是我们最期待的编码环节。我们将创建两个文件:INLINECODE39b2a53b 作为程序的入口,INLINECODE091d7eb8 作为地图展示的主页面。

#### 1. 配置 main.dart

在这个文件中,我们将设置应用的基本主题,并指定首页为 HomePage。

import ‘package:flutter/material.dart‘;
import ‘package:custom_maps_app/HomePage.dart‘; // 确保包名正确

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: ‘Custom Maps Demo‘,
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.green, // 选用清新的绿色作为主题色
      ),
      home: const HomePage(), // 启动我们创建的地图主页
    );
  }
}

#### 2. 构建地图主页

这是整个应用的核心。我们需要做以下几件事:

  • 加载图片资源并转换为适用于 Google Maps 的字节格式。
  • 定义一组坐标点(模拟真实数据)。
  • 将图片和坐标结合起来,创建标记。
  • 在地图上渲染这些标记。

让我们直接看代码,我会通过注释详细解释每一步的操作:

// 引入必要的库
import ‘dart:async‘;
import ‘dart:typed_data‘; // 用于处理图片字节流
import ‘dart:ui‘ as ui; // 用于图片处理
import ‘package:flutter/material.dart‘;
import ‘package:flutter/services.dart‘; // 用于加载 Asset 图片
import ‘package:google_maps_flutter/google_maps_flutter.dart‘;

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State {
  // 1. 定义地图控制器,用于后续控制地图行为(如移动镜头)
  final Completer _controller = Completer();

  // 2. 设置初始相机位置(例如聚焦在孟买)
  static const CameraPosition _kGoogle = CameraPosition(
    target: LatLng(19.0759837, 72.8776559), // 孟买的坐标
    zoom: 14.0,
  );

  // 存储我们自定义标记的图片字节数据
  final List _markers = [];

  // 存储图片路径的列表,对应不同类型的标记
  final List _images = [
    ‘assets/images/car.png‘,
    ‘assets/images/bus.png‘, 
    ‘assets/images/travelling.png‘, 
    ‘assets/images/bicycle.png‘,
    ‘assets/images/food-delivery.png‘
  ];

  // 模拟的数据源:不同地点的经纬度
  final List _latLen = const [
    LatLng(19.0759837, 72.8776559), // 地点 1
    LatLng(28.679079, 77.069710),   // 地点 2 (新德里)
    LatLng(26.850000, 80.949997),   // 地点 3
    LatLng(24.879999, 74.629997),   // 地点 4
    LatLng(16.166600, 74.830000),   // 地点 5
  ];

  @override
  void initState() {
    super.initState();
    loadImagesAndCreateMarkers(); // 初始化时加载图片并创建标记
  }

  // 3. 核心方法:加载资源图片并创建 Marker 对象
  Future loadImagesAndCreateMarkers() async {
    for (int i = 0; i < _latLen.length; i++) {
      // 获取当前索引对应的图片路径
      // 为了演示效果,我们循环使用图片列表中的图片
      final String imagePath = _images[i % _images.length];
      
      // 从 Asset 中加载图片并转换为字节流
      // 这一步是关键:Google Maps 的 Marker 需要字节流而不是 Widget
      final Uint8List markerIcon = awaitgetBytesFromAsset(imagePath, 100);

      // 创建 Marker 对象
      setState(() {
        _markers.add(
          Marker(
            markerId: MarkerId(i.toString()), // 标记的唯一 ID
            position: _latLen[i],              // 标记的位置
            icon: BitmapDescriptor.fromBytes(markerIcon), // 设置自定义图标
            infoWindow: InfoWindow( // 点击标记时显示的信息窗口
              title: 'This is Marker ${i + 1}',
              snippet: 'Custom Icon',
            ),
          ),
        );
      });
    }
  }

  // 辅助函数:将图片文件转换为 Uint8List(字节流)
  Future getBytesFromAsset(String path, int width) async {
    try {
      // 1. 加载图片数据
      ByteData data = await rootBundle.load(path);
      
      // 2. 使用 instantiateImageCodec 将图片解码为 codec
      ui.Codec codec = await ui.instantiateImageCodec(
        data.buffer.asUint8List(),
        targetWidth: width, // 设置目标图片宽度,避免图片过大占用内存
      );
      
      // 3. 获取第一帧
      ui.FrameInfo fi = await codec.getNextFrame();
      
      // 4. 将图片转换为字节流并返回
      return (await fi.image.toByteData(format: ui.ImageByteFormat.png))!
          .buffer
          .asUint8List();
    } catch (e) {
      print("Error loading image: $e");
      // 如果加载失败,返回一个空的字节流,防止程序崩溃
      // 在实际生产中,你可能希望返回一个默认的占位图
      return Uint8List(0);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Flutter Custom Markers"),
        backgroundColor: Colors.green[700],
      ),
      body: GoogleMap(
        initialCameraPosition: _kGoogle, // 初始位置
        markers: Set.of(_markers), // 将我们创建的标记列表传递给地图
        mapType: MapType.normal,          // 地图类型:普通视图
        onMapCreated: (GoogleMapController controller) {
          _controller.complete(controller); // 地图创建完成后保存控制器
        },
      ),
    );
  }
}

深入理解:为什么这样写?

在上面的代码中,getBytesFromAsset 函数可以说是最关键的一环。你可能会问,为什么不能直接把 Image Widget 放到 Marker 里?

这是因为在 Flutter 中,Widget 是纯 Dart 代码描述的 UI 元素,而 Google Maps 的原生层(无论是 Android 还是 iOS)并不认识 Dart 的 Widget。它们只认识原生的 Bitmap(位图)数据。因此,我们必须先把图片转换成二进制字节流(Uint8List),然后通过 BitmapDescriptor.fromBytes 把它“喂”给原生的地图引擎。这就是所谓的“桥接”过程。

最佳实践与优化建议

作为一名追求卓越的开发者,仅仅让功能跑通是不够的,我们还需要考虑用户体验和性能。

1. 图片尺寸管理

在上面的代码中,你可能注意到了 INLINECODEb6d8b7d4 这个参数。Google Maps 上的 Marker 不需要像屏幕海报那样高清。通常 100px 到 150px 宽度的图片就足够清晰了。直接加载原始高分辨率图片不仅浪费内存加载时间,还会导致地图滚动时出现掉帧。因此,务必在 INLINECODE0beebcf0 中限制图片宽度。

2. 内存复用

如果你的地图上有很多重复的图标(比如 100 个相同的汽车标记),你不需要为每个标记都调用一次 INLINECODEb3c83f13。你可以把这 100 个相同的图片字节流存储在一个变量中,复用同一个 INLINECODEc37c74df。这能极大地降低内存占用,避免应用因内存溢出(OOM)而闪退。

3. 集群标记

如果你有成百上千个标记,把它们全部渲染在地图上会让屏幕变得混乱不堪,甚至无法点击。建议使用标记聚类功能。当标记靠得很近时,自动合并成一个数字图标,点击放大后再展开具体的图标。这通常需要引入额外的插件如 flutter_google_maps_cluster

4. 处理异步加载

在 INLINECODEc5c7138d 中,我们通过 INLINECODE68062a05 更新标记列表。这意味着页面刚打开时,标记可能还没有加载出来,用户会看到一片空白地图。为了提升体验,你可以添加一个 INLINECODEcdb95602(加载圈),等到标记列表不为空时再显示地图,或者使用像 INLINECODE398921e4 这样的工具来管理加载状态。

总结

通过这篇文章,我们一步步实现了从零到一构建自定义 Google Maps 的过程。我们不仅学会了如何配置 API Key 和 Android 环境,还深入理解了 Flutter 与原生 Maps SDK 交互的底层机制——即如何通过字节流传输图片资源。

掌握了自定义标记后,你的地图应用将不再局限于单调的红色大头针。你可以利用它来构建配送追踪系统、旅游打卡应用、甚至是类似 Pokémon GO 的增强现实游戏。

希望这篇指南对你有所帮助。现在,你可以尝试修改代码,加入你自己的图片,或者结合 Geolocator 获取你的实时位置并在地图上标记出来。编码愉快!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。如需转载,请注明文章出处豆丁博客和来源网址。https://shluqu.cn/49565.html
点赞
0.00 平均评分 (0% 分数) - 0