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

Flutter中Google Maps自定义InfoWindow在不同文本缩放系数下的错位问题

Flutter中Google Maps自定义InfoWindow在不同文本缩放系数下的错位问题

我之前也碰到过类似的Google Maps自定义InfoWindow定位坑,尤其是文本缩放系数变化时的错位问题,太头疼了!结合你的描述和代码来看,问题大概率出在文本缩放系数的一致性、预计算尺寸与实际渲染尺寸的偏差,或者坐标转换的细节上,下面给你几个针对性的解决思路:

一、确保文本缩放系数在计算和渲染时完全对齐

你提到App里把文本缩放系数限制在了0.8-1.6之间,但看你的calculateSize方法里直接用了mediaQuery.textScaler,如果没有做clamp处理,那预计算的文本尺寸就会和实际渲染的不一致,这肯定会导致错位。

修改计算逻辑,把textScaler也做同样的clamp:

final TextScaler textScaler = mediaQuery.textScaler.clamp(
  minScaleFactor: 0.8, 
  maxScaleFactor: 1.6
);

这样预计算的文本高度/宽度和Widget实际渲染出来的就会完全匹配,不会因为缩放差异导致尺寸计算错误。

二、放弃预计算,改用GlobalKey获取实际渲染尺寸

有时候预计算的文本尺寸会因为字体渲染、文本换行等细节和实际有偏差,尤其是多语言或长文本场景。改用GlobalKey获取Widget的真实尺寸会更可靠:

  1. 把InfoWindowWidget改成StatefulWidget(或者用GlobalKey绑定StatelessWidget,但Stateful更灵活):
class InfoWindowWidget extends StatefulWidget {
  const InfoWindowWidget({required this.device, required this.onSizeCalculated, super.key});
  final Device device;
  final Function(Size size) onSizeCalculated; // 回调传递真实尺寸

  @override
  State<InfoWindowWidget> createState() => _InfoWindowWidgetState();
}

class _InfoWindowWidgetState extends State<InfoWindowWidget> {
  @override
  void initState() {
    super.initState();
    // 等待Widget渲染完成后获取尺寸
    WidgetsBinding.instance.addPostFrameCallback((_) {
      final size = context.size;
      if (size != null) {
        widget.onSizeCalculated(size);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    // 原来的build代码不变
    final String titleText = 'Transmit Code: ${widget.device.transmitCode}';
    final String volumeText = 'Water Volume: ${widget.device.volume}';
    final String updateText = 'Last Update: ${DateFormat('d/M/yyyy HH:mm:ss').format(DateTime.fromMillisecondsSinceEpoch(widget.device.lastUpdate))}';

    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Container(
          width: InfoWindowWidget.kMaxWidth,
          padding: const EdgeInsets.all(InfoWindowWidget.kPadding),
          decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.circular(InfoWindowWidget.kBorderRadius),
            boxShadow: [
              BoxShadow(
                blurRadius: InfoWindowWidget.kShadowBlur,
                offset: InfoWindowWidget.kShadowOffset,
                color: Colors.black.withOpacity(0.1),
              )
            ],
          ),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.min,
            children: [
              Row(
                children: [
                  const Icon(Icons.radio, size: InfoWindowWidget.kIconSize),
                  const SizedBox(width: InfoWindowWidget.kIconSpacing),
                  Expanded(
                    child: Text(
                      titleText,
                      style: typo.heading.text18.copyWith(height: 1.3),
                      maxLines: 2,
                      overflow: TextOverflow.ellipsis,
                    ),
                  ),
                ],
              ),
              const SizedBox(height: InfoWindowWidget.kContentSpacing),
              Text(
                volumeText,
                style: typo.body.text16.copyWith(height: 1.3),
                maxLines: 2,
                overflow: TextOverflow.ellipsis,
              ),
              Text(
                updateText,
                style: typo.body.text16.copyWith(height: 1.3),
                maxLines: 2,
                overflow: TextOverflow.ellipsis,
              ),
            ],
          ),
        ),
        CustomPaint(
          size: Size(InfoWindowWidget.kTriangleWidth, InfoWindowWidget.kTriangleHeight),
          painter: TrianglePainter(),
        ),
      ],
    );
  }
}

// 补充三角形绘制的Painter
class TrianglePainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.white
      ..style = PaintingStyle.fill;
    final path = Path()
      ..moveTo(size.width / 2, 0)
      ..lineTo(0, size.height)
      ..lineTo(size.width, size.height)
      ..close();
    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
  1. 在使用InfoWindow的地方,通过回调拿到真实尺寸后再计算定位偏移:
// 假设你用Overlay来显示自定义InfoWindow
final overlayState = Overlay.of(context);
final mapKey = GlobalKey(); // 给GoogleMap组件绑定这个key
Size? infoWindowSize;
LatLng markerLatLng = const LatLng(31.2304, 121.4737); // 示例经纬度

late final OverlayEntry overlayEntry;
overlayEntry = OverlayEntry(
  builder: (context) {
    if (infoWindowSize == null) {
      return const SizedBox.shrink();
    }
    // 先获取Marker的全局屏幕坐标
    final RenderBox mapRenderBox = mapKey.currentContext!.findRenderObject() as RenderBox;
    final Offset markerLocalPos = mapController.getLatLngToScreenCoordinate(markerLatLng)!;
    final Offset markerGlobalPos = mapRenderBox.localToGlobal(markerLocalPos);

    return Positioned(
      left: markerGlobalPos.dx - infoWindowSize!.width / 2,
      top: markerGlobalPos.dy - infoWindowSize!.height,
      child: InfoWindowWidget(
        device: yourDeviceData,
        onSizeCalculated: (size) {
          setState(() {
            infoWindowSize = size;
            overlayEntry.markNeedsBuild();
          });
        },
      ),
    );
  },
);
overlayState.insert(overlayEntry);

三、检查坐标转换的完整性

Google Maps的getLatLngToScreenCoordinate返回的是MapWidget内部的本地坐标,不是屏幕全局坐标。如果你的MapWidget上面有AppBar或者侧边栏,直接用这个坐标计算会导致偏移。

正确的转换步骤:

// 1. 获取MapWidget的RenderBox(需要给GoogleMap组件绑定GlobalKey)
final RenderBox mapRenderBox = mapKey.currentContext!.findRenderObject() as RenderBox;
// 2. 把经纬度转成MapWidget本地坐标
final Offset markerLocalPos = await mapController.getLatLngToScreenCoordinate(markerLatLng);
// 3. 转成屏幕全局坐标
final Offset markerGlobalPos = mapRenderBox.localToGlobal(markerLocalPos);

用这个全局坐标来计算InfoWindow的Positioned位置,才能保证在任何布局下都准确。

四、考虑Marker的锚点默认值

默认情况下,Google Maps Marker的锚点是(0.5, 1.0)(底部中心),你的InfoWindow底部有个三角形,要让三角形尖端正好对准Marker的顶部中心,偏移计算需要注意:

  • 水平方向:InfoWindow的中心要和Marker锚点的x坐标对齐,也就是markerGlobalPos.dx - infoWindowSize!.width / 2
  • 垂直方向:InfoWindow的底部三角形尖端要和Marker锚点的y坐标对齐,也就是markerGlobalPos.dy - infoWindowSize!.height(因为InfoWindow的总高度包含了三角形的高度)

按照这几个思路调整后,应该就能解决不同文本缩放系数下的错位问题了,我之前就是这么搞定的!

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.08 08:24:34