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

Flutter中基于Mapbox实现车辆移动动画的技术求助

实现Flutter Mapbox车辆平滑移动动画方案

由于没有适配Mapbox的现成动画插件,我们可以通过手动位置插值+Flutter动画控制器来实现类似的车辆平滑移动效果,以下是具体实现步骤:


核心思路

当收到新的车辆位置时,不直接更新标记位置,而是通过AnimationController驱动一个从当前位置到目标位置的插值动画,同时计算车辆朝向角度,让移动过程更自然。


具体实现

1. 依赖与初始化

确保已集成mapbox_gl包,然后在StatefulWidget中初始化动画控制器及位置变量:

import 'package:flutter/material.dart';
import 'package:mapbox_gl/mapbox_gl.dart';
import 'dart:math' as math;

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

  @override
  State<VehicleTrackingPage> createState() => _VehicleTrackingPageState();
}

class _VehicleTrackingPageState extends State<VehicleTrackingPage> with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  LatLng? _currentVehiclePos;
  LatLng? _targetVehiclePos;
  Animation<LatLng>? _posAnimation;

  @override
  void initState() {
    super.initState();
    // 初始化动画控制器,默认时长可根据需求调整
    _animationController = AnimationController(
      duration: const Duration(milliseconds: 500),
      vsync: this,
    );
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

2. 自定义位置插值Tween

Flutter默认没有LatLng类型的Tween,我们需要自定义一个来实现经纬度的平滑插值:

class LatLngTween extends Tween<LatLng> {
  LatLngTween({required LatLng begin, required LatLng end}) : super(begin: begin, end: end);

  @override
  LatLng lerp(double t) {
    return LatLng(
      begin!.latitude + (end!.latitude - begin!.latitude) * t,
      begin!.longitude + (end!.longitude - begin!.longitude) * t,
    );
  }
}

3. 位置更新与动画触发

编写方法处理新位置的到来,启动插值动画:

// 处理车辆位置更新
void updateVehicleLocation(LatLng newPos) {
  if (_currentVehiclePos == null) {
    // 初始位置直接赋值
    setState(() => _currentVehiclePos = newPos);
    return;
  }

  // 重置之前的动画,避免冲突
  _animationController.reset();
  _targetVehiclePos = newPos;

  // 动态计算动画时长:根据距离调整,每米10ms,限制300ms-2000ms区间
  final distance = _calculateDistance(_currentVehiclePos!, newPos);
  _animationController.duration = Duration(
    milliseconds: (distance * 10).clamp(300, 2000).toInt(),
  );

  // 创建位置插值动画
  _posAnimation = LatLngTween(
    begin: _currentVehiclePos,
    end: newPos,
  ).animate(CurvedAnimation(
    parent: _animationController,
    curve: Curves.easeInOut,
  ));

  // 监听动画帧,更新当前位置
  _posAnimation!.addListener(() {
    setState(() => _currentVehiclePos = _posAnimation!.value);
  });

  // 启动动画
  _animationController.forward();
}

// Haversine公式计算两点距离(单位:米)
double _calculateDistance(LatLng start, LatLng end) {
  const earthRadius = 6371000;
  final lat1 = math.pi * start.latitude / 180;
  final lon1 = math.pi * start.longitude / 180;
  final lat2 = math.pi * end.latitude / 180;
  final lon2 = math.pi * end.longitude / 180;

  final dLat = lat2 - lat1;
  final dLon = lon2 - lon1;

  final a = math.sin(dLat / 2) * math.sin(dLat / 2) +
      math.cos(lat1) * math.cos(lat2) * math.sin(dLon / 2) * math.sin(dLon / 2);
  final c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a));
  return earthRadius * c;
}

4. 计算车辆朝向角度

让车辆始终朝向移动方向,通过方位角计算实现:

// 计算两点间的方位角(车辆朝向)
double _calculateBearing(LatLng start, LatLng end) {
  final lat1 = math.pi * start.latitude / 180;
  final lon1 = math.pi * start.longitude / 180;
  final lat2 = math.pi * end.latitude / 180;
  final lon2 = math.pi * end.longitude / 180;

  final dLon = lon2 - lon1;
  final y = math.sin(dLon) * math.cos(lat2);
  final x = math.cos(lat1) * math.sin(lat2) - math.sin(lat1) * math.cos(lat2) * math.cos(dLon);
  var bearing = math.atan2(y, x) * 180 / math.pi;
  bearing = (bearing + 360) % 360;
  return bearing;
}

5. 渲染Mapbox地图与车辆标记

在build方法中添加Mapbox地图,并绑定动画后的位置和朝向:

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: _currentVehiclePos == null
        ? const Center(child: CircularProgressIndicator())
        : MapboxMap(
            accessToken: 'YOUR_MAPBOX_ACCESS_TOKEN',
            initialCameraPosition: CameraPosition(
              target: _currentVehiclePos!,
              zoom: 16,
            ),
            // 可选:开启地图跟随车辆
            onMapCreated: (controller) {
              _posAnimation?.addListener(() {
                controller.animateCamera(
                  CameraUpdate.newCameraPosition(
                    CameraPosition(target: _currentVehiclePos!, zoom: 16),
                  ),
                  duration: const Duration(milliseconds: 100),
                );
              });
            },
            markers: [
              Marker(
                markerId: const MarkerId('vehicle'),
                position: _currentVehiclePos!,
                // 设置车辆朝向
                rotation: _targetVehiclePos != null
                    ? _calculateBearing(_currentVehiclePos!, _targetVehiclePos!)
                    : 0,
                // 替换为你的车辆图标
                icon: BitmapDescriptor.fromAssetImage(
                  const ImageConfiguration(size: Size(40, 40)),
                  'assets/vehicle_icon.png',
                ),
              ),
            ],
          ),
  );
}
}

优化建议

  • 防抖处理:如果位置更新过于频繁,可添加防抖逻辑,比如100ms内只处理最后一次位置更新,避免动画堆积。
  • 图标优化:使用带有透明背景的车辆PNG图标,确保旋转时显示正常。
  • 边界处理:当车辆移动到地图边缘时,自动调整地图视野范围。

内容的提问来源于stack exchange,提问作者Gowsik Raja

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 01:55:20