Flutter地图标记增强插件google_maps_marker_widgets的使用

发布于 1周前 作者 vueper 来自 Flutter

Flutter 地图标记增强插件 google_maps_marker_widgets 的使用

Google Maps Marker Widgets

google_maps_marker_widgets 是一个 Flutter 包,用于在 Google 地图上使用任意小部件作为标记。

特性

  • 可以使用 任何 小部件作为 [Marker] 显示在 [GoogleMap] 上。
  • 更新标记位置时具有平滑动画(无瞬移)。
  • 可自定义的 [LocationPuck] 小部件,允许手动控制位置更新和外观。

截图

动画标记小部件 自定义位置指针
animated_markers location_puck

开始使用

安装和设置 google_maps_flutter

如果你还没有安装和设置 google_maps_flutter,请先进行安装:

flutter pub add google_maps_flutter

添加权限

iOS

Info.plist 文件中添加权限以访问用户的地理位置。

对于仅在使用时访问:

<key>NSLocationWhenInUseUsageDescription</key>
<string>允许此应用在使用时访问您的位置?</string>

对于在后台也监控位置数据的应用:

<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>此应用在任何时候跟踪您的位置会表现得更好。</string>

Android

打开 app/src/main/AndroidManifest.xml 并添加以下权限之一:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

添加 geolocatorflutter_compass(可选)

flutter pub add geolocator
flutter pub add flutter_compass

使用方法

google_maps_marker_widgets 有三个主要组件:

1. [MarkerWidget] - 在 [GoogleMap] 中为 [Marker] 提供视觉内容的小部件

final treeMarkerId = MarkerId('treeMarker');
final treeMarkerWidget = MarkerWidget(
  markerId: treeMarkerId,
  child: Icon(Icons.park, color: Colors.green, size: 45),
);

2. [MarkerWidgetsController] - 管理 [MarkerWidget] 及其关联的 [Marker][GoogleMap]

你可以使用 markerWidgetsController 来添加、删除和更新标记。

创建控制器并添加标记

final markerWidgetsController = MarkerWidgetsController();
final treeMarker = Marker(
  markerId: treeMarkerId,
  anchor: Offset(0.5, 0.5),
  position: LatLng(37, -108)
);
markerWidgetsController.addMarkerWidget(
  markerWidget: treeMarkerWidget,
  marker: treeMarker
);

注意 [Marker.anchor] 被设置为 Offset(0.5, 0.5)。这确保了标记在位置上居中。默认情况下,来自 [GoogleMap] 的锚点是 Offset(0.5, 1.0)

更新标记的位置

final treeMarker = markerWidgetsController.markerForId(treeMarkerId)!;
final newPosition = LatLng(33, -105);
final updatedMarker = treeMarker.copyWith(positionParam: newPosition);
markerWidgetsController.updateMarker(marker: updatedMarker);

当更新标记时,默认情况下会使用动画。你可以通过传递 animated: false 来禁用此行为。

3. [MarkerWidgets] - 用于 [GoogleMap] 的包装小部件

[MarkerWidgets] 是使用 google_maps_marker_widgets 的主要入口点。

[MarkerWidgetsController] 传递给 [MarkerWidgets],并在 [MarkerWidgets.builder] 方法中创建你的 [GoogleMap],传入提供的标记集合。

MarkerWidgets(
  markerWidgetsController: _markerWidgetsController,
  builder: (context, markers) => GoogleMap(
    initialCameraPosition: CameraPosition(target: LatLng(41.8, -99.65), zoom: 4),
    markers: markers,
  ),
)

完整示例

