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;
}
}
代码解析
-
导入必要的包
import 'package:dynamic_map/dynamic_map.dart'; import 'package:flutter/material.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, ); } }, ), ], ), ); }
-
定义
_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')); } }
-
定义
_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, ), ); }) ], ); } }
-
定义
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
更多关于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.xml
和 Info.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. 处理地图事件
你可以通过 onMapCreated
、onCameraMove
等回调来处理地图事件。
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}');
},
);