Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Builds the widget and returns it's bitmap through callback
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: callback,
);
},
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);
}
@skppetr0007

This comment has been minimized.

Copy link

@skppetr0007 skppetr0007 commented Dec 7, 2019

Thanks for sharing! Very helpful.

@shinayser

This comment has been minimized.

Copy link

@shinayser shinayser commented Dec 16, 2019

Nice code! But when I try to use it I get the following error:

Unhandled Exception: 'package:flutter/src/rendering/proxy_box.dart': Failed assertion: line 2904 pos 12: '!debugNeedsPaint': is not true.

Any clue about what could be causing this?

@itsJoKr

This comment has been minimized.

Copy link
Owner Author

@itsJoKr itsJoKr commented Dec 17, 2019

@shinayser Hey, thanks for checking it out. Can you please try with having a marker widget as Text("Marker") and then report to me if that doesn't work.

About your error. I'm not really sure why it happens. All I know is that when you try to get RenderObject of some widgets it might throw an error like that. One of those widgets is Offstage, so can you just try with only Text so we can know if it's something to do with your widget tree.

@skppetr0007

This comment has been minimized.

Copy link

@skppetr0007 skppetr0007 commented Dec 17, 2019

@itsJoKr this worked with flutter v1.9.1 and doesn't work with v1.12.1 because of error that @shinayser pointed out

@itsJoKr

This comment has been minimized.

Copy link
Owner Author

@itsJoKr itsJoKr commented Dec 17, 2019

@skippetr That's weird I've just tried it on master branch 1.13.3-pre and it works. Are you sure you don't have some widgets that could fail to fetch RenderObject? Have you tried with the simplest Text as a marker?

@shinayser

This comment has been minimized.

Copy link

@shinayser shinayser commented Dec 17, 2019

What was causing the problem is that the widget is never being painted. Since you are "rearranging" the entries, it nevers get painted (perhaps a problem with the maps' plataform view?).
If you remove the "rearrange" call, it works nicely but gives another problem: the widgets are painted over the map, on top left screen.

So, to fix that, I put the widgets in a positioned widget with -1000 left position. Its a huge workaround, but works 🤣🤣🤣

@itsJoKr

This comment has been minimized.

Copy link
Owner Author

@itsJoKr itsJoKr commented Dec 18, 2019

@skippetr @shinayser I'm able to reproduce it now. Unfortunately, something changed with how overlay works and I don't know what or why. I guess some kind of optimization that completely skips the painting when it determines that overlay is not visible.

I've updated the gist that should translate and build the widgets off the screen, as I think that's the next best solution.

One other possible solution is to use _MarkerHelper directly (without overlays) and put it in a stack below the map. For my use case, I already had a map in stack with some buttons on top of it. So you can put _MarkerHelper below the map, thus it will not be visible and you will receive a callback with bitmaps.

@bdairy

This comment has been minimized.

Copy link

@bdairy bdairy commented Dec 23, 2019

Thanks for the great solution you saved my day,, I have one minor issue though,, Markers are not loaded instantly, u have to move the map a little in order to show it,, any idea what could be the issue?? basically in every map move I am calling an API to get markers within the bounding box.. Initially I am calling the API once map is loaded,, but as you see in the video,, I need to move the map a little to show the first marker.
map-app2

@willyk311

This comment has been minimized.

Copy link

@willyk311 willyk311 commented Jan 15, 2020

Thanks for your code, its very helpful!
Altho I'm still having a problem. The map will be shown on the initial start of the map, but the callback function won't be called and I get this
Unhandled Exception: 'package:flutter/src/rendering/proxy_box.dart': Failed assertion: line 2904 pos 12: '!debugNeedsPaint': is not true.
But when I navigate through my app via the BottomNavigationBar and go back to the Stateful Map Widget, btw the page is then created completely new by calling the widgets "constructor", the marker is correctly shown.

I have put the MarkerHelper directly in the Stack below the map. I really have no clue, although the solution might be trivial.

@itsJoKr

This comment has been minimized.

Copy link
Owner Author

@itsJoKr itsJoKr commented Jan 16, 2020

@willyk311 Thanks for checking it out. If I understand you, the generator is having issues on initial build of the map, but works on later rebuild. I've tried to reproduce it but it all works fine for me.

Can you remove it from the stack and try to use it as a separate overlay. So you should only add this to initState:

    MarkerGenerator(markerWidgets, (bitmaps) {      
        setState(() { // set state to rebuild widget and set markers on google map       
            markers = mapBitmapsToMarkers(bitmaps);      
        });    
    }).generate(context); 
@willyk311

This comment has been minimized.

Copy link

@willyk311 willyk311 commented Jan 16, 2020

If I use your MapScreen.dart from your github it works with the MarkerGenerator from this gist. But when I use it in my own file the marker doesn't look like it should.

The widget I want to turn into a bitmap looks like this:

Container(
        alignment: Alignment.center,
        padding: EdgeInsets.only(top: 10.0),
        height: _wHeight, // its the size of the DecorationImage below
        width: _wWidth,
        decoration: BoxDecoration(
          image: DecorationImage(
            image: AssetImage("assets/images/example.png"),
          ),
        ),
        child: Row(
          mainAxisSize: MainAxisSize.max,
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            Image(
                image: AssetImage("assets/images/icon.png"),
                color: _iconColor),
            Text("Test 123",
                style: TextStyle(
                    fontFamily: "Roboto",
                    fontSize: 18.0,
                    color: Colors.white,
                    fontWeight: FontWeight.bold)),
          ],
        )
    )

What I get on the initial start with the MarkerGenerator from this gist and the call from initState is a white string, which doesn't have the correct FontSize and two stupid looking underlines in the color _iconColor of the icon.png. And of course with the mentioned exception.
When I rebuild the map, it looks more or less correct, while still having the two underlines.
When I put the MarkerHelper call in the Stack below the googleMap, I dont get anything on the initial start, but on the rebuild I dont have those underlines.

@itsJoKr

This comment has been minimized.

Copy link
Owner Author

@itsJoKr itsJoKr commented Jan 16, 2020

Hey @willyk311 That repo was not really maintained as I thought repo was not needed. I've now updated the repo and this gist.

It should fix your problem with weird fonts and yellow underlines. But I'm not sure I can reproduce your issue when it doesn't draw markers in the first build. Can you try to clone the repo and see if everything works for you now: https://github.com/itsJoKr/markers_generator_sample

@willyk311

This comment has been minimized.

Copy link

@willyk311 willyk311 commented Jan 17, 2020

Thanks for your immediate responses. It really seems to be a problem with my Widget that I want to turn into the bitmap. I have used your widget from your _getMarkerWidget() method and it is shown as intended. It looks like its because of the Images I have in my Widget. Because the only thing that gets displayed is the Text, which is the child of the row. The text is even displayed correctly.

And I dont get the exception anymore. It really is weird with Android Studio and the emulator. I had stuff printed out in the console, which wasnt in the code anymore. Seems like the exception it showed me, was also an old one, because I tried on a physical device and I had no exception whatsoever.

@willyk311

This comment has been minimized.

Copy link

@willyk311 willyk311 commented Jan 20, 2020

Still couldnt get it to work. Have you tested it with widgets that contain images?

@wbonnefond

This comment has been minimized.

Copy link

@wbonnefond wbonnefond commented Jan 20, 2020

I'm running into the same problem with Images in the markers as well. My initial thought is that since image loading is asynchronous (even with bundled assets) the MarkerGenerator generates the marker bitmaps before the images are loaded and in the RenderObject. I'm going to see if there's a way to postpone marker generation until the images finish loading.

@itsJoKr

This comment has been minimized.

Copy link
Owner Author

@itsJoKr itsJoKr commented Jan 21, 2020

@willyk311 I've tried it now and the problem is indeed what @wbonnefond said. One way to make it work:

  1. Add image somewhere on the screen (it can be below the map, invisible) so that Flutter loads the image into the memory
  2. Wrap MarkerGenerator.generate with some delay: Future.delayed(Duration(milliseconds: 50), () {

Not a nice solution and I don't think this MarkerGenerator is supposed to support this kind of stuff. Proper implementation for any marker would be that you draw it directly on to the canvas and convert that to bitmap (without using widgets, just pure dart:ui). Obviously this would take more time. The MarkerGenerator is like quick way out of this but works just for simple cases.

@willyk311

This comment has been minimized.

Copy link

@willyk311 willyk311 commented Jan 23, 2020

@wbonnefond @itsJoKr
Thanks guys. Yeah I tried it with precacheImage not knowing, that its also asynchronous haha. I've read somewhere that with some kind of timeout before getting the renderObject it should work, but it didnt. Wrapping MarkerGenerator.generate's content like you mentioned in Future.delayed did the work! I just hope that I dont have to adjust the time when having many icons or icons with bigger size.

For adding the images somewhere on the screen/off-screen I would have to create a widget between my main and map widget. Too bad there isnt a more elegant way of doing this.

@magicblackbird

This comment has been minimized.

Copy link

@magicblackbird magicblackbird commented Feb 29, 2020

Thanks for sharing, you saved my day.

@daniloapr

This comment has been minimized.

Copy link

@daniloapr daniloapr commented Aug 30, 2020

Thanks for sharing! I have been looking for a solution like this for a while.

It works perfectly with simple widget such as Container, Column, Row, Text, Icon... It is not working when the widget has an Image.asset, though. It is not much problem, just giving you a feedback :D

@itsJoKr

This comment has been minimized.

Copy link
Owner Author

@itsJoKr itsJoKr commented Aug 31, 2020

@daniloapr True. This generator is a quick way to convert a simple widget to marker. It is not supposed to support image, but that can be also fixed with a bit of hack I've explained in the comment above: https://gist.github.com/itsJoKr/ce5ec57bd6dedf74d1737c1f39481913#gistcomment-3146815

@daniloapr

This comment has been minimized.

Copy link

@daniloapr daniloapr commented Sep 1, 2020

Yes, I saw that! And I agree that adding images is not the main purpose of the MarkerGenerator.

@princeteck

This comment has been minimized.

Copy link

@princeteck princeteck commented Sep 27, 2020

@daniloapr True. This generator is a quick way to convert a simple widget to marker. It is not supposed to support image, but that can be also fixed with a bit of hack I've explained in the comment above: https://gist.github.com/itsJoKr/ce5ec57bd6dedf74d1737c1f39481913#gistcomment-3146815

What if the image is in form of base64 string or a network image ? like Image.memory(base64Decode(image.split(',').last)) or CachedNetworkImage(imageUrl: image). It works out of the google map plugin but not inside even if I am rendering the image in list view below the map.

@itsJoKr

This comment has been minimized.

Copy link
Owner Author

@itsJoKr itsJoKr commented Sep 28, 2020

@daniloapr True. This generator is a quick way to convert a simple widget to marker. It is not supposed to support image, but that can be also fixed with a bit of hack I've explained in the comment above: https://gist.github.com/itsJoKr/ce5ec57bd6dedf74d1737c1f39481913#gistcomment-3146815

What if the image is in form of base64 string or a network image ? like Image.memory(base64Decode(image.split(',').last)) or CachedNetworkImage(imageUrl: image). It works out of the google map plugin but not inside even if I am rendering the image in list view below the map.

CacheNetworkImage is async fetching images so that's problematic. As said, you can draw directly to cavnas by yourself and convert that to marker: https://stackoverflow.com/questions/56597739/how-to-customize-google-maps-marker-icon-in-flutter/56644186

@princeteck

This comment has been minimized.

Copy link

@princeteck princeteck commented Sep 28, 2020

@daniloapr True. This generator is a quick way to convert a simple widget to marker. It is not supposed to support image, but that can be also fixed with a bit of hack I've explained in the comment above: https://gist.github.com/itsJoKr/ce5ec57bd6dedf74d1737c1f39481913#gistcomment-3146815

What if the image is in form of base64 string or a network image ? like Image.memory(base64Decode(image.split(',').last)) or CachedNetworkImage(imageUrl: image). It works out of the google map plugin but not inside even if I am rendering the image in list view below the map.

CacheNetworkImage is async fetching images so that's problematic. As said, you can draw directly to cavnas by yourself and convert that to marker: https://stackoverflow.com/questions/56597739/how-to-customize-google-maps-marker-icon-in-flutter/56644186

That is not working as Future<BitmapDescriptor> cannot be assigned to BitmapDescriptor.

@princeteck

This comment has been minimized.

Copy link

@princeteck princeteck commented Sep 28, 2020

also The argument type 'Image (where Image is defined in /Library/flutter/packages/flutter/lib/src/widgets/image.dart)' can't be assigned to the parameter type 'Image (where Image is defined in /Library/flutter/bin/cache/pkg/sky_engine/lib/ui/painting.dart)'.

@princeteck

This comment has been minimized.

Copy link

@princeteck princeteck commented Sep 29, 2020

I was able to make the changes as needed and now their are 2 issues left.

  1. sometimes some images are only shown or sometimes none and sometimes all.
  2. only working on simulator but not real devices.
List<Marker> mapBitmapsToMarkers(List<Uint8List> bitmaps) {
    List<Marker> markersList = [];
    bitmaps.asMap().forEach((i, bmp) async {
      final city = cities[i];
      ui.Codec codec = await ui.instantiateImageCodec(
          base64Decode(city.image.split(",").last).buffer.asUint8List());
      ui.FrameInfo fi = await codec.getNextFrame();

      print(city.name);

      final BitmapDescriptor marker =
          await getMarkerIcon(fi.image, Size(100.0, 100.0));
      markersList.add(
        Marker(
          markerId: MarkerId(city.name),
          position: city.position,
          // icon: BitmapDescriptor.fromBytes(bmp), // type 1
          // icon: BitmapDescriptor.fromBytes(
              base64Decode(city.image.split(",").last)), // type 2
          icon: marker, // type 3
          onTap: () => print(city.name),
        ),
      );
    });
    return markersList;
  }

type 3 is what I am looking at as the solution.

sometimes the images are shown on even physical device but most of the time not at all.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.