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

如何在Flutter的OpenStreetMap中围绕定位Pin绘制带距离尺度的椭圆?

嘿,这个需求确实有点绕——毕竟CircleMarker的useRadiusInMeter是专门为圆形做的适配,椭圆确实没有现成组件直接支持基于距离单位的缩放。不过咱们可以通过CustomPainter结合地图投影的比例尺转换来实现,我给你拆解下具体思路和代码示例:

核心思路

要实现基于米级的椭圆,关键是把距离单位(米)转换成当前地图缩放级别下的屏幕像素,然后用CustomPainter绘制椭圆并应用旋转。核心步骤是:

  • 利用地图的投影工具,计算当前缩放级别下1米对应的屏幕像素数
  • 自定义Painter,将椭圆的宽高(米)转成像素后绘制,同时支持旋转
  • 将Painter挂载到地图的Overlay层,跟着用户定位点移动
步骤1:计算米与屏幕像素的转换系数

首先需要从地图控制器获取投影信息,通过对比同一点附近1米的经纬度对应的屏幕坐标,算出像素与米的比例:

// 假设你已经有地图控制器和用户定位点
final MapController mapController = ...;
final LatLng userLocation = ...;

// 获取地图投影,用于经纬度转屏幕坐标
final projection = mapController.projection;
final centerPixel = projection.toScreenLocation(userLocation);

// 计算用户定位点东边1米对应的经纬度(近似公式,也可以用geolocator库的更精确方法)
final latDelta = 0.0; // 纬度方向偏移0米
final lonDelta = 1 / (111111 * cos(userLocation.latitude * pi / 180)); // 1米对应的经度偏移
final eastPoint = LatLng(userLocation.latitude + latDelta, userLocation.longitude + lonDelta);

// 获取东边1米点的屏幕坐标,计算1米对应的像素数
final eastPixel = projection.toScreenLocation(eastPoint);
final pixelsPerMeter = (eastPixel.x - centerPixel.x).abs();
步骤2:自定义椭圆绘制Painter

接下来写一个CustomPainter,接收米级的宽高、旋转角度和像素转换系数,绘制椭圆:

class DistanceEllipsePainter extends CustomPainter {
  final double widthMeters;
  final double heightMeters;
  final double rotationDegrees;
  final double pixelsPerMeter;
  final Offset centerOffset;

  DistanceEllipsePainter({
    required this.widthMeters,
    required this.heightMeters,
    required this.rotationDegrees,
    required this.pixelsPerMeter,
    required this.centerOffset,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final fillPaint = Paint()
      ..color = Colors.blue.withOpacity(0.3)
      ..style = PaintingStyle.fill;
    final strokePaint = Paint()
      ..color = Colors.blue
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2;

    // 把米转成屏幕像素
    final widthPixels = widthMeters * pixelsPerMeter;
    final heightPixels = heightMeters * pixelsPerMeter;

    // 画布变换:先平移到中心点,再旋转
    canvas.save();
    canvas.translate(centerOffset.dx, centerOffset.dy);
    canvas.rotate(rotationDegrees * pi / 180); // 转成弧度

    // 绘制椭圆(基于中心点的矩形)
    final ellipseRect = Rect.fromCenter(
      center: Offset.zero,
      width: widthPixels,
      height: heightPixels,
    );
    canvas.drawOval(ellipseRect, fillPaint);
    canvas.drawOval(ellipseRect, strokePaint);

    canvas.restore();
  }

  @override
  bool shouldRepaint(covariant DistanceEllipsePainter oldDelegate) {
    // 只有当参数变化时重绘
    return widthMeters != oldDelegate.widthMeters ||
        heightMeters != oldDelegate.heightMeters ||
        rotationDegrees != oldDelegate.rotationDegrees ||
        pixelsPerMeter != oldDelegate.pixelsPerMeter ||
        centerOffset != oldDelegate.centerOffset;
  }
}
步骤3:将椭圆挂载到地图上

用OverlayEntry把椭圆绘制到地图上,这样它会跟着地图移动和缩放:

OverlayEntry? _ellipseOverlay;

void _showEllipseOnMap(BuildContext context) {
  // 先计算像素转换系数(复用步骤1的代码)
  final projection = mapController.projection;
  final centerPixel = projection.toScreenLocation(userLocation);
  // ...(省略pixelsPerMeter计算)

  _ellipseOverlay = OverlayEntry(
    builder: (context) => Positioned(
      // 定位到椭圆的左上角,确保中心点对齐用户定位点
      left: centerPixel.x - (50 * pixelsPerMeter)/2, // 50米宽度的一半
      top: centerPixel.y - (20 * pixelsPerMeter)/2, // 20米高度的一半
      width: 50 * pixelsPerMeter,
      height: 20 * pixelsPerMeter,
      child: CustomPaint(
        painter: DistanceEllipsePainter(
          widthMeters: 50, // 椭圆宽度50米
          heightMeters: 20, // 椭圆高度20米
          rotationDegrees: 45, // 旋转45度
          pixelsPerMeter: pixelsPerMeter,
          centerOffset: Offset(
            (50 * pixelsPerMeter)/2,
            (20 * pixelsPerMeter)/2,
          ),
        ),
      ),
    ),
  );

  Overlay.of(context)!.insert(_ellipseOverlay!);
}

// 监听地图缩放和移动事件,更新椭圆
void _setupMapListeners() {
  mapController.listenerMapSingleTapping.addListener(() {
    _updateEllipse();
  });
  mapController.listenerMapZoom.addListener(() {
    _updateEllipse();
  });
}

void _updateEllipse() {
  // 重新计算像素转换系数,然后标记Overlay需要重绘
  final projection = mapController.projection;
  final centerPixel = projection.toScreenLocation(userLocation);
  // ...(省略pixelsPerMeter计算)
  _ellipseOverlay?.markNeedsBuild();
}
关键注意事项
  • 比例尺更新:地图缩放或平移时,必须重新计算pixelsPerMeter,否则椭圆的大小会和实际距离不符,所以一定要监听地图的缩放和移动事件
  • 经纬度转米的精度:上面用的是近似公式,如果需要更高精度,可以用geolocator库的distanceBetween方法反向计算经纬度偏移
  • Overlay生命周期:记得在页面销毁时移除OverlayEntry,避免内存泄漏

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.28 13:12:34