Flutter动态地图展示插件dynamic_map的使用

Flutter 动态地图展示插件 dynamic_map 的使用

在本教程中,我们将详细介绍如何在 Flutter 应用程序中使用 dynamic_map 插件来展示动态地图。该插件可以帮助你在应用中添加互动式地图,并且可以轻松地添加标记、多边形和折线等。

安装 dynamic_map 插件

首先,你需要在项目的 pubspec.yaml 文件中添加 dynamic_map 插件依赖项:

dependencies:
  flutter:
    sdk: flutter
  dynamic_map: ^版本号

然后运行 flutter pub get 命令来安装插件。

创建一个基本的地图应用

下面是一个完整的示例代码,展示了如何使用 dynamic_map 插件来创建一个具有动态标记和折线的地图应用。

示例代码

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

import 'toggleable_marker/toggleable_marker.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: 'Map Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
  late List<ToggleMarkerWrapper> toggleMarkers;

  late StatefulPolylineController<ORSModel> polylineController;

  late final AnimatedMapController animatedMapController;

  void _centerOnPoints() {
    animatedMapController.animatedFitCamera(
      cameraFit: CameraFit.coordinates(
        padding: const EdgeInsets.all(64),
        coordinates: toggleMarkers
                .map((e) => e.animatedMarkerController.currentPosition)
                .toList() +
            (polylineController.currentPolylinePoints ?? []),
      ),
    );
  }

  @override
  void initState() {
    animatedMapController = AnimatedMapController(vsync: this);
    toggleMarkers = [
      ToggleMarkerWrapper(
        initialPosition: const LatLng(38.93132871414923, -77.07046226186635),
      ),
      ToggleMarkerWrapper(
          initialPosition:
              const LatLng(38.930236019389184, -77.05503322891566)),
      ToggleMarkerWrapper(
          initialPosition: const LatLng(38.94030046698981, -77.03774754300335)),
    ];
    polylineController = StatefulPolylineController(
      navigationHandler: defaultORSNavigationHandler(
          '5b3ce3597851110001cf6248a1a1addc948a48c0b5adf2c4d07589e4'),
    );
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: DynamicMap(
        mapOptions: const MapOptions(
            initialCenter: LatLng(38.930236019389184, -77.05503322891566),
            maxZoom: 18.5),
        markers: List.generate(
          toggleMarkers.length,
          (index) => DynamicMapMarker(
            controller: toggleMarkers[index].animatedMarkerController,
            builder: (context) => ToggleableMarker(
                nameVisibilityNotifier:
                    toggleMarkers[index].nameVisibilityNotifier,
                markerIndex: index),
            width: 200,
            height: 100,
          ),
        ),
        animatedMapController: animatedMapController,
        polylines: [
          StatefulPolyline(
            statefulPolylineController: polylineController,
            initialRoutingPoints: toggleMarkers
                .map((e) => e.animatedMarkerController.currentPosition)
                .toList(),
          ),
        ],
      ),
      floatingActionButton: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.end,
        children: [
          FloatingActionButton.extended(
              onPressed: () async {
                toggleMarkers[0]
                    .animatedMarkerController
                    .moveTo(animatedMapController.mapController.camera.center);
                _centerOnPoints();

                await polylineController.updateRouteFromAPI([
                  toggleMarkers[0].animatedMarkerController.currentPosition,
                  toggleMarkers[1].animatedMarkerController.currentPosition,
                  toggleMarkers[2].animatedMarkerController.currentPosition
                ]);

                _centerOnPoints();
              },
              label: const Text('Center 1')),
          const SizedBox(
            height: 8,
          ),
          FloatingActionButton.extended(
              onPressed: () async {
                toggleMarkers[1]
                    .animatedMarkerController
                    .moveTo(animatedMapController.mapController.camera.center);
                _centerOnPoints();

                await polylineController.updateRouteFromAPI([
                  toggleMarkers[0].animatedMarkerController.currentPosition,
                  toggleMarkers[1].animatedMarkerController.currentPosition,
                  toggleMarkers[2].animatedMarkerController.currentPosition
                ]);
                _centerOnPoints();
              },
              label: const Text('Center 2')),
          const SizedBox(
            height: 8,
          ),
          FloatingActionButton.extended(
              onPressed: () async {
                toggleMarkers[2].animatedMarkerController.moveTo(
                      animatedMapController.mapController.camera.center,
                    );
                _centerOnPoints();

                await polylineController.updateRouteFromAPI([
                  toggleMarkers[0].animatedMarkerController.currentPosition,
                  toggleMarkers[1].animatedMarkerController.currentPosition,
                  toggleMarkers[2].animatedMarkerController.currentPosition,
                ]);
                _centerOnPoints();
              },
              label: const Text('Center 3')),
          const SizedBox(
            height: 8,
          ),
          FloatingActionButton.extended(
              onPressed: () async {
                for (final element in toggleMarkers) {
                  element.changeTooltipVisibility();
                }
              },
              label: const Text('Labels')),
          const SizedBox(
            height: 8,
          ),
          FloatingActionButton.extended(
              onPressed: () async {
                _centerOnPoints();
              },
              label: const Text('Center Map')),
          const SizedBox(
            height: 8,
          ),
          StreamBuilder<ORSModel?>(
            stream: polylineController.dataStream,
            builder: (context, snapshot) {
              if (snapshot.hasData) {
                return _RoutingFAB(
                  data: snapshot.data!,
                  key: ValueKey(snapshot.data!),
                );
              }
              if (snapshot.hasError) {
                return FloatingActionButton.extended(
                  onPressed: () {
                    showDialog(
                        context: context,
                        builder: (_) => AlertDialog(
                              title: const Text('Error'),
                              content: Text(snapshot.error.toString()),
                              actions: [
                                TextButton(
                                  child: const Text('Close'),
                                  onPressed: () {
                                    Navigator.of(context).pop();
                                  },
                                ),
                              ],
                            ));
                  },
                  label: const Text('Routing Error'),
                  backgroundColor: Colors.red,
                );
              } else {
                return Container(
                  width: 0,
                );
              }
            },
          ),
        ],
      ),
    );
  }
}

