You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.02 03:36:34