Flutter实现定位、距离时长计算及Google Map折线绘制
Flutter 地图位置功能实现
前置依赖配置
首先在项目pubspec.yaml中添加计算距离需要的依赖,同时确认已引入现有代码里使用的google_maps_flutter、location、cloud_firestore依赖:
dependencies: geolocator: ^10.1.0 # 其余原有依赖保持不变
在对应文件头部引入数学计算库:
import 'dart:math';
同时在Android、iOS原生配置文件中开启位置权限,否则无法获取用户当前位置。
核心代码修改
替换原有MyMap对应的State类代码,新增逻辑会覆盖全部需求:获取当前位置、读取Firebase最新公交位置、计算距离与预计时长、绘制路线折线。
新增状态变量
在_MyMapState类中新增以下状态字段,存储位置、计算结果、地图元素:
final loc.Location location = loc.Location(); late GoogleMapController _controller; bool _added = false; // 新增字段 LatLng? _currentUserLocation; LatLng? _targetBusLocation; double _remainingDistance = 0.0; // 单位:公里 int _estimatedArrivalMinutes = 0; // 单位:分钟 Set<Polyline> _routeLines = {}; Set<Marker> _mapMarkers = {};
新增获取当前位置方法
在initState生命周期中调用该方法,申请权限后拉取用户实时位置:
@override void initState() { super.initState(); _fetchCurrentLocation(); } Future<void> _fetchCurrentLocation() async { bool serviceEnabled; loc.PermissionStatus permissionStatus; serviceEnabled = await location.serviceEnabled(); if (!serviceEnabled) { serviceEnabled = await location.requestService(); if (!serviceEnabled) return; } permissionStatus = await location.hasPermission(); if (permissionStatus == loc.PermissionStatus.denied) { permissionStatus = await location.requestPermission(); if (permissionStatus != loc.PermissionStatus.granted) return; } final userLocData = await location.getLocation(); setState(() { _currentUserLocation = LatLng(userLocData.latitude!, userLocData.longitude!); }); _updateRouteAndMetrics(); }
新增距离、时长计算与路线更新方法
使用Haversine公式计算两点间球面距离,按城市公交平均时速20km/h估算到达时间,同时生成两点间折线与地图标记:
void _updateRouteAndMetrics() { if (_currentUserLocation == null || _targetBusLocation == null) return; // 计算两点直线距离 const earthRadius = 6371; final lat1 = _currentUserLocation!.latitude * pi / 180; final lat2 = _targetBusLocation!.latitude * pi / 180; final deltaLat = (_targetBusLocation!.latitude - _currentUserLocation!.latitude) * pi / 180; final deltaLon = (_targetBusLocation!.longitude - _currentUserLocation!.longitude) * pi / 180; final a = sin(deltaLat/2)*sin(deltaLat/2) + cos(lat1)*cos(lat2)*sin(deltaLon/2)*sin(deltaLon/2); final c = 2 * atan2(sqrt(a), sqrt(1-a)); _remainingDistance = earthRadius * c; // 估算到达时间,可根据实际运营车速调整数值 const averageBusSpeed = 20; _estimatedArrivalMinutes = (_remainingDistance / averageBusSpeed * 60).round(); // 生成路线折线 _routeLines.clear(); _routeLines.add(Polyline( polylineId: const PolylineId('bus_route'), points: [_currentUserLocation!, _targetBusLocation!], color: Colors.blueAccent, width: 5, )); // 生成两端标记 _mapMarkers.clear(); _mapMarkers.addAll([ Marker( markerId: const MarkerId('user_pos'), position: _currentUserLocation!, icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen), infoWindow: const InfoWindow(title: '你的位置'), ), Marker( markerId: const MarkerId('bus_pos'), position: _targetBusLocation!, icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueRed), infoWindow: const InfoWindow(title: '公交位置'), ) ]); setState(() {}); }
重构Build方法
用Stack布局叠加上方信息展示卡片,同时监听Firebase数据变化实时更新公交位置:
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Live UOH Buses Locations')), body: StreamBuilder( stream: FirebaseFirestore.instance.collection('location').snapshots(), builder: (context, AsyncSnapshot<QuerySnapshot> snapshot) { if (!snapshot.hasData) return const Center(child: CircularProgressIndicator()); // 读取Firebase中最新的公交位置 final busData = snapshot.data!.docs.singleWhere((element) => element.id == widget.user_id); _targetBusLocation = LatLng(busData['latitude'], busData['longitude']); _updateRouteAndMetrics(); return Stack( children: [ GoogleMap( mapType: MapType.normal, markers: _mapMarkers, polylines: _routeLines, initialCameraPosition: CameraPosition(target: _targetBusLocation!, zoom: 14.47), onMapCreated: (GoogleMapController controller) { _controller = controller; _added = true; // 自动调整视角包含两个位置点 if (_currentUserLocation != null) { final bounds = LatLngBounds( southwest: LatLng( min(_currentUserLocation!.latitude, _targetBusLocation!.latitude), min(_currentUserLocation!.longitude, _targetBusLocation!.longitude) ), northeast: LatLng( max(_currentUserLocation!.latitude, _targetBusLocation!.latitude), max(_currentUserLocation!.longitude, _targetBusLocation!.longitude) ) ); controller.animateCamera(CameraUpdate.newLatLngBounds(bounds, 120)); } }, ), // 距离与时长展示卡片 Positioned( top: 16, left: 16, right: 16, child: Card( elevation: 4, child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text(*'剩余距离:${_remainingDistance.toStringAsFixed(2)} 公里'*, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), const SizedBox(height: 8), Text(*'预计到达时长:$_estimatedArrivalMinutes 分钟'*, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), ], ), ), ), ) ], ); }, ) ); }
补充说明
- 当前实现绘制的是两点间直线折线,如需真实道路路线,可将
_routeLines中的points集合替换为路径规划接口返回的道路坐标序列即可。 - 到达时长为固定车速估算值,如需更精准结果,可在接入路径规划接口时直接使用接口返回的实时时长字段。
- 原有代码中相机更新的
mymap方法可根据需要保留,不影响新增功能运行。
内容的提问来源于stack exchange,提问作者Zaryab Khalid
相关产品推荐
相关产品推荐