class _RoutingFAB extends StatefulWidget {
  const _RoutingFAB({Key? key, required this.data}) : super(key: key);
  final ORSModel data;

  @override
  State<_RoutingFAB> createState() => _RoutingFABState();
}

class _RoutingFABState extends State<_RoutingFAB>
    with TickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<Color?> animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 750),
      vsync: this,
    );
    animation = ColorTween(
      begin: Colors.green,
      end: Colors.blue,
    ).animate(_controller)
      ..addListener(() {
        setState(() {});
      });
    _controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return FloatingActionButton.extended(
        backgroundColor: animation.value,
        onPressed: () async {
          showModalBottomSheet(
              context: context,
              isScrollControlled: true,
              shape: const RoundedRectangleBorder(
                  borderRadius: BorderRadius.only(
                      topLeft: Radius.circular(15),
                      topRight: Radius.circular(15))),
              builder: (_) {
                final duration = Duration(
                    seconds: widget.data.routes!.first.summary!.duration! ~/ 1);
                return SingleChildScrollView(
                  padding: const EdgeInsets.all(16),
                  child: Column(
                    children: [
                      Padding(
                        padding: const EdgeInsets.all(8.0),
                        child: Text(
                          '${duration.inHours}h ${duration.inMinutes % 60}m ETA',
                          style: const TextStyle(
                              fontWeight: FontWeight.bold, fontSize: 26),
                        ),
                      ),
                      const Divider(
                        height: 16,
                      ),
                      ...List.generate(
                          widget.data.routes!.first.segments!.length, (index) {
                        final segment =
                            widget.data.routes!.first.segments![index];
                        return _Segment(segment: segment);
                      })
                    ],
                  ),
                );
              });
        },
        label: const Text('Routing Info'));
  }
}

class _Segment extends StatelessWidget {
  const _Segment({Key? key, required this.segment}) : super(key: key);
  final Segments segment;

  @override
  Widget build(BuildContext context) {
    final duration = Duration(seconds: segment.duration! ~/ 1);
    return Column(
      children: [
        const Divider(
          thickness: 2,
          height: 16,
        ),
        Text(
          '${duration.inHours}h ${duration.inMinutes % 60}m ETA, ${segment.distance} meters',
          style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
        ),
        const Divider(
          thickness: 1,
        ),
        ...List.generate(segment.steps!.length, (index) {
          final item = segment.steps![index];
          return ListTile(
            title: Text(item.instruction!),
            subtitle: Text(item.name!),
            leading: Text(
              '${item.distance}\nmeters',
              textAlign: TextAlign.center,
            ),
            trailing: Text(
              '${((item.duration ?? 0) / 60).toStringAsFixed(2)}\nminutes',
              textAlign: TextAlign.center,
            ),
          );
        })
      ],
    );
  }
}