import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_compass/flutter_compass.dart';
import 'package:geolocator/geolocator.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:google_maps_marker_widgets/google_maps_marker_widgets.dart';

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

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

  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Google Maps Marker Widgets Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.lightGreen),
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

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

  [@override](/user/override)
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final MarkerWidgetsController _markerWidgetsController = MarkerWidgetsController();
  StreamSubscription<Position>? _deviceLocationStream;
  StreamSubscription<CompassEvent>? _compassEventsStream;
  CameraPosition? _cameraPosition;
  GoogleMapController? _mapController;

  [@override](/user/override)
  void initState() {
    _addSampleWidgetMarkers();
    super.initState();
  }

  [@override](/user/override)
  void dispose() {
    _deviceLocationStream?.cancel();
    _compassEventsStream?.cancel();
    super.dispose();
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      body: MarkerWidgets(
        markerWidgetsController: _markerWidgetsController,
        builder: (context, markers) => GoogleMap(
          onMapCreated: _onMapCreated,
          onCameraMove: _onCameraMove,
          initialCameraPosition: CameraPosition(target: LatLng(41.8, -99.65), zoom: 4),
          markers: markers,
          myLocationButtonEnabled: false,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _moveMarkers,
        child: Icon(Icons.play_arrow_rounded),
      ),
    );
  }

  void _onMapCreated(GoogleMapController mapController) async {
    _mapController = mapController;
    try {
      // 检查权限并开始流式位置数据
      await _startLocationDataStreaming();
      final currentLocation = await Geolocator.getCurrentPosition();
      final newLatLng = LatLng(currentLocation.latitude, currentLocation.longitude);
      final cameraUpdate = CameraUpdate.newLatLng(newLatLng);
      await _mapController?.animateCamera(cameraUpdate);
    } catch (error) {
      // TODO: 处理错误
    }
  }

  void _onCameraMove(CameraPosition cameraPosition) {
    _cameraPosition = cameraPosition;
  }

  void _addSampleWidgetMarkers() {
    // 添加一些示例小部件以演示动画
    // 注意锚点 / Offset 设置为 0.5, 0.5
    // 这会将您的小部件居中在所提供的位置上

    final treeMarkerId = MarkerId('treeMarker');
    _markerWidgetsController.addMarkerWidget(
      markerWidget: MarkerWidget(
        markerId: treeMarkerId,
        child: Icon(Icons.park, color: Colors.green, size: 45),
      ),
      marker: Marker(
        markerId: treeMarkerId,
        anchor: Offset(0.5, 0.5),
        position: LatLng(37, -108),
      ),
    );
    final smileyMarkerId = MarkerId('smileyMarker');
    _markerWidgetsController.addMarkerWidget(
      markerWidget: MarkerWidget(
        markerId: smileyMarkerId,
        child: Text(
          '😀',
          style: TextStyle(fontSize: 40),
        ),
      ),
      marker: Marker(
        markerId: smileyMarkerId,
        position: LatLng(37, -101),
        anchor: Offset(0.5, 0.5),
      ),
    );
    final flightMarkerId = MarkerId('flightMarker');
    _markerWidgetsController.addMarkerWidget(
      markerWidget: MarkerWidget(
        markerId: flightMarkerId,
        child: CircleAvatar(
          backgroundColor: Colors.blue,
          child: Icon(
            Icons.flight,
            color: Colors.white,
          ),
        ),
      ),
      marker: Marker(
        markerId: flightMarkerId,
        anchor: Offset(0.5, 0.5),
        position: LatLng(37, -93.65),
      ),
    );
  }

  // 将标记移动到新随机位置
  void _moveMarkers() {
    final markers = _markerWidgetsController.markers.value;
    for (var marker in markers) {
      if (marker.markerId != MarkerId('device')) {
        final newLat = 41.8 + Random().nextDouble() * 10 * (Random().nextBool() ? -1 : 1);
        final newLng = -99 + Random().nextDouble() * 10 * (Random().nextBool() ? -1 : 1);
        final newMarker = marker.copyWith(positionParam: LatLng(newLat, newLng));
        _markerWidgetsController.updateMarker(newMarker);
      }
    }
  }

  Future<void> _startLocationDataStreaming() async {
    bool serviceEnabled;
    LocationPermission permission;

    // 测试位置服务是否启用
    serviceEnabled = await Geolocator.isLocationServiceEnabled();
    if (!serviceEnabled) {
      // 位置服务未启用,不要继续访问位置并请求用户启用位置服务
      return Future.error('位置服务已禁用。');
    }

    permission = await Geolocator.checkPermission();
    if (permission == LocationPermission.denied) {
      permission = await Geolocator.requestPermission();
      if (permission == LocationPermission.denied) {
        // 权限被拒绝,下次可以再次请求权限(这是 Android 的 shouldShowRequestPermissionRationale 返回 true 的情况。根据 Android 指南,您的应用此时应显示解释性 UI)
        return Future.error('位置权限被拒绝');
      }
    }

    if (permission == LocationPermission.deniedForever) {
      // 权限被永久拒绝,处理这种情况
      return Future.error('位置权限被永久拒绝,我们无法请求权限。');
    }

    // 开始流式当前位置
    _deviceLocationStream = Geolocator.getPositionStream().listen(_onDeviceLocationUpdate);
    _compassEventsStream = FlutterCompass.events?.listen(_onCompassEvent);
  }

  void _onCompassEvent(CompassEvent compassEvent) {
    final currentHeading = compassEvent.heading;
    final marker = _markerWidgetsController.markerForId(MarkerId('device'));
    if (marker != null) {
      double rotationParam = (currentHeading ?? 0) - (_cameraPosition?.bearing ?? 0);
      final newMarker = marker.copyWith(rotationParam: rotationParam);
      _markerWidgetsController.updateMarker(newMarker, animated: false);
    }
  }

  void _onDeviceLocationUpdate(Position position) {
    final newLatLng = LatLng(position.latitude, position.longitude);
    final deviceMaker = _markerWidgetsController.markerForId(MarkerId('device'));
    if (deviceMaker == null) {
      // 添加设备标记到地图
      _markerWidgetsController.addDeviceLocationPuck(
        initialPosition: newLatLng,
        locationPuck: LocationPuck(showHeading: true),
      );
    } else {
      final positionParam = LatLng(position.latitude, position.longitude);
      final newDeviceMarker = deviceMaker.copyWith(positionParam: positionParam);
      _markerWidgetsController.updateMarker(newDeviceMarker);
    }
  }
}

更多关于Flutter地图标记增强插件google_maps_marker_widgets的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter地图标记增强插件google_maps_marker_widgets的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是一个关于如何使用 google_maps_marker_widgets 插件在 Flutter 中增强地图标记的示例代码。这个插件提供了一些高级功能,比如自定义标记、标记集群等。

首先,确保在你的 pubspec.yaml 文件中添加依赖项:

dependencies:
  flutter:
    sdk: flutter
  google_maps_flutter: ^2.1.1  # 确保使用兼容的版本
  google_maps_marker_widgets: ^2.0.0  # 确保使用兼容的版本

然后运行 flutter pub get 来获取依赖项。

接下来是一个完整的 Flutter 应用示例,展示了如何使用 google_maps_marker_widgets 插件:

import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:google_maps_marker_widgets/google_maps_marker_widgets.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Google Maps Marker Widgets Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MapScreen(),
    );
  }
}

class MapScreen extends StatefulWidget {
  @override
  _MapScreenState createState() => _MapScreenState();
}

class _MapScreenState extends State<MapScreen> {
  final Completer<GoogleMapController> _controller = Completer();
  Set<Marker> _markers = Set<Marker>();
  Set<MarkerWithInfoWindow> _customMarkers = Set<MarkerWithInfoWindow>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Google Maps Marker Widgets Demo'),
      ),
      body: GoogleMap(
        mapType: MapType.hybrid,
        initialCameraPosition: CameraPosition(
          target: LatLng(37.7749, -122.4194),
          zoom: 11.0,
        ),
        markers: Set<Marker>.of(_markers),
        onMapCreated: (GoogleMapController controller) {
          _controller.complete(controller);
          _addMarkers();
        },
        myLocationEnabled: true,
        zoomGesturesEnabled: true,
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _showMarkerInfoWindow();
        },
        tooltip: 'Show Marker Info Window',
        child: Icon(Icons.info),
      ),
    );
  }

  void _addMarkers() {
    setState(() {
      _markers.add(Marker(
        markerId: MarkerId('marker1'),
        position: LatLng(37.7749, -122.4194),
        infoWindow: InfoWindow(title: 'Marker 1', snippet: 'San Francisco'),
      ));

      // Using google_maps_marker_widgets for custom markers
      _customMarkers.add(MarkerWithInfoWindow(
        markerId: MarkerId('custom_marker1'),
        position: LatLng(37.7649, -122.4394),
        marker: Marker(
          icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueAzure),
        ),
        infoWindow: InfoWindow(
          title: 'Custom Marker',
          snippet: 'This is a custom marker with google_maps_marker_widgets',
        ),
        infoWindowBuilder: (context, markerId, onTap) {
          return Container(
            height: 200,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Text('Custom Info Window'),
                ElevatedButton(
                  onPressed: () => onTap(),
                  child: Text('Close'),
                ),
              ],
            ),
          );
        },
      ));

      // Note: google_maps_marker_widgets does not directly modify the markers set of GoogleMap,
      // so you need to manage the markers manually and use MarkerLayer to render custom markers.
      // Below is a conceptual example of how you might integrate MarkerLayer if needed.
      // However, for simplicity, we'll just keep the markers in _customMarkers and handle info windows separately.
    });
  }

  void _showMarkerInfoWindow() {
    _controller.future.then((controller) {
      if (_customMarkers.isNotEmpty) {
        final MarkerWithInfoWindow marker = _customMarkers.first;
        controller.showMarkerInfoWindow(marker.markerId);
      }
    });
  }
}

注意事项

  1. MarkerLayer 的使用

    • 在上面的代码中,我们没有直接使用 MarkerLayer,因为 google_maps_marker_widgets 插件主要提供的是自定义信息窗口的功能。
    • 如果你需要更复杂的自定义标记(例如带有动画或复杂布局的标记),你可能需要结合 MarkerLayerStack 小部件来自行管理这些标记的渲染。
  2. InfoWindowBuilder

    • MarkerWithInfoWindow 类允许你使用 infoWindowBuilder 来自定义信息窗口。
    • 在上面的例子中,我们定义了一个简单的自定义信息窗口,包含一个文本和一个关闭按钮。
  3. 错误处理

    • 在实际应用中,添加错误处理逻辑来处理例如地图加载失败、标记点击事件等情况。
  4. 依赖版本

    • 确保 google_maps_fluttergoogle_maps_marker_widgets 插件的版本是兼容的。

这个示例展示了如何使用 google_maps_marker_widgets 插件在 Flutter 应用中增强地图标记的功能。希望这对你有所帮助!

回到顶部