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的真实尺寸会更可靠:
- 把
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; }
- 在使用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