class ToggleMarkerWrapper {
  ToggleMarkerWrapper({required LatLng initialPosition})
      : animatedMarkerController =
            AnimatedMarkerController(initialPosition: initialPosition);
  final AnimatedMarkerController animatedMarkerController;
  final ValueNotifier<bool> nameVisibilityNotifier = ValueNotifier<bool>(false);

  bool changeTooltipVisibility() {
    return nameVisibilityNotifier.value = !nameVisibilityNotifier.value;
  }
}

代码解析

  1. 导入必要的包

    import 'package:dynamic_map/dynamic_map.dart';
    import 'package:flutter/material.dart';
    
  2. 定义主应用类

    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Map Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: const MyHomePage(title: 'Home Page'),
        );
      }
    }
    
  3. 定义主页面状态类

    class MyHomePage extends StatefulWidget {
      const MyHomePage({Key? key, required this.title}) : super(key: key);
    
      final String title;
    
      @override
      State<MyHomePage> createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
      late List<ToggleMarkerWrapper> toggleMarkers;
    
      late StatefulPolylineController<ORSModel> polylineController;
    
      late final AnimatedMapController animatedMapController;
    
      void _centerOnPoints() {
        animatedMapController.animatedFitCamera(
          cameraFit: CameraFit.coordinates(
            padding: const EdgeInsets.all(64),
            coordinates: toggleMarkers
                    .map((e) => e.animatedMarkerController.currentPosition)
                    .toList() +
                (polylineController.currentPolylinePoints ?? []),
          ),
        );
      }
    
      @override
      void initState() {
        animatedMapController = AnimatedMapController(vsync: this);
        toggleMarkers = [
          ToggleMarkerWrapper(
            initialPosition: const LatLng(38.93132871414923, -77.07046226186635),
          ),
          ToggleMarkerWrapper(
              initialPosition:
                  const LatLng(38.930236019389184, -77.05503322891566)),
          ToggleMarkerWrapper(
              initialPosition: const LatLng(38.94030046698981, -77.03774754300335)),
        ];
        polylineController = StatefulPolylineController(
          navigationHandler: defaultORSNavigationHandler(
              '5b3ce3597851110001cf6248a1a1addc948a48c0b5adf2c4d07589e4'),
        );
        super.initState();
      }
    
  4. 构建页面内容

    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: DynamicMap(
          mapOptions: const MapOptions(
              initialCenter: LatLng(38.930236019389184, -77.05503322891566),
              maxZoom: 18.5),
          markers: List.generate(
            toggleMarkers.length,
            (index) => DynamicMapMarker(
              controller: toggleMarkers[index].animatedMarkerController,
              builder: (context) => ToggleableMarker(
                  nameVisibilityNotifier:
                      toggleMarkers[index].nameVisibilityNotifier,
                  markerIndex: index),
              width: 200,
              height: 100,
            ),
          ),
          animatedMapController: animatedMapController,
          polylines: [
            StatefulPolyline(
              statefulPolylineController: polylineController,
              initialRoutingPoints: toggleMarkers
                  .map((e) => e.animatedMarkerController.currentPosition)
                  .toList(),
            ),
          ],
        ),
        floatingActionButton: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.end,
          children: [
            FloatingActionButton.extended(
                onPressed: () async {
                  toggleMarkers[0]
                      .animatedMarkerController
                      .moveTo(animatedMapController.mapController.camera.center);
                  _centerOnPoints();
    
                  await polylineController.updateRouteFromAPI([
                    toggleMarkers[0].animatedMarkerController.currentPosition,
                    toggleMarkers[1].animatedMarkerController.currentPosition,
                    toggleMarkers[2].animatedMarkerController.currentPosition
                  ]);
    
                  _centerOnPoints();
                },
                label: const Text('Center 1')),
            const SizedBox(
              height: 8,
            ),
            FloatingActionButton.extended(
                onPressed: () async {
                  toggleMarkers[1]
                      .animatedMarkerController
                      .moveTo(animatedMapController.mapController.camera.center);
                  _centerOnPoints();
    
                  await polylineController.updateRouteFromAPI([
                    toggleMarkers[0].animatedMarkerController.currentPosition,
                    toggleMarkers[1].animatedMarkerController.currentPosition,
                    toggleMarkers[2].animatedMarkerController.currentPosition
                  ]);
                  _centerOnPoints();
                },
                label: const Text('Center 2')),
            const SizedBox(
              height: 8,
            ),
            FloatingActionButton.extended(
                onPressed: () async {
                  toggleMarkers[2].animatedMarkerController.moveTo(
                        animatedMapController.mapController.camera.center,
                      );
                  _centerOnPoints();
    
                  await polylineController.updateRouteFromAPI([
                    toggleMarkers[0].animatedMarkerController.currentPosition,
                    toggleMarkers[1].animatedMarkerController.currentPosition,
                    toggleMarkers[2].animatedMarkerController.currentPosition,
                  ]);
                  _centerOnPoints();
                },
                label: const Text('Center 3')),
            const SizedBox(
              height: 8,
            ),
            FloatingActionButton.extended(
                onPressed: () async {
                  for (final element in toggleMarkers) {
                    element.changeTooltipVisibility();
                  }
                },
                label: const Text('Labels')),
            const SizedBox(
              height: 8,
            ),
            FloatingActionButton.extended(
                onPressed: () async {
                  _centerOnPoints();
                },
                label: const Text('Center Map')),
            const SizedBox(
              height: 8,
            ),
            StreamBuilder<ORSModel?>(
              stream: polylineController.dataStream,
              builder: (context, snapshot) {
                if (snapshot.hasData) {
                  return _RoutingFAB(
                    data: snapshot.data!,
                    key: ValueKey(snapshot.data!),
                  );
                }
                if (snapshot.hasError) {
                  return FloatingActionButton.extended(
                    onPressed: () {
                      showDialog(
                          context: context,
                          builder: (_) => AlertDialog(
                                title: const Text('Error'),
                                content: Text(snapshot.error.toString()),
                                actions: [
                                  TextButton(
                                    child: const Text('Close'),
                                    onPressed: () {
                                      Navigator.of(context).pop();
                                    },
                                  ),
                                ],
                              ));
                    },
                    label: const Text('Routing Error'),
                    backgroundColor: Colors.red,
                  );
                } else {
                  return Container(
                    width: 0,
                  );
                }
              },
            ),
          ],
        ),
      );
    }
    
  5. 定义 _RoutingFAB

    class _RoutingFAB extends StatefulWidget {
      const _RoutingFAB({Key? key, required this.data}) : super(key: key);
      final ORSModel data;
    
      @override
      State<_RoutingFAB> createState() => _RoutingFABState();
    }
    
    class _RoutingFABState extends State<_RoutingFAB>
        with TickerProviderStateMixin {
      late AnimationController _controller;
      late Animation<Color?> animation;
    
      @override
      void initState() {
        super.initState();
        _controller = AnimationController(
          duration: const Duration(milliseconds: 750),
          vsync: this,
        );
        animation = ColorTween(
          begin: Colors.green,
          end: Colors.blue,
        ).animate(_controller)
          ..addListener(() {
            setState(() {});
          });
        _controller.forward();
      }
    
      @override
      Widget build(BuildContext context) {
        return FloatingActionButton.extended(
            backgroundColor: animation.value,
            onPressed: () async {
              showModalBottomSheet(
                  context: context,
                  isScrollControlled: true,
                  shape: const RoundedRectangleBorder(
                      borderRadius: BorderRadius.only(
                          topLeft: Radius.circular(15),
                          topRight: Radius.circular(15))),
                  builder: (_) {
                    final duration = Duration(
                        seconds: widget.data.routes!.first.summary!.duration! ~/ 1);
                    return SingleChildScrollView(
                      padding: const EdgeInsets.all(16),
                      child: Column(
                        children: [
                          Padding(
                            padding: const EdgeInsets.all(8.0),
                            child: Text(
                              '${duration.inHours}h ${duration.inMinutes % 60}m ETA',
                              style: const TextStyle(
                                  fontWeight: FontWeight.bold, fontSize: 26),
                            ),
                          ),
                          const Divider(
                            height: 16,
                          ),
                          ...List.generate(
                              widget.data.routes!.first.segments!.length, (index) {
                            final segment =
                                widget.data.routes!.first.segments![index];
                            return _Segment(segment: segment);
                          })
                        ],
                      ),
                    );
                  });
            },
            label: const Text('Routing Info'));
      }
    }
    
  6. 定义 _Segment

    class _Segment extends StatelessWidget {
      const _Segment({Key? key, required this.segment}) : super(key: key);
      final Segments segment;
    
      @override
      Widget build(BuildContext context) {
        final duration = Duration(seconds: segment.duration! ~/ 1);
        return Column(
          children: [
            const Divider(
              thickness: 2,
              height: 16,
            ),
            Text(
              '${duration.inHours}h ${duration.inMinutes % 60}m ETA, ${segment.distance} meters',
              style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
            ),
            const Divider(
              thickness: 1,
            ),
            ...List.generate(segment.steps!.length, (index) {
              final item = segment.steps![index];
              return ListTile(
                title: Text(item.instruction!),
                subtitle: Text(item.name!),
                leading: Text(
                  '${item.distance}\nmeters',
                  textAlign: TextAlign.center,
                ),
                trailing: Text(
                  '${((item.duration ?? 0) / 60).toStringAsFixed(2)}\nminutes',
                  textAlign: TextAlign.center,
                ),
              );
            })
          ],
        );
      }
    }
    
  7. 定义 ToggleMarkerWrapper

    class ToggleMarkerWrapper {
      ToggleMarkerWrapper({required LatLng initialPosition})
          : animatedMarkerController =
              AnimatedMarkerController(initialPosition: initialPosition);
      final AnimatedMarkerController animatedMarkerController;
      final ValueNotifier<bool> nameVisibilityNotifier = ValueNotifier<bool>(false);
    
      bool changeTooltipVisibility() {
        return nameVisibilityNotifier.value = !nameVisibilityNotifier.value;
      }
    }
    

