Flutter custom Google Map marker info window

后端 未结 6 1980
广开言路
广开言路 2020-12-31 01:24

I am working on Google Map Markers in Flutter.

On the click of each Marker, I want to show a Custom Info Window which can include a button, image etc. But in Flutte

6条回答
  •  别那么骄傲
    2020-12-31 01:38

    Stumbled across this problem and found a solution which works for me:

    To solve it I did write a Custom Info Widget, feel free to customize it. For example with some shadow via ClipShadowPath.

    Implementation

    import 'dart:async';
    
    import 'package:flutter/material.dart';
    import 'package:google_maps_flutter/google_maps_flutter.dart';
    
    import 'custom_info_widget.dart';
    
    void main() => runApp(MyApp());
    
    class PointObject {
      final Widget child;
      final LatLng location;
    
      PointObject({this.child, this.location});
    }
    
    class MyApp extends StatelessWidget {
      // This widget is the root of your application.
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          initialRoute: "/",
          routes: {
            "/": (context) => HomePage(),
          },
        );
      }
    }
    
    class HomePage extends StatefulWidget {
      @override
      _HomePageState createState() => _HomePageState();
    }
    
    class _HomePageState extends State {
      PointObject point = PointObject(
        child:  Text('Lorem Ipsum'),
        location: LatLng(47.6, 8.8796),
      );
    
      StreamSubscription _mapIdleSubscription;
      InfoWidgetRoute _infoWidgetRoute;
      GoogleMapController _mapController;
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Container(
            color: Colors.green,
            child: GoogleMap(
              initialCameraPosition: CameraPosition(
                target: const LatLng(47.6, 8.6796),
                zoom: 10,
              ),
              circles: Set()
                ..add(Circle(
                  circleId: CircleId('hi2'),
                  center: LatLng(47.6, 8.8796),
                  radius: 50,
                  strokeWidth: 10,
                  strokeColor: Colors.black,
                )),
              markers: Set()
                ..add(Marker(
                  markerId: MarkerId(point.location.latitude.toString() +
                      point.location.longitude.toString()),
                  position: point.location,
                  onTap: () => _onTap(point),
                )),
              onMapCreated: (mapController) {
                _mapController = mapController;
              },
    
              /// This fakes the onMapIdle, as the googleMaps on Map Idle does not always work
              /// (see: https://github.com/flutter/flutter/issues/37682)
              /// When the Map Idles and a _infoWidgetRoute exists, it gets displayed.
              onCameraMove: (newPosition) {
                _mapIdleSubscription?.cancel();
                _mapIdleSubscription = Future.delayed(Duration(milliseconds: 150))
                    .asStream()
                    .listen((_) {
                  if (_infoWidgetRoute != null) {
                    Navigator.of(context, rootNavigator: true)
                        .push(_infoWidgetRoute)
                        .then(
                      (newValue) {
                        _infoWidgetRoute = null;
                      },
                    );
                  }
                });
              },
            ),
          ),
        );
      }
     /// now my _onTap Method. First it creates the Info Widget Route and then
      /// animates the Camera twice:
      /// First to a place near the marker, then to the marker.
      /// This is done to ensure that onCameraMove is always called 
    
      _onTap(PointObject point) async {
        final RenderBox renderBox = context.findRenderObject();
        Rect _itemRect = renderBox.localToGlobal(Offset.zero) & renderBox.size;
    
        _infoWidgetRoute = InfoWidgetRoute(
          child: point.child,
          buildContext: context,
          textStyle: const TextStyle(
            fontSize: 14,
            color: Colors.black,
          ),
          mapsWidgetSize: _itemRect,
        );
    
        await _mapController.animateCamera(
          CameraUpdate.newCameraPosition(
            CameraPosition(
              target: LatLng(
                point.location.latitude - 0.0001,
                point.location.longitude,
              ),
              zoom: 15,
            ),
          ),
        );
        await _mapController.animateCamera(
          CameraUpdate.newCameraPosition(
            CameraPosition(
              target: LatLng(
                point.location.latitude,
                point.location.longitude,
              ),
              zoom: 15,
            ),
          ),
        );
      }
    }
    
    

    CustomInfoWidget:

    import 'package:flutter/material.dart';
    import 'package:flutter/painting.dart';
    import 'package:meta/meta.dart';
    
    class _InfoWidgetRouteLayout extends SingleChildLayoutDelegate {
      final Rect mapsWidgetSize;
      final double width;
      final double height;
    
      _InfoWidgetRouteLayout(
          {@required this.mapsWidgetSize,
          @required this.height,
          @required this.width});
    
      /// Depending of the size of the marker or the widget, the offset in y direction has to be adjusted;
      /// If the appear to be of different size, the commented code can be uncommented and
      /// adjusted to get the right position of the Widget.
      /// Or better: Adjust the marker size based on the device pixel ratio!!!!)
    
      @override
      Offset getPositionForChild(Size size, Size childSize) {
    //    if (Platform.isIOS) {
        return Offset(
          mapsWidgetSize.center.dx - childSize.width / 2,
          mapsWidgetSize.center.dy - childSize.height - 50,
        );
    //    } else {
    //      return Offset(
    //        mapsWidgetSize.center.dx - childSize.width / 2,
    //        mapsWidgetSize.center.dy - childSize.height - 10,
    //      );
    //    }
      }
    
      @override
      BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
        //we expand the layout to our predefined sizes
        return BoxConstraints.expand(width: width, height: height);
      }
    
      @override
      bool shouldRelayout(_InfoWidgetRouteLayout oldDelegate) {
        return mapsWidgetSize != oldDelegate.mapsWidgetSize;
      }
    }
    
    class InfoWidgetRoute extends PopupRoute {
      final Widget child;
      final double width;
      final double height;
      final BuildContext buildContext;
      final TextStyle textStyle;
      final Rect mapsWidgetSize;
    
      InfoWidgetRoute({
        @required this.child,
        @required this.buildContext,
        @required this.textStyle,
        @required this.mapsWidgetSize,
        this.width = 150,
        this.height = 50,
        this.barrierLabel,
      });
    
      @override
      Duration get transitionDuration => Duration(milliseconds: 100);
    
      @override
      bool get barrierDismissible => true;
    
      @override
      Color get barrierColor => null;
    
      @override
      final String barrierLabel;
    
      @override
      Widget buildPage(BuildContext context, Animation animation,
          Animation secondaryAnimation) {
        return MediaQuery.removePadding(
          context: context,
          removeBottom: true,
          removeLeft: true,
          removeRight: true,
          removeTop: true,
          child: Builder(builder: (BuildContext context) {
            return CustomSingleChildLayout(
              delegate: _InfoWidgetRouteLayout(
                  mapsWidgetSize: mapsWidgetSize, width: width, height: height),
              child: InfoWidgetPopUp(
                infoWidgetRoute: this,
              ),
            );
          }),
        );
      }
    }
    
    class InfoWidgetPopUp extends StatefulWidget {
      const InfoWidgetPopUp({
        Key key,
        @required this.infoWidgetRoute,
      })  : assert(infoWidgetRoute != null),
            super(key: key);
    
      final InfoWidgetRoute infoWidgetRoute;
    
      @override
      _InfoWidgetPopUpState createState() => _InfoWidgetPopUpState();
    }
    
    class _InfoWidgetPopUpState extends State {
      CurvedAnimation _fadeOpacity;
    
      @override
      void initState() {
        super.initState();
        _fadeOpacity = CurvedAnimation(
          parent: widget.infoWidgetRoute.animation,
          curve: Curves.easeIn,
          reverseCurve: Curves.easeOut,
        );
      }
    
      @override
      Widget build(BuildContext context) {
        return FadeTransition(
          opacity: _fadeOpacity,
          child: Material(
            type: MaterialType.transparency,
            textStyle: widget.infoWidgetRoute.textStyle,
            child: ClipPath(
              clipper: _InfoWidgetClipper(),
              child: Container(
                color: Colors.white,
                padding: EdgeInsets.only(bottom: 10),
                child: Center(child: widget.infoWidgetRoute.child),
              ),
            ),
          ),
        );
      }
    }
    
    class _InfoWidgetClipper extends CustomClipper {
      @override
      Path getClip(Size size) {
        Path path = Path();
        path.lineTo(0.0, size.height - 20);
        path.quadraticBezierTo(0.0, size.height - 10, 10.0, size.height - 10);
        path.lineTo(size.width / 2 - 10, size.height - 10);
        path.lineTo(size.width / 2, size.height);
        path.lineTo(size.width / 2 + 10, size.height - 10);
        path.lineTo(size.width - 10, size.height - 10);
        path.quadraticBezierTo(
            size.width, size.height - 10, size.width, size.height - 20);
        path.lineTo(size.width, 10.0);
        path.quadraticBezierTo(size.width, 0.0, size.width - 10.0, 0.0);
        path.lineTo(10, 0.0);
        path.quadraticBezierTo(0.0, 0.0, 0.0, 10);
        path.close();
        return path;
      }
    
      @override
      bool shouldReclip(CustomClipper oldClipper) => false;
    }
    
    

提交回复
热议问题