skip to Main Content

I try to create a custom marker (BitmapDescriptor) in Flutter to display it on a screen where I use Google Maps library.
The only problem I have is that my custom marker is positioned on the wrong place on the map. If I use a default marker instead then the position of the marker is displayed correct.
Below is my code and also 2 screenshots with the right position of the marker and also the wrong position of the marker (custom marker).

Screenshot Marker – Right Position

Marker - Right Position

Screenshot Marker – Wrong Position

Marker - Wrong Position

/// Load an asset from root library and return it back as a list of bytes
Future<Uint8List> loadImageAsBytes( {required String path, required Size size}) async {
  
    ByteData data = await rootBundle.load(path);
    ui.Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List(),
      targetWidth: size.width.toInt());
    ui.FrameInfo fi = await codec.getNextFrame();

    return ((await fi.image.toByteData(format: ui.ImageByteFormat.png)) ??
          ByteData(0))
      .buffer
      .asUint8List();
}


/// Load the icon from a list of bytes and return it back as an Image
Future<ui.Image> getImageFromPath(String imagePath, Size iconSize) async {

    Uint8List imageBytes = await loadImageAsBytes(size: iconSize, path: imagePath);

    final Completer<ui.Image> completer = Completer();

    ui.decodeImageFromList(imageBytes, (ui.Image img) {
        return completer.complete(img);
    });

    return completer.future;
}


 /// Here we draw a custom marker which will include the vehicle icon and vehicle plate number.
  Future<BitmapDescriptor> createCustomMarkerWithPlateRegAndIcon({required String iconPath, required Size size, required String plateReg}) async {

    // Create a TextBox where to place the Vehicle Plate Number
    TextSpan span = TextSpan(
        style: const TextStyle(
          height: 1.2,
          color: Colors.black,
          fontSize: 30.0,
          fontWeight: FontWeight.bold,
        ),
        text: plateReg);

    // Align the Vehicle Plate Registration to center in the above TextBox
    TextPainter tp = TextPainter(
      text: span,
      textAlign: TextAlign.center,
      textDirection: TextDirection.ltr,
    );
    // Computes the visual position of the glyphs for painting the text.
    tp.layout();

    // Start recording a new drawing
    ui.PictureRecorder recorder = ui.PictureRecorder();

    // Create an empty canvas where the drawing will be painted
    Canvas canvas = Canvas(recorder);

    // TextBox background colour for Plate Registration (I set it to dark yellow to reflect UK style)
    Paint textBgBoxPaint = Paint()
      ..color = const Color.fromARGB(255, 240, 200, 50);

    // Create a rectangle where to place the Veh Plate Number
    Rect rect = Rect.fromLTWH(0, 0, tp.width + 30, 50);

    // Draw on the canvas the rectangle with yellow background and rounded corners
    canvas.drawRRect(
      RRect.fromRectAndRadius(rect, const Radius.circular(20.0)),
      textBgBoxPaint,
    );

    // Add the Plate Registration to canvas and align it to center in the TextBox
    tp.paint(canvas, const Offset(15.0, 5.0));

    // Create a rectangle where will be placed the vehicle icon
    Rect rectForImage = Rect.fromLTWH(
      0,
      55,
      size.width,
      size.height,
    );

    // Add path to rectangle image
    canvas.clipPath(Path()..addRect(rectForImage));

    // Get the vehicle icon which will gonna be inserted on the canvas
    ui.Image image = await getImageFromPath(iconPath, size);

    // Paint the icon on the canvas
    paintImage(
        canvas: canvas,
        image: image,
        rect: rectForImage,
        fit: BoxFit.fitHeight);

    // Stop the drawing
    ui.Picture p = recorder.endRecording();

    // Take the whole drawing and convert it to a PNG and after to a byte data
    ByteData? pngBytes = await (await p.toImage(
      160,
      160,
    ))
        .toByteData(format: ui.ImageByteFormat.png);

    // This is an empty byte data for null safety
    ByteData emptyData = Uint8List(0).buffer.asByteData();

    // Convert the PNG from byteData to a list of bytes.
    Uint8List data = Uint8List.view((pngBytes ?? emptyData).buffer);

    // Return the PNG (in format list of bytes) as a Google Marker of type BitmapDescriptor
    return BitmapDescriptor.fromBytes(data);
  }

Thanks for reading this !

4

Answers


  1. Solution 1:

    Replace this with your code (add the same margin from the left):

    // Create a rectangle where will be placed the vehicle icon
    Rect rectForImage = Rect.fromLTWH(
      55,
      55,
      size.width,
      size.height,
    );
    

    Solution 2:

    // Get rect(rect.width) and the size.width(icon width)
    left_margin = (rect.width/2 - size.width/2);
    // Create a rectangle where will be placed the vehicle icon
    Rect rectForImage = Rect.fromLTWH(
      left_margin,
      55,
      size.width,
      size.height,
    );
    
    Login or Signup to reply.
  2. Please try this solution.

    Rect rectForImage = Rect.fromLTWH(
          (tp.width + 30)/2,
          55,
          size.width,
          size.height,
        );
    
    Login or Signup to reply.
  3. At first you need a class that will convert your marker image and then add text on top of the marker. You have to pass different marker size (like x1, x2, x3), you have to check your screen size and then pass the correct marker to the class below:

    import 'dart:async';
    import 'dart:typed_data';
    import 'dart:ui' as ui;
    import 'package:flutter/material.dart';
    import 'package:flutter/services.dart';
    
    class MarkersWithLabel {
      static Future<Uint8List?> getBytesFromCanvasDynamic(
          {required String iconPath,
          required String plateReg,
          required double fontSize,
          required Size iconSize}) async {
        final Paint paint = Paint()
          ..color = const Color.fromARGB(255, 240, 200, 50);
        final ui.PictureRecorder pictureRecorder = ui.PictureRecorder();
        final Canvas canvas = Canvas(pictureRecorder);
    
        //The label code
        TextSpan span = TextSpan(
          style: TextStyle(
            fontSize: fontSize,
            color: Colors.black,
            letterSpacing: 1.0,
          ),
          text: plateReg.length > 15 ? plateReg.substring(0, 15) + '...' : plateReg,
        );
    
        TextPainter painter = TextPainter(
            text: span,
            textAlign: TextAlign.center,
            textDirection: ui.TextDirection.ltr);
        painter.text = TextSpan(
            text:
                plateReg.length > 15 ? plateReg.substring(0, 15) + '...' : plateReg,
            style: TextStyle(
                fontSize: fontSize,
                letterSpacing: 2,
                color: Colors.black,
                fontWeight: FontWeight.w600));
    
        painter.layout(
          minWidth: 0,
        );
        int halfTextHeight = painter.height ~/ 2;
        double fortyPercentWidth = painter.width * 0.20;
        int textWidth = painter.width.toInt() + fortyPercentWidth.toInt();
        int textHeight = painter.height.toInt() + halfTextHeight;
    
        // Text box rectangle for Vehicle registration label
        Rect rect =
            Rect.fromLTWH(0, 0, textWidth.toDouble(), textHeight.toDouble());
        RRect rectRadius = RRect.fromRectAndRadius(rect, const Radius.circular(10));
    
        canvas.drawRRect(rectRadius, paint);
        painter.paint(
            canvas, Offset(fortyPercentWidth / 2, halfTextHeight.toDouble() / 2));
    
        double x = (textWidth) / 2;
        double y = textHeight.toDouble();
    
        Path arrow = Path()
          ..moveTo(x - 25, y)
          ..relativeLineTo(50, 0)
          ..relativeLineTo(-25, 25)
          ..close();
    
        // Draw an arrow under the Text Box Label
        canvas.drawPath(arrow, paint);
    
        // Load the icon from the path as a list of bytes
        final ByteData dataStart = await rootBundle.load(iconPath);
    
        // Resize the icon to a smaller size. If the icons by default have a huge size will be crazy big on the screen.
        // I discovered in my case that the best iconSize for a custom marker is width = 75 and height = 100.
        ui.Codec codec = await ui.instantiateImageCodec(
            dataStart.buffer.asUint8List(),
            targetWidth: iconSize.width.toInt());
        ui.FrameInfo fi = await codec.getNextFrame();
    
        Uint8List dataEnd =
            ((await fi.image.toByteData(format: ui.ImageByteFormat.png)) ??
                    ByteData(0))
                .buffer
                .asUint8List();
    
        ui.Image image = await _loadImage(Uint8List.view(dataEnd.buffer));
    
        //Move the icon from left to right or up to down
        canvas.drawImage(image, Offset(x - (image.width / 2), y + 25), Paint());
    
        ui.Picture p = pictureRecorder.endRecording();
    
        //This sets the total height of the icon along with the text
        ByteData? pngBytes = await (await p.toImage(
          textWidth < image.width ? image.width : textWidth,
          textHeight + image.height + 25,
        ))
            .toByteData(format: ui.ImageByteFormat.png);
    
        return pngBytes?.buffer.asUint8List();
      }
    
      static Future<ui.Image> _loadImage(Uint8List img) async {
        final Completer<ui.Image> completer = Completer();
        ui.decodeImageFromList(img, (ui.Image img) {
          return completer.complete(img);
        });
        return completer.future;
      }
    }
    

    Then call this class from anywhere in your app and set it to your icon variable.

    final Uint8List? icon = await MarkersWithLabel.getBytesFromCanvasDynamic(
            returnImageUrl(), pointsData.name!, returnFontSize());
    

    My returnImageUrl() and returnFontSize() functions just calculates my screen size and returns a pre defined image (like 1x, 2x) url from assets and fontSize. It helps my marker look symmetric for different screen sizes.

    Then just pass your icon to Map marker:

    MarkerId markerId = MarkerId('1');
        return Marker(
            markerId: markerId,
            position: LatLng(myLat, myLong),
            icon: BitmapDescriptor.fromBytes(icon!));
    

    I tried to make the marker as dynamic as possible. Play around with the MarkersWithLabel class to find your desired result. Check the screenshot for final result.

    Map With Green Marker

    Code to resize the icon was added otherwise the icons was huge on the map like in the photo below:

    Markers With Huge Size

    The code was improved because in some cases the markers was cropped like in the photo below:

    Cropped Markers

    At the end the markers will look like this (for iconSize I’ve used width = 75 and height = 100).
    Photo below:

    Marker With Normal Size and Not Cropped

    Login or Signup to reply.
  4. I have found an good solution to make custom widgets as Map Marker and wrote down an article , Check it out

    https://medium.com/p/5e50d382bc99

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search