更多关于Flutter动态地图展示插件dynamic_map的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter动态地图展示插件dynamic_map的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


在Flutter中,如果你想要展示动态地图,可以使用 dynamic_map 插件。dynamic_map 是一个用于在 Flutter 应用中展示动态地图的插件,它支持各种地图服务,如 Google Maps、Mapbox 等。

1. 添加依赖

首先,你需要在 pubspec.yaml 文件中添加 dynamic_map 插件的依赖:

dependencies:
  flutter:
    sdk: flutter
  dynamic_map: ^1.0.0  # 请使用最新版本

然后运行 flutter pub get 来安装依赖。

2. 配置地图服务

在使用 dynamic_map 之前,你需要配置你想要使用的地图服务。例如,如果你使用 Google Maps,你需要在 AndroidManifest.xmlInfo.plist 中添加相应的 API 密钥。

Android (android/app/src/main/AndroidManifest.xml):

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.yourapp">
    <application
        ...>
        <meta-data
            android:name="com.google.android.geo.API_KEY"
            android:value="YOUR_GOOGLE_MAPS_API_KEY"/>
    </application>
</manifest>

iOS (ios/Runner/Info.plist):

<key>io.flutter.embedded_views_preview</key>
<true/>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app uses your location to show it on the map.</string>
<key>com.google.maps.api_key</key>
<string>YOUR_GOOGLE_MAPS_API_KEY</string>

3. 使用 DynamicMap 插件

在 Flutter 应用中使用 DynamicMap 插件来展示动态地图。以下是一个简单的示例:

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

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

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

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

class _MapScreenState extends State<MapScreen> {
  late DynamicMapController _mapController;

  @override
  void initState() {
    super.initState();
    _mapController = DynamicMapController();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Dynamic Map'),
      ),
      body: DynamicMap(
        controller: _mapController,
        initialCameraPosition: CameraPosition(
          target: LatLng(37.7749, -122.4194), // San Francisco
          zoom: 12,
        ),
        onMapCreated: (controller) {
          // 地图创建完成后的回调
          print('Map created');
        },
      ),
    );
  }
}

4. 控制地图

你可以使用 DynamicMapController 来控制地图的行为,例如移动相机、添加标记等。

_mapController.animateCamera(
  CameraUpdate.newLatLng(LatLng(34.0522, -118.2437)), // Los Angeles
);

_mapController.addMarker(
  MarkerOptions(
    position: LatLng(37.7749, -122.4194), // San Francisco
    title: 'San Francisco',
  ),
);

5. 处理地图事件

你可以通过 onMapCreatedonCameraMove 等回调来处理地图事件。

DynamicMap(
  controller: _mapController,
  initialCameraPosition: CameraPosition(
    target: LatLng(37.7749, -122.4194),
    zoom: 12,
  ),
  onMapCreated: (controller) {
    print('Map created');
  },
  onCameraMove: (CameraPosition position) {
    print('Camera moved to: ${position.target}');
  },
);
回到顶部