Skip to content

Instantly share code, notes, and snippets.

@itsJoKr
Last active November 11, 2023 11:16
Show Gist options
  • Star 40 You must be signed in to star a gist
  • Fork 9 You must be signed in to fork a gist
  • Save itsJoKr/ce5ec57bd6dedf74d1737c1f39481913 to your computer and use it in GitHub Desktop.
Save itsJoKr/ce5ec57bd6dedf74d1737c1f39481913 to your computer and use it in GitHub Desktop.
Quick way to convert the widget to marker, not supposed to work with images.
import 'package:flutter/material.dart';
import 'dart:typed_data';
import 'package:flutter/rendering.dart';
import 'dart:ui' as ui;
/// This just adds overlay and builds [_MarkerHelper] on that overlay.
/// [_MarkerHelper] does all the heavy work of creating and getting bitmaps
class MarkerGenerator {
final Function(List<Uint8List>) callback;
final List<Widget> markerWidgets;
MarkerGenerator(this.markerWidgets, this.callback);
void generate(BuildContext context) {
WidgetsBinding.instance
.addPostFrameCallback((_) => afterFirstLayout(context));
}
void afterFirstLayout(BuildContext context) {
addOverlay(context);
}
void addOverlay(BuildContext context) {
OverlayState overlayState = Overlay.of(context);
OverlayEntry entry = OverlayEntry(
builder: (context) {
return _MarkerHelper(
markerWidgets: markerWidgets,
callback: (List<Uint8List> bitmapList) {
callback.call(bitmapList);
// Remove marker widgets from Overlay when finished
entry.remove();
},
);
},
maintainState: true);
overlayState.insert(entry);
}
}
/// Maps are embeding GoogleMap library for Andorid/iOS into flutter.
///
/// These native libraries accept BitmapDescriptor for marker, which means that for custom markers
/// you need to draw view to bitmap and then send that to BitmapDescriptor.
///
/// Because of that Flutter also cannot accept Widget for marker, but you need draw it to bitmap and
/// that's what this widget does:
///
/// 1) It draws marker widget to tree
/// 2) After painted access the repaint boundary with global key and converts it to uInt8List
/// 3) Returns set of Uint8List (bitmaps) through callback
class _MarkerHelper extends StatefulWidget {
final List<Widget> markerWidgets;
final Function(List<Uint8List>) callback;
const _MarkerHelper({Key key, this.markerWidgets, this.callback})
: super(key: key);
@override
_MarkerHelperState createState() => _MarkerHelperState();
}
class _MarkerHelperState extends State<_MarkerHelper> with AfterLayoutMixin {
List<GlobalKey> globalKeys = List<GlobalKey>();
@override
void afterFirstLayout(BuildContext context) {
_getBitmaps(context).then((list) {
widget.callback(list);
});
}
@override
Widget build(BuildContext context) {
return Transform.translate(
offset: Offset(MediaQuery.of(context).size.width, 0),
child: Material(
type: MaterialType.transparency,
child: Stack(
children: widget.markerWidgets.map((i) {
final markerKey = GlobalKey();
globalKeys.add(markerKey);
return RepaintBoundary(
key: markerKey,
child: i,
);
}).toList(),
),
),
);
}
Future<List<Uint8List>> _getBitmaps(BuildContext context) async {
var futures = globalKeys.map((key) => _getUint8List(key));
return Future.wait(futures);
}
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: ui.ImageByteFormat.png);
return byteData.buffer.asUint8List();
}
}
/// AfterLayoutMixin
mixin AfterLayoutMixin<T extends StatefulWidget> on State<T> {
@override
void initState() {
super.initState();
WidgetsBinding.instance
.addPostFrameCallback((_) => afterFirstLayout(context));
}
void afterFirstLayout(BuildContext context);
}
@Jip1912
Copy link

Jip1912 commented May 8, 2021

@Dereumaux-Adrien Thanks for the quick reply. The thing is, when I put the widget in the builder function, I get a different result. Take a look at this: https://i.gyazo.com/1fc24b7734e800375cd280744a1b4997.jpg. The left one is just made in the builder, the right one is a marker. The widgets have the exact same code:

        Container(
          color: Colors.red.withOpacity(0.2),
          width: 100,
          height: 100,
            child: CircleAvatar(
              radius: 50,
              backgroundImage: NetworkImage("https://flutter.dev/images/catalog-widget-placeholder.png"),
            ),
        );`

@Dereumaux-Adrien
Copy link

@Dereumaux-Adrien Thanks for the quick reply. The thing is, when I put the widget in the builder function, I get a different result. Take a look at this: https://i.gyazo.com/1fc24b7734e800375cd280744a1b4997.jpg. The left one is just made in the builder, the right one is a marker. The widgets have the exact same code:

        Container(
          color: Colors.red.withOpacity(0.2),
          width: 100,
          height: 100,
            child: CircleAvatar(
              radius: 50,
              backgroundImage: NetworkImage("https://flutter.dev/images/catalog-widget-placeholder.png"),
            ),
        );`

I see, to center the marker properly (as long as they are centered in their container):

anchor: Offset(0.5, 0.5),

But I can't help you for the space that is taken around (can lead to fail click), I still have this problem as well.

@Jip1912
Copy link

Jip1912 commented May 8, 2021

@Dereumaux-Adrien Thanks for the quick reply. The thing is, when I put the widget in the builder function, I get a different result. Take a look at this: https://i.gyazo.com/1fc24b7734e800375cd280744a1b4997.jpg. The left one is just made in the builder, the right one is a marker. The widgets have the exact same code:

        Container(
          color: Colors.red.withOpacity(0.2),
          width: 100,
          height: 100,
            child: CircleAvatar(
              radius: 50,
              backgroundImage: NetworkImage("https://flutter.dev/images/catalog-widget-placeholder.png"),
            ),
        );`

I see, to center the marker properly (as long as they are centered in their container):

anchor: Offset(0.5, 0.5),

But I can't help you for the space that is taken around (can lead to fail click), I still have this problem as well.

Where do you put that anchor?

@Dereumaux-Adrien
Copy link

Dereumaux-Adrien commented May 8, 2021

I hope you are using google_maps_flutter, then it should be:

Marker(
    markerId: MarkerId("user_position_marker"),
    position: position,
    anchor: Offset(0.5, 0.5),
    icon: CustomIconsGenerator().userMarkerDescriptor!,
  );

@Jip1912
Copy link

Jip1912 commented May 9, 2021

I hope you are using google_maps_flutter, then it should be:

Marker(
    markerId: MarkerId("user_position_marker"),
    position: position,
    anchor: Offset(0.5, 0.5),
    icon: CustomIconsGenerator().userMarkerDescriptor!,
  );

This fixed it, thanks a lot! Do you have any idea why the container's width is correct, but the height is wrong? Changing the height itself doesn't do anything. https://i.gyazo.com/1fc24b7734e800375cd280744a1b4997.jpg

@Dereumaux-Adrien
Copy link

@Jip1912 Sorry, I have no Idea about that..
Would you be so kind as to provide how you call your MarkerGenerator and use it to fill the marker icon?

@Jip1912
Copy link

Jip1912 commented May 11, 2021

@Jip1912 Sorry, I have no Idea about that..
Would you be so kind as to provide how you call your MarkerGenerator and use it to fill the marker icon?

MarkerGenerator(
      widget,
      (bitmaps) { 
          GeoPoint geo = geoPoint;
          _markers.add(Marker(
            markerId: new MarkerId(i.toString()),
            position: new LatLng(geo.latitude, geo.longitude),
            icon: BitmapDescriptor.fromBytes(bitmaps),
            anchor: Offset(0.5, 0.5),
            zIndex: i.toDouble(),
            onTap: () {
              setState(() {
                ......
              });
            }
          ));
          setState(() {});
        },
    ).generate(context);

I do it like this, with Set<Marker> _markers = {}; and

        GoogleMap(
          markers: _markers,
          ....

@Dereumaux-Adrien
Copy link

@Jip1912 I found why you have this problem, and I am having the same issue.
When first painting the widgets it paints them on a full screen, you can see that the ratio of your widget should be the same as the screen (width to height). You can see it more clearly by scaling your pixelRatio in the following line:
var image = await boundary.toImage(pixelRatio: 2);

I guess that our problem comes with the way we use generator or from your version of the generator itself.
It wouldbe even worse with a widget that has a bigger width than height, example from my side:
https://gyazo.com/c6ecb17e7d8cc085cd7ca7969c140981

we need to find how to limit the painting to the widget and not get the whole screen

@Jip1912
Copy link

Jip1912 commented May 12, 2021

@Dereumaux-Adrien Good catch. I noticed that I can change the size manually but the problem is that I get an error after I do this.

  Future<Uint8List> _getUint8List(GlobalKey markerKey) async {
    return new Future.delayed(const Duration(milliseconds: 20), () async {
      RenderRepaintBoundary boundary = markerKey.currentContext.findRenderObject();
      print("Boundary constraints: ${boundary.constraints}");
      boundary.layout(BoxConstraints(maxWidth: 360, maxHeight: 360, minWidth: 360, minHeight: 360), parentUsesSize: true);
      print("Boundary constraints after changing: ${boundary.constraints}");
      var image = await boundary.toImage(pixelRatio: 0.55);
      ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);
      return byteData.buffer.asUint8List();
    });
  }

This returns:

Boundary constraints: BoxConstraints(w=360.0, h=692.0)
Boundary constraints after changing: BoxConstraints(w=360.0, h=360.0)

So I manually changed the size to 360x360 I think. But then I get this error:

E/flutter ( 7254): [ERROR:flutter/lib/ui/ui_dart_state.cc(186)] Unhandled Exception: 'package:flutter/src/rendering/proxy_box.dart': Failed assertion: line 3089 pos 12: '!debugNeedsPaint': is not true.
E/flutter ( 7254): #0      _AssertionError._doThrowNew (dart:core-patch/errors_patch.dart:46:39)
E/flutter ( 7254): #1      _AssertionError._throwNew (dart:core-patch/errors_patch.dart:36:5)
E/flutter ( 7254): #2      RenderRepaintBoundary.toImage (package:flutter/src/rendering/proxy_box.dart:3089:12)
E/flutter ( 7254): #3      _MarkerHelperState._getUint8List.<anonymous closure> (package:studievriend/main/marker_generator.dart:108:32)
E/flutter ( 7254): #4      _MarkerHelperState._getUint8List.<anonymous closure> (package:studievriend/main/marker_generator.dart:102:65)
E/flutter ( 7254): #5      new Future.delayed.<anonymous closure> (dart:async/future.dart:315:39)
E/flutter ( 7254): #6      _rootRun (dart:async/zone.dart:1346:47)
E/flutter ( 7254): #7      _CustomZone.run (dart:async/zone.dart:1258:19)
E/flutter ( 7254): #8      _CustomZone.runGuarded (dart:async/zone.dart:1162:7)
E/flutter ( 7254): #9      _CustomZone.bindCallbackGuarded.<anonymous closure> (dart:async/zone.dart:1202:23)
E/flutter ( 7254): #10     _rootRun (dart:async/zone.dart:1354:13)
E/flutter ( 7254): #11     _CustomZone.run (dart:async/zone.dart:1258:19)
E/flutter ( 7254): #12     _CustomZone.bindCallback.<anonymous closure> (dart:async/zone.dart:1186:23)
E/flutter ( 7254): #13     Timer._createTimer.<anonymous closure> (dart:async-patch/timer_patch.dart:18:15)
E/flutter ( 7254): #14     _Timer._runTimers (dart:isolate-patch/timer_impl.dart:395:19)
E/flutter ( 7254): #15     _Timer._handleMessage (dart:isolate-patch/timer_impl.dart:426:5)
E/flutter ( 7254): #16     _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:184:12)
E/flutter ( 7254):

Maybe you can do something with this?

Edit

I found a solution! In the build function, wrap the RepaintBoundary with a SizedBox and a Stack like this:

        child: Stack(
            children: <Widget> [
            SizedBox(
              height: 360,
              width: 360,
              child: RepaintBoundary(
                key: markerKey,
                child: widget.markerWidgets,
              ),
            ),
          ]
        ),

I found the right width and height by the method I mentioned above.

@Dereumaux-Adrien
Copy link

@Jip1912 I tried again with the very first version from itsJoKr but only one widget and it works flawlessly with one widget:
https://gyazo.com/43846adf029557357090a83f708cd369

The code I called it with is simple as well:

MarkerGenerator(
      [
           widget
      ],
      (bitmaps) {
        _markers.add(Marker(
            markerId: new MarkerId("randomText"),
            position: new LatLng(0, 0),
            icon: BitmapDescriptor.fromBytes(bitmaps.first),
            anchor: Offset(0.5, 0.5),
            onTap: () {
              setState(() {});
            }));
        setState(() {});
      },
    ).generate(context);

We must have lost something when changing everything from list to single generator.
I will try to use it for my usecase just like so.

@Jip1912
Copy link

Jip1912 commented May 13, 2021

@Dereumaux-Adrien yeah that's a way easier solution, don't know why I didn't come up with that, but oh well it does the same thing. Is it also for you the case that the onTap is bit off? Like when I tap next to the marker it also get's triggered.

@Dereumaux-Adrien
Copy link

Dereumaux-Adrien commented May 15, 2021 via email

@Dereumaux-Adrien
Copy link

@Jip1912 I tried it with the default google_maps_flutter markers and they seem to have the same issue...
I guess it is increased with the size of the markers.

@mhmyesman
Copy link

I'm having an issue where the callback doesn't seem to be working until after I hot reload the emulator.

When it gets called after a hot reload, the image displayed is just blue.

This is the generator, I cleaned it a little, but didn't change from list, as some of you guys have: https://github.com/dawnn-team/dawnn_client/blob/map-markers/lib/src/util/generator.dart

And then this is how I create it, and later call generate(): https://github.com/dawnn-team/dawnn_client/blob/map-markers/lib/src/navigation/maps_page.dart#L105

@mhmyesman
Copy link

...I added a setState() after creating the generator object, so now the image gets painted.

However, it's still just blue.

@Jip1912
Copy link

Jip1912 commented May 16, 2021

I'm having an issue where the callback doesn't seem to be working until after I hot reload the emulator.

When it gets called after a hot reload, the image displayed is just blue.

This is the generator, I cleaned it a little, but didn't change from list, as some of you guys have: https://github.com/dawnn-team/dawnn_client/blob/map-markers/lib/src/util/generator.dart

And then this is how I create it, and later call generate(): https://github.com/dawnn-team/dawnn_client/blob/map-markers/lib/src/navigation/maps_page.dart#L105

I suppose you are working with images. Because it waits until the images are loaded, sometimes afterFirstLayout is not called, you should change it to this:

class MarkerGenerator {
...
void generate(BuildContext context) {
  if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
    SchedulerBinding.instance.addPostFrameCallback((_) => afterFirstLayout(context));
  } else {
    afterFirstLayout(context);
  }
}

@mhmyesman
Copy link

Hm, it's still giving me a blue image.

Any ideas?

@Jip1912
Copy link

Jip1912 commented May 26, 2021

Hm, it's still giving me a blue image.

Any ideas?

Try this: before calling MarkerGenerator, wait for all images to be loaded, just search up how to do this if you don't know.

@mhmyesman
Copy link

Sorry, couldn't find anything ☹️. Maybe you could give me a pointer?

@Jip1912
Copy link

Jip1912 commented May 30, 2021

Sorry, couldn't find anything ☹️. Maybe you could give me a pointer?

Are they networkimages? Then this should work https://stackoverflow.com/questions/46326584/how-do-i-tell-when-a-networkimage-has-finished-loading, otherwise maybe this https://stackoverflow.com/questions/25296563/how-to-wait-until-images-loaded-in-dart

@darwin-morocho
Copy link

Very bad code because you are using RepaintBoundary that consumes a lot of resources and sometimes it could fail. You could do it better using a custom painter https://www.youtube.com/watch?v=nIV9_FXSiYw

@Jip1912
Copy link

Jip1912 commented Jun 26, 2021

Very bad code because you are using RepaintBoundary that consumes a lot of resources and sometimes it could fail. You could do it better using a custom painter https://www.youtube.com/watch?v=nIV9_FXSiYw

Can you share the code?

@darwin-morocho
Copy link

Very bad code because you are using RepaintBoundary that consumes a lot of resources and sometimes it could fail. You could do it better using a custom painter https://www.youtube.com/watch?v=nIV9_FXSiYw

Can you share the code?

import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:meta/meta.dart' show required;

class Place {
  final String id, title, vicinity;
  final LatLng position;

  Place(
      {@required this.id,
      @required this.title,
      @required this.position,
      this.vicinity = ''});

  static Place fromJson(Map<String, dynamic> json) {
    final coords = List<double>.from(json['position']);

    return Place(
      id: json['id'],
      title: json['title'],
      vicinity: json['vicinity'] != null ? json['vicinity'] : '',
      position: LatLng(coords[0], coords[1]),
    );
  }
}


import 'package:flutter/material.dart';
import 'dart:ui' as ui;
import 'package:google_maps/models/place.dart';

class MyCustomMarker extends CustomPainter {
  final Place place;
  final int duration;

  MyCustomMarker(this.place, this.duration);

  void _buildMiniRect(Canvas canvas, Paint paint, double size) {
    paint.color = Colors.black;
    final rect = Rect.fromLTWH(0, 0, size, size);
    canvas.drawRect(rect, paint);
  }

  void _buildParagraph({
    @required Canvas canvas,
    @required List<String> texts,
    @required double width,
    @required Offset offset,
    Color color = Colors.black,
    double fontSize = 18,
    String fontFamily,
    TextAlign textAlign = TextAlign.left,
  }) {
    final ui.ParagraphBuilder builder = ui.ParagraphBuilder(
      ui.ParagraphStyle(
        maxLines: 2,
        textAlign: textAlign,
      ),
    );
    builder.pushStyle(
      ui.TextStyle(
        color: color,
        fontSize: fontSize,
        fontFamily: fontFamily,
      ),
    );

    builder.addText(texts[0]);

    if (texts.length > 1) {
      builder.pushStyle(ui.TextStyle(
        fontWeight: FontWeight.bold,
      ));
      builder.addText(texts[1]);
    }

    final ui.Paragraph paragraph = builder.build();

    paragraph.layout(ui.ParagraphConstraints(width: width));
    canvas.drawParagraph(
      paragraph,
      Offset(offset.dx, offset.dy - paragraph.height / 2),
    );
  }

  _shadow(Canvas canvas, double witdh, double height) {
    final path = Path();
    path.lineTo(0, height);
    path.lineTo(witdh, height);
    path.lineTo(witdh, 0);
    path.close();
    canvas.drawShadow(path, Colors.black, 5, true);
  }

  @override
  void paint(Canvas canvas, Size size) {
    final Paint paint = Paint();
    paint.color = Colors.white;

    final height = size.height - 15;
    _shadow(canvas, size.width, height);

    final RRect rrect = RRect.fromLTRBR(
      0,
      0,
      size.width,
      height,
      Radius.circular(0),
    );
    canvas.drawRRect(rrect, paint);

    final rect = Rect.fromLTWH(size.width / 2 - 2.5, height, 5, 15);

    canvas.drawRect(rect, paint);

    _buildMiniRect(canvas, paint, height);

    if (this.duration == null) {
      _buildParagraph(
        canvas: canvas,
        texts: [String.fromCharCode(Icons.gps_fixed.codePoint)],
        width: height,
        fontSize: 40,
        textAlign: TextAlign.center,
        offset: Offset(0, height / 2),
        color: Colors.white,
        fontFamily: Icons.gps_fixed.fontFamily,
      );
    } else {
      _buildParagraph(
        canvas: canvas,
        texts: ["${this.duration}\n", "MIN"],
        width: height,
        fontSize: 24,
        textAlign: TextAlign.center,
        offset: Offset(0, height / 2),
        color: Colors.white,
      );
    }

    _buildParagraph(
      canvas: canvas,
      texts: [this.place.title],
      width: size.width - height - 20,
      offset: Offset(height + 10, height / 2),
      fontSize: 24,
    );
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
}

Next you can use this code to get your custom Marker as an instance of UInt8List

Future<Uint8List> placeToMarker(Place place, {@required int duration}) async {
  ui.PictureRecorder recorder = ui.PictureRecorder();
  ui.Canvas canvas = ui.Canvas(recorder);
  final ui.Size size = ui.Size(350, 110);
  MyCustomMarker customMarker = MyCustomMarker(place, duration);
  customMarker.paint(canvas, size);
  ui.Picture picture = recorder.endRecording();
  final ui.Image image = await picture.toImage(
    size.width.toInt(),
    size.height.toInt(),
  );

  final ByteData byteData = await image.toByteData(
    format: ui.ImageByteFormat.png,
  );
  return byteData.buffer.asUint8List();
}

From: meedu.app

@Jip1912
Copy link

Jip1912 commented Aug 13, 2021

MyCustomMarker

Hi thanks for the code, sorry for the late reply. I've tried the code and it works as you showed, but how do I use my own widget with this? I don't know how the custom painter and canvas works and I'm not sure if I can use my own widget with this as marker. This is currently my custom marker:

SizedBox(
  width: 100,
  height: 140,
  child: Stack(
    clipBehavior: Clip.none,
    children: [
      Center(
        child: Container(
          //Rcolor: Colors.red.withOpacity(0.5),
          width: 100,
          height: 100,
            child: CircleAvatar(
              radius: 50,
              backgroundImage: _image,
            ),
          decoration: new BoxDecoration(
            shape: BoxShape.circle,
            border: new Border.all(
              color: Colors.amber[400],
              width: 4.0,
            ),
          ),
        ),
      ),
      Positioned.fill(
        bottom: 0,
        child: Align(
          alignment: Alignment.bottomCenter,
          child: Container(
            decoration: new BoxDecoration(
              borderRadius: BorderRadius.circular(4.0),
              color: kPrimaryColor
            ),
            child: Padding(
              padding: const EdgeInsets.symmetric(horizontal:4.0),
              child: Text(
                "€${element['uurloon'].toString()}",
                style: TextStyle(color: Colors.white, fontSize: 18),
              ),
            )
          ),
        ),
      )
    ],
  ),
);

How do I use this with your code?

@AymanProjects
Copy link

Excellent work! Thank you

@DarkMikey
Copy link

DarkMikey commented Sep 27, 2021

Good work, but I couldn't get it to fully work with network image and assets. Somehow, when I initially opened the map, nothing would happen. After that everything worked fine.

After all I switched to the canvas solution as mentioned by @itsJoKr which works very well and efficient.

https://stackoverflow.com/a/58954691/5589379

@Rumanali786
Copy link

can anyone tell me how to use this class
please answer me
Thank you

@SamiAlsubhi
Copy link

This is a working simple example, customize for your needs:

import 'dart:async';
import 'dart:ui' as ui;

import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';

Future<BitmapDescriptor?> getMarkerIconFromCanvas(String text) async {
  final ui.PictureRecorder pictureRecorder = ui.PictureRecorder();
  final Canvas canvas = Canvas(pictureRecorder);
  final Paint paint1 = Paint()..color = Colors.grey;
  const int size = 100; //change this according to your app
  canvas.drawCircle(const Offset(size / 2, size / 2), size / 2.0, paint1);
  TextPainter painter = TextPainter(textDirection: TextDirection.ltr);
  painter.text = TextSpan(
    text: text, //you can write your own text here or take from parameter
    style: const TextStyle(
        fontSize: size / 4, color: Colors.black, fontWeight: FontWeight.bold),
  );
  painter.layout();
  painter.paint(
    canvas,
    Offset(size / 2 - painter.width / 2, size / 2 - painter.height / 2),
  );

  final img = await pictureRecorder.endRecording().toImage(size, size);
  final data = await img.toByteData(format: ui.ImageByteFormat.png);
  final imageData = data?.buffer.asUint8List();
  if (imageData != null) {
    return BitmapDescriptor.fromBytes(imageData);
  }
  return null;
}

@mehmetext
Copy link

I found a link that explains how to use this class: https://stackoverflow.com/questions/52591556/custom-markers-with-flutter-google-maps-plugin

And I updated with null-safety:

import 'package:flutter/material.dart';
import 'dart:typed_data';
import 'package:flutter/rendering.dart';
import 'dart:ui' as ui;

/// This just adds overlay and builds [_MarkerHelper] on that overlay.
/// [_MarkerHelper] does all the heavy work of creating and getting bitmaps
class MarkerGenerator {
  final Function(List<Uint8List>) callback;
  final List<Widget> markerWidgets;

  MarkerGenerator(this.markerWidgets, this.callback);

  void generate(BuildContext context) {
    WidgetsBinding.instance
        .addPostFrameCallback((_) => afterFirstLayout(context));
  }

  void afterFirstLayout(BuildContext context) {
    addOverlay(context);
  }

  void addOverlay(BuildContext context) {
    OverlayState overlayState = Overlay.of(context);

    late OverlayEntry entry;

    entry = OverlayEntry(
        builder: (context) {
          return _MarkerHelper(
            markerWidgets: markerWidgets,
            callback: (List<Uint8List> bitmapList) {
              callback.call(bitmapList);
              // Remove marker widgets from Overlay when finished
              entry.remove();
            },
          );
        },
        maintainState: true);

    overlayState.insert(entry);
  }
}

/// Maps are embeding GoogleMap library for Andorid/iOS  into flutter.
///
/// These native libraries accept BitmapDescriptor for marker, which means that for custom markers
/// you need to draw view to bitmap and then send that to BitmapDescriptor.
///
/// Because of that Flutter also cannot accept Widget for marker, but you need draw it to bitmap and
/// that's what this widget does:
///
/// 1) It draws marker widget to tree
/// 2) After painted access the repaint boundary with global key and converts it to uInt8List
/// 3) Returns set of Uint8List (bitmaps) through callback
class _MarkerHelper extends StatefulWidget {
  final List<Widget> markerWidgets;
  final Function(List<Uint8List>) callback;

  const _MarkerHelper({
    Key? key,
    required this.markerWidgets,
    required this.callback,
  }) : super(key: key);

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

class _MarkerHelperState extends State<_MarkerHelper> with AfterLayoutMixin {
  List<GlobalKey> globalKeys = <GlobalKey>[];

  @override
  void afterFirstLayout(BuildContext context) {
    _getBitmaps(context).then((list) {
      widget.callback(list);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Transform.translate(
      offset: Offset(MediaQuery.of(context).size.width, 0),
      child: Material(
        type: MaterialType.transparency,
        child: Stack(
          children: widget.markerWidgets.map((i) {
            final markerKey = GlobalKey();
            globalKeys.add(markerKey);
            return RepaintBoundary(
              key: markerKey,
              child: i,
            );
          }).toList(),
        ),
      ),
    );
  }

  Future<List<Uint8List>> _getBitmaps(BuildContext context) async {
    var futures = globalKeys.map((key) => _getUint8List(key));
    return Future.wait(futures);
  }

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

/// AfterLayoutMixin
mixin AfterLayoutMixin<T extends StatefulWidget> on State<T> {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance
        .addPostFrameCallback((_) => afterFirstLayout(context));
  }

  void afterFirstLayout(BuildContext context);
}

@longdw
Copy link

longdw commented Oct 13, 2023

@mehmetext I use your code above, when I pass a Image.asset("xxx") to markerWidgets, it can't display the image, but can display Text

      Container(
        color: Colors.green,
        width: 100,
        height: 100,
        child: Row(
          children: [
            Image.asset("images/logo.png", width: 50, height: 50,),
            spaceH(6),
            Text(
              "rain",
              style: const TextStyle(
                  color: Colors.white,
                  fontSize: 16
              ),
            )
          ],
        ),
      )
    ], (List<Uint8List> data) {
             Call the map control here
    }).generate(context);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment