-
-
Save itsJoKr/ce5ec57bd6dedf74d1737c1f39481913 to your computer and use it in GitHub Desktop.
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); | |
} |
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?
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_FXSiYwCan 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
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?
Excellent work! Thank you
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.
can anyone tell me how to use this class
please answer me
Thank you
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;
}
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);
}
@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);
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