custom_map_markers icon indicating copy to clipboard operation
custom_map_markers copied to clipboard

disable map reload

Open rahmanrezaee opened this issue 3 years ago • 3 comments

reload I think is so bad UX for user

an example is for test data which build it in if we get data from the server and wait for data and reload the map takes many seconds. please find a right way to implement this issue

rahmanrezaee avatar Jun 17 '22 19:06 rahmanrezaee

Yes same issue here, would be cool if we could do live location updates, like Uber. Maybe process all the widget things in the background ? (Not sure that is possible in flutter) Or maybe once markers are added make it so we can move them without reloading ? But great work still I am gonna try to contribute if I have the time !

theo-michel avatar Jun 28 '22 21:06 theo-michel

I believe that for the nature of builders, the reload of the map itself cannot be disabled, but, you can use their own classes of custom markers and markers controller to generate your own custom markers in the state and just pass them to the map

Here my code:

GoogleMapController? _mapController;
List<Location> locations = [];
late MarkersController markersController;
Set<Marker> markers = {};

void markersControllerListener () {
   // This listener only will be called when all the widgets are converted to images
    if (markersController.images != null) {
      try {
        setState(() {
          markers = locations.map<Marker> (
          (location) => Marker (
            markerId: MarkerId (location.id),
            position: LatLng (location.lat, location.lon),
            onTap: () => widget.openStore (location.id),
            icon: BitmapDescriptor.fromBytes(markersController.images! [locations.indexOf(location)])
          )
          ).toSet();
        });
      } catch (error) {
        showErrorDialog(
          context,
          title: S.of(context).error,
          message: S.of(context).anErrorOccurredLoadingMarkers
        );
      }
    }
  }

  // Call this function in your initState
  void _loadLocationMarkers () async {
    if (mounted) {
      locations = Provider.of<Locations> (context, listen: false).locations;
    }

    markersController = MarkersController (
      value: List<Uint8List?>.filled(locations.length, null),
      childCount: locations.length
    );

    markersController.addListener(markersControllerListener);
  }
  
@override
  Widget build(BuildContext context) {
    return Stack(
      children: [
            // Add A generated list of custom markers in the tree of your app to capture the images and convert them to pngs to be added as bytes in the google marker
        ...(
            markersController.ready
            ? []
            : List.generate(
              markersController.childCount,
              (index) => Positioned(
              left: -MediaQuery.of(context).size.width,
              child: CustomMarker(
                child: locations[index].record.type.marker , // Here i have an enum with a widget called marker
                screenshotDelay: Duration.zero, // Remove delay
                onImageCaptured: (data) {
                  markersController.updateRenderedImage(index, data);
                },
              ),
            )
          )
        ),
        GoogleMap(
          myLocationEnabled: runtime != "Development",
          myLocationButtonEnabled: false,
          tiltGesturesEnabled: false,
          zoomControlsEnabled: false,
          mapToolbarEnabled: false,
          mapType: MapType.normal,
          rotateGesturesEnabled: false,
          buildingsEnabled: false,
          minMaxZoomPreference: const MinMaxZoomPreference(
            0, 20
          ),
          onMapCreated: (controller) async {
           ...
          },  
          markers: markers,
        ),
      ],
    );
  }

As a note, I had to extract markers controller from the package cause it is private and custom markers you can use the model from the package located in /src folder.

Good Luck!

Djcharles26 avatar Aug 08 '22 15:08 Djcharles26

I have used the code from @Djcharles26 to try to add Widget containing markers to the map on click of a fab. ^ Hope this helps ^^

import 'dart:typed_data';

import 'package:flutter/cupertino.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:collection/collection.dart';
import 'dart:ui' as ui;
import 'package:synchronized/synchronized.dart';

class MapOptiTemp extends StatefulWidget {
  const MapOptiTemp({Key? key}) : super(key: key);

  @override
  State<MapOptiTemp> createState() => _MapOptiTempState();
}

class _MapOptiTempState extends State<MapOptiTemp> {
  // GoogleMapController? _mapController;
  List<Point> locations = [];
  late MarkersController markersController;
  Set<Marker> markers = {};
  double latitude = 0.0;
  double longitude = 0.0;

  @override
  void initState() {
    _loadLocationMarkers();
    super.initState();
  }

  void markersControllerListener() {
    // This listener only will be called when all the widgets are converted to images
    if (markersController.images != null) {
      try {
        setState(() {
          markers = locations
              .map<Marker>((point) => Marker(
                  markerId: MarkerId(point.id),
                  position: LatLng(point.lat, point.long),
                  onTap: () {},
                  icon: BitmapDescriptor.fromBytes(
                      markersController.images![locations.indexOf(point)])))
              .toSet();
        });
      } catch (error) {
        print("Error custom Map: $error");
      }
    }
  }

// Call this function in your initState
  void _loadLocationMarkers() async {
    if (mounted) {
      locations = [
        Point("init point 1", 45.413811, 6.991759),
        Point("init point 2", 45.414698, 6.988807)
      ];
    }
    markersController = MarkersController(
        value: List<Uint8List?>.filled(locations.length, null),
        childCount: locations.length);

    markersController.addListener(markersControllerListener);
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        // Add A generated list of custom markers in the tree of your app to capture the images and convert them to pngs to be added as bytes in the google marker
        ...(markersController.ready
            ? []
            : List.generate(
                markersController.childCount,
                (index) => Positioned(
                      left: -MediaQuery.of(context).size.width,
                      child: CustomMarker(
                        child: Icon(Icons.account_circle),
                        screenshotDelay: Duration.zero, // Remove delay
                        onImageCaptured: (data) {
                          markersController.updateRenderedImage(index, data);
                        },
                      ),
                    ))),

        GoogleMap(
          initialCameraPosition: CameraPosition(
              target: LatLng(45.449320496612295, 6.976185903919156)),
          myLocationButtonEnabled: false,
          tiltGesturesEnabled: false,
          zoomControlsEnabled: false,
          mapToolbarEnabled: false,
          mapType: MapType.hybrid,
          rotateGesturesEnabled: false,
          buildingsEnabled: false,
          onCameraMove: (object) {
            print("Markers : $markers");
            latitude = object.target.latitude;
            longitude = object.target.longitude;
          },
          minMaxZoomPreference: const MinMaxZoomPreference(0, 20),
          onMapCreated: (controller) async {},
          markers: markers,
        ),
        Positioned(
            bottom: 16,
            left: 0,
            right: 0,

            child: FloatingActionButton(
              child: Icon(Icons.add),
                onPressed: () async {
              print("latitude $latitude");
              print("longitude $longitude");
              locations.add(Point("New point", latitude, longitude));
              markersController.prepareAddWidget();
              markersController.childCount = locations.length;
              setState(() {});
            }))
      ],
    );
  }
}

class Point {
  String id = "";
  double lat = 0.0;
  double long = 0.0;

  Point(this.id, this.lat, this.long);
}

/// [MarkersController] handles the state of rendered markers and notify
/// listeners when all marker are rendered and captured.

class MarkersController extends ValueNotifier<List<Uint8List?>> {
  int childCount = 0;

  List<Uint8List?> renderedWidgets = List.empty(growable: true);

  MarkersController({required List<Uint8List?> value, required this.childCount})
      : super(value) {
    renderedWidgets = List<Uint8List?>.filled(childCount, null);
  }

  updateRenderedImage(int index, Uint8List? data) {
    renderedWidgets[index] = data;
    if (ready) {
      value = List.from(renderedWidgets);
    }
  }

  bool get ready => !renderedWidgets.any((image) => image == null);

  void prepareAddWidget() {
    renderedWidgets = renderedWidgets.toList();
    renderedWidgets.add(null);
    childCount++;
  }

  List<Uint8List>? get images => ready ? value.cast<Uint8List>() : null;
}

////////////////CUSTOM MARKER//////////////////////

/// a widgets that capture a png screenshot of its content and pass it through
/// [onImageCaptured].
class CustomMarker extends StatefulWidget {
  final Widget child;
  final Function(Uint8List?)? onImageCaptured;
  final Duration? screenshotDelay;

  const CustomMarker(
      {Key? key,
      required this.child,
      this.onImageCaptured,
      this.screenshotDelay})
      : super(key: key);

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

class _CustomMarkerState extends State<CustomMarker> {
  final GlobalKey key = GlobalKey();
  final Function eq = const ListEquality().equals;
  Uint8List? _lastImage;
  final lock = Lock();

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      lock.synchronized(() async {
        await Future.delayed(
            widget.screenshotDelay ?? const Duration(milliseconds: 500));
        final _image = await _capturePng(key);
        if (_lastImage == null || !eq(_lastImage!, _image)) {
          _lastImage = _image;
          widget.onImageCaptured?.call(_image);
        } else {
          widget.onImageCaptured?.call(_lastImage);
        }
      });
    });
    return RepaintBoundary(
      key: key,
      child: widget.child,
    );
  }

  Future<Uint8List?> _capturePng(GlobalKey iconKey) async {
    try {
      final RenderRepaintBoundary? boundary =
          iconKey.currentContext?.findRenderObject() as RenderRepaintBoundary?;
      if (kDebugMode && (boundary?.debugNeedsPaint ?? false)) {
        await Future.delayed(const Duration(milliseconds: 200));
        return _capturePng(iconKey);
      }
      ui.Image? image = await boundary?.toImage(pixelRatio: 3.0);
      ByteData? byteData =
          await image?.toByteData(format: ui.ImageByteFormat.png);
      var pngBytes = byteData?.buffer.asUint8List();
      return pngBytes;
    } catch (e) {
      return null;
    }
  }
}

theo-michel avatar Aug 13 '22 13:08 theo-michel