Flutter自定义Google Map标记信息窗口

时间:2019-01-09 06:13:16

标签: flutter flutter-layout

我正在使用Flutter开发Google Map Markers。

单击每个标记,我想显示一个自定义信息窗口,其中可以包含按钮,图像等。但是在Flutter中,有一个属性TextInfoWindow仅接受String

如何实现将按钮和图像添加到地图标记的InfoWindow

6 个答案:

答案 0 :(得分:4)

我今天偶然发现了一个相同的问题,我无法在TextInfoWindow中正确显示多行字符串。最后,我实现了一个模态底部工作表(https://docs.flutter.io/flutter/material/showModalBottomSheet.html)来规避该问题,该工作表会在您单击标记时显示,在我的情况下效果很好。

我也可以想象出许多用例,其中您想完全自定义标记的信息窗口,但是在GitHub(https://github.com/flutter/flutter/issues/23938)上阅读此问题似乎是不可能的,因为InfoWindow不是Flutter小部件。

答案 1 :(得分:2)

您可以将由小部件组成的标记显示为自定义“信息窗口”。基本上,您是在创建小部件的png图像并将其显示为标记。

import 'dart:typed_data';
import 'dart:ui';

import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';

class MarkerInfo extends StatefulWidget {
  final Function getBitmapImage;
  final String text;
  MarkerInfo({Key key, this.getBitmapImage, this.text}) : super(key: key);

  @override
  _MarkerInfoState createState() => _MarkerInfoState();
}

class _MarkerInfoState extends State<MarkerInfo> {
  final markerKey = GlobalKey();

  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) => getUint8List(markerKey)
        .then((markerBitmap) => widget.getBitmapImage(markerBitmap)));
  }

  Future<Uint8List> getUint8List(GlobalKey markerKey) async {
    RenderRepaintBoundary boundary =
        markerKey.currentContext.findRenderObject();
    var image = await boundary.toImage(pixelRatio: 2.0);
    ByteData byteData = await image.toByteData(format: ImageByteFormat.png);
    return byteData.buffer.asUint8List();
  }

  @override
  Widget build(BuildContext context) {
    return RepaintBoundary(
      key: markerKey,
      child: Container(
        padding: EdgeInsets.only(bottom: 29),
        child: Container(
          width: 100,
          height: 100,
          color: Color(0xFF000000),
          child: Text(
            widget.text,
            style: TextStyle(
              color: Color(0xFFFFFFFF),
            ),
          ),
        ),
      ),
    );
  }
}

如果使用这种方法,则必须确保呈现窗口小部件,否则将无法正常工作。要将小部件转换为图像-必须渲染小部件才能进行转换。我将小部件隐藏在Stack中的地图下。

return Stack(
        children: <Widget>[
          MarkerInfo(
              text: tripMinutes.toString(),
              getBitmapImage: (img) {
                customMarkerInfo = img;
              }),
          GoogleMap(
            markers: markers,
 ...

最后一步是创建标记。从窗口小部件传递的数据保存在customMarkerInfo中-字节,因此请将其转换为Bitmap。

markers.add(
          Marker(
            position: position,
            icon: BitmapDescriptor.fromBytes(customMarkerInfo),
            markerId: MarkerId('MarkerID'),
          ),
        );

Example

答案 2 :(得分:1)

下面是我为项目中的自定义InfoWindow实施的4个步骤

第1步:为GoogleMap和“信息窗口自定义”创建一个堆栈。

Stack(
  children: <Widget>[
    Positioned.fill(child: GoogleMap(...),),
    Positioned(
      top: {offsetY},
      left: {offsetX},
      child: YourCustomInfoWidget(...),
    )
  ]
)

第2步:当用户单击带有功能的屏幕上的标记计算器位置时:

screenCoordinate = await _mapController.getScreenCoordinate(currentPosition.target)

步骤3:计算器offsetY,offsetX和setState。

相关问题:https://github.com/flutter/flutter/issues/41653

devicePixelRatio = Platform.isAndroid ? MediaQuery.of(context).devicePixelRatio : 1.0;

offsetY = (screenCoordinate?.y?.toDouble() ?? 0) / devicePixelRatio - infoWidget.size.width;
offsetX = (screenCoordinate?.x?.toDouble() ?? 0) / devicePixelRatio - infoWidget.size.height;

第4步:点击时禁用标记自动移动相机

Marker(
   ...
   consumeTapEvents: true,)

答案 3 :(得分:1)

要创建基于窗口小部件的信息窗口,您需要将窗口小部件堆叠在Google地图上。借助ChangeNotifierProviderChangeNotifierConsumer,即使相机在Google地图上移动,您也可以轻松地重建小部件。

InfoWindowModel类:

class InfoWindowModel extends ChangeNotifier {
  bool _showInfoWindow = false;
  bool _tempHidden = false;
  User _user;
  double _leftMargin;
  double _topMargin;

  void rebuildInfoWindow() {
    notifyListeners();
  }

  void updateUser(User user) {
    _user = user;
  }

  void updateVisibility(bool visibility) {
    _showInfoWindow = visibility;
  }

  void updateInfoWindow(
    BuildContext context,
    GoogleMapController controller,
    LatLng location,
    double infoWindowWidth,
    double markerOffset,
  ) async {
    ScreenCoordinate screenCoordinate =
        await controller.getScreenCoordinate(location);
    double devicePixelRatio =
        Platform.isAndroid ? MediaQuery.of(context).devicePixelRatio : 1.0;
    double left = (screenCoordinate.x.toDouble() / devicePixelRatio) -
        (infoWindowWidth / 2);
    double top =
        (screenCoordinate.y.toDouble() / devicePixelRatio) - markerOffset;
    if (left < 0 || top < 0) {
      _tempHidden = true;
    } else {
      _tempHidden = false;
      _leftMargin = left;
      _topMargin = top;
    }
  }

  bool get showInfoWindow =>
      (_showInfoWindow == true && _tempHidden == false) ? true : false;

  double get leftMargin => _leftMargin;

  double get topMargin => _topMargin;

  User get user => _user;
}

完整示例可以在我的blog上找到!

答案 4 :(得分:0)

偶然发现了这个问题,找到了适合我的解决方案:

要解决此问题,我确实写了Custom Info Widget,请随时对其进行自定义。

实施

首先,我确实有一个由一个位置和一个小部件组成的对象点。 而且我确实计算了taxicapDistance。


Marker(
    markerId: MarkerId(point.location.latitude.toString() + point.location.longitude.toString()),
    position: point.location,
    onTap: () => _onTap(point),
    );


  double _taxicabDistance(LatLng p1, LatLng p2) =>
      (p1.latitude - p2.latitude).abs() + (p1.longitude - p2.longitude).abs();

现在是我的_onTap方法。首先,它将“摄像机”动画化到标记的位置,然后导航器推送“信息”小部件的弹出路径。

需要Future延迟,因为移动动画需要一些时间。

_onTap(PointObject point) async {
    final RenderBox renderBox = context.findRenderObject();
    Rect _itemRect = renderBox.localToGlobal(Offset.zero) & renderBox.size;

    InfoWidgetRoute _infoWidgetRoute = InfoWidgetRoute(
      child: point.child,
      buildContext: context,
      textStyle: const TextStyle(
        fontSize: 14,
        color: Colors.black,
      ),
      mapsWidgetSize: _itemRect,
    );

     if (_taxicabDistance(_cameraPosition.target, point.location) > 0.0001) {
      await mapController.animateCamera(
        CameraUpdate.newCameraPosition(
          CameraPosition(
            target: LatLng(
              point.location.latitude,
              point.location.longitude,
            ),
            zoom: 15,
          ),
        ),
      );

      Future.delayed(
        Platform.isIOS
            ? Duration(milliseconds: 500)
            : Duration(milliseconds: 1250),
        () {

          Navigator.of(context, rootNavigator: true)
              .push(_infoWidgetRoute)
              .then<void>(
            (newValue) {
              _infoWidgetRoute = null;
            },
          );
        },
      );
    } else {
      Navigator.of(context, rootNavigator: true)
          .push(_infoWidgetRoute)
          .then<void>(
        (newValue) {
          _infoWidgetRoute = null;
        },
      );
    }
}

答案 5 :(得分:0)

这是一种创建不依赖于InfoWindow的自定义标记的解决方案。 尽管如此,该方法不允许您在自定义标记上添加按钮

Flutter谷歌地图插件可让我们使用图像数据/资产来创建自定义标记。因此,此方法使用在Canvas上绘制以创建自定义标记,并使用PictureRecorder将其转换为图片,随后Google地图插件将使用该图片来呈现自定义标记。

在Canvas上绘制示例代码并将其转换为可由插件使用的Image数据。

void paintTappedImage() async {
    final ui.PictureRecorder recorder = ui.PictureRecorder();
    final Canvas canvas = Canvas(recorder, Rect.fromPoints(const Offset(0.0, 0.0), const Offset(200.0, 200.0)));
    final Paint paint = Paint()
      ..color = Colors.black.withOpacity(1)
      ..style = PaintingStyle.fill;
    canvas.drawRRect(
        RRect.fromRectAndRadius(
            const Rect.fromLTWH(0.0, 0.0, 152.0, 48.0), const Radius.circular(4.0)),
        paint);
    paintText(canvas);
    paintImage(labelIcon, const Rect.fromLTWH(8, 8, 32.0, 32.0), canvas, paint,
        BoxFit.contain);
    paintImage(markerImage, const Rect.fromLTWH(24.0, 48.0, 110.0, 110.0), canvas,
        paint, BoxFit.contain);
    final Picture picture = recorder.endRecording();
    final img = await picture.toImage(200, 200);
    final pngByteData = await img.toByteData(format: ImageByteFormat.png);
    setState(() {
      _customMarkerIcon = BitmapDescriptor.fromBytes(Uint8List.view(pngByteData.buffer));
    });
  }

  void paintText(Canvas canvas) {
    final textStyle = TextStyle(
      color: Colors.white,
      fontSize: 24,
    );
    final textSpan = TextSpan(
      text: '18 mins',
      style: textStyle,
    );
    final textPainter = TextPainter(
      text: textSpan,
      textDirection: TextDirection.ltr,
    );
    textPainter.layout(
      minWidth: 0,
      maxWidth: 88,
    );
    final offset = Offset(48, 8);
    textPainter.paint(canvas, offset);
  }

  void paintImage(
      ui.Image image, Rect outputRect, Canvas canvas, Paint paint, BoxFit fit) {
    final Size imageSize =
        Size(image.width.toDouble(), image.height.toDouble());
    final FittedSizes sizes = applyBoxFit(fit, imageSize, outputRect.size);
    final Rect inputSubrect =
        Alignment.center.inscribe(sizes.source, Offset.zero & imageSize);
    final Rect outputSubrect =
        Alignment.center.inscribe(sizes.destination, outputRect);
    canvas.drawImageRect(image, inputSubrect, outputSubrect, paint);
  }

点击标记后,我们可以用Canvas生成的新图像替换点击的图像。从Google Maps插件示例应用中获取的示例代码相同。

void _onMarkerTapped(MarkerId markerId) async {
  final Marker tappedMarker = markers[markerId];
  if (tappedMarker != null) {
    if (markers.containsKey(selectedMarker)) {
      final Marker resetOld =
      markers[selectedMarker].copyWith(iconParam: _markerIconUntapped);
      setState(() {
        markers[selectedMarker] = resetOld;
      });
    }
    Marker newMarker;
    selectedMarker = markerId;
    newMarker = tappedMarker.copyWith(iconParam: _customMarkerIcon);
    setState(() {
      markers[markerId] = newMarker;
    });
    tappedCount++;
  }
}

参考:

How to convert a flutter canvas to Image

Flutter plugin example app.

Google maps flutter plugin custom marker in action.