mapbox-maps-flutter icon indicating copy to clipboard operation
mapbox-maps-flutter copied to clipboard

Widget annotation

Open HasnainElexoft opened this issue 2 years ago • 14 comments

How can I add a widget annotation on map

HasnainElexoft avatar Oct 19 '23 10:10 HasnainElexoft

Here's how I did it, hope it helps you:

Widget -> Capture Widget -> Unit8list -> draw to mapbox

Result

vanvixi avatar Oct 31 '23 03:10 vanvixi

hello @vanvixi,
I have tried that and it works fine but I want to use flutter widget because I want to add 3d model as my annotation. is that possible ..? thanks I can achieve that using flutter_map but that plugin does not support terrain view as this plugin and I want to have the terrain view and the 3d model on the maps

fodedoumbouya avatar Nov 04 '23 21:11 fodedoumbouya

@fodedoumbouya Any update on this? Did you figure out how?

mattrltrent avatar Mar 01 '25 10:03 mattrltrent

yes and I use PointAnnotationOptions like this: PointAnnotationOptions( geometry: Point(coordinates: Position(position.longitude, position.latitude)), textOffset: [position.latitude, position.longitude], symbolSortKey: 10, image: image, //Uint8List iconSize: 0.5, );

fodedoumbouya avatar Mar 03 '25 06:03 fodedoumbouya

Image

I am displaying markers similar to you But depending on the zoom level to display markers again. markers when zoomed in will show images, text up showing far away will only show icons

when I zoom in it captures the widget -> causing lag, do you have any way to optimize

JasonEastar avatar May 15 '25 11:05 JasonEastar

For my part, I added a small timer so that the icons are repainted only once after the user finishes zooming in and moving the map. Without the timer, the icons would be repainted multiple times during map movement.

fodedoumbouya avatar May 15 '25 12:05 fodedoumbouya

Code Implementation:


void move(CameraChangedEventData? camera) {
  // Cancel the previous timer if it exists
  moveEndTimer?.cancel();

  // Set a new timer to delay the repainting
  moveEndTimer = Timer(
    const Duration(milliseconds: 250),
    () async {
      // Get the current camera state
      final camera = await mapController.getCameraState();
      final center = camera.center;
      final zoom = camera.zoom;

      // Add your repainting logic here
      // For example: repaintIcons(center, zoom);
    },
  );
}

fodedoumbouya avatar May 15 '25 12:05 fodedoumbouya

@fodedoumbouya I tried but it's still not very smooth, when zoom stops it still jerks when the widget is captured

JasonEastar avatar May 15 '25 12:05 JasonEastar

I have a question. do you draw all of your icons on the maps or you draw only the ones that are visible ..?

fodedoumbouya avatar May 15 '25 12:05 fodedoumbouya

I show all. But my marker data is not much. I am testing only about 10 markers but still lag

JasonEastar avatar May 15 '25 12:05 JasonEastar

yes 10 markers should not make it lag can I see how you draw it ..?

fodedoumbouya avatar May 15 '25 12:05 fodedoumbouya

**i am code like this. first i will create widgets to hold the UIs i want to capture then i capture all and display

Maybe my code is not very optimized, please help me check it. pls**

@override
  Widget build(BuildContext context) {
    return Positioned(
      top: -200,
      left: -200,
      child: Opacity(
        opacity: 1, // Ẩn widget nhưng vẫn render
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            // === MARKER ĐƠN GIẢN (Dùng khi zoom < 15) ===

            // Marker Livestream đơn giản
            RepaintBoundary(
              key: _simpleLivestreamKey,
              child: CustomMarkerWidget(
                boundaryKey: GlobalKey(),
                type: 'livestream',
                rating: 4.5,
              ),
            ),
            const SizedBox(height: 20),

            // Marker Post đơn giản
            RepaintBoundary(
              key: _simplePostKey,
              child: CustomMarkerWidget(
                boundaryKey: GlobalKey(),
                type: 'post',
                rating: 5.0,
              ),
            ),
            const SizedBox(height: 20),

            // Marker Image đơn giản
            RepaintBoundary(
              key: _simpleImageKey,
              child: CustomMarkerWidget(
                boundaryKey: GlobalKey(),
                type: 'image',
                rating: 5.0,
              ),
            ),
            const SizedBox(height: 20),

            // === MARKER CHI TIẾT (Dùng khi zoom >= 15) ===

            // Marker Livestream chi tiết
            RepaintBoundary(
              key: _detailLivestreamKey,
              child: PhotoMarkerWidget(
                boundaryKey: GlobalKey(),
                rating: '4.5',
                type: 'hot',
                imageUrl:
                    'https://images.unsplash.com/photo-1747134392291-33541db5f30f?q=80&w=1740&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
              ),
            ),
            const SizedBox(height: 20),

            // Marker Post chi tiết
            RepaintBoundary(
              key: _detailPostKey,
              child: PhotoMarkerWidget(
                boundaryKey: GlobalKey(),
                rating: '5.0',
                type: 'standard',
                imageUrl:
                    'https://images.unsplash.com/photo-1728044849221-851cf8587fac?q=80&w=1740&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
              ),
            ),
            const SizedBox(height: 20),

            // Marker Image chi tiết
            RepaintBoundary(
              key: _detailImageKey,
              child: PhotoMarkerWidget(
                boundaryKey: GlobalKey(),
                rating: '5.0',
                type: 'standard',
                imageUrl:
                    'https://images.unsplash.com/photo-1726064855870-9a438a9517bf?q=80&w=1740&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
              ),
            ),
          ],
        ),
      ),
    );
  }
}
Future<void> _captureAllMarkers() async {
    Map<String, Uint8List?> simpleMarkerImages = {};
    Map<String, Uint8List?> detailMarkerImages = {};

    print('Đang chụp marker đơn giản...');

    // Đợi một chút để đảm bảo widget đã render
    await Future.delayed(const Duration(milliseconds: 200));

    // Chụp marker đơn giản cho livestream
    Uint8List? simpleLivestreamMarker =
        await _captureMarker(_simpleLivestreamKey);
    if (simpleLivestreamMarker != null) {
      simpleMarkerImages['livestream'] = simpleLivestreamMarker;
    }

    // Chụp marker đơn giản cho post
    Uint8List? simplePostMarker = await _captureMarker(_simplePostKey);
    if (simplePostMarker != null) {
      simpleMarkerImages['post'] = simplePostMarker;
    }

    // Chụp marker đơn giản cho image
    Uint8List? simpleImageMarker = await _captureMarker(_simpleImageKey);
    if (simpleImageMarker != null) {
      simpleMarkerImages['image'] = simpleImageMarker;
    }

    // Chụp marker chi tiết cho livestream
    Uint8List? detailLivestreamMarker =
        await _captureMarker(_detailLivestreamKey);
    if (detailLivestreamMarker != null) {
      detailMarkerImages['livestream'] = detailLivestreamMarker;
    }

    // Chụp marker chi tiết cho post
    Uint8List? detailPostMarker = await _captureMarker(_detailPostKey);
    if (detailPostMarker != null) {
      detailMarkerImages['post'] = detailPostMarker;
    }

    // Chụp marker chi tiết cho image
    Uint8List? detailImageMarker = await _captureMarker(_detailImageKey);
    if (detailImageMarker != null) {
      detailMarkerImages['image'] = detailImageMarker;
    }

    // Gửi kết quả qua callback
    widget.onMarkersGenerated(simpleMarkerImages, detailMarkerImages);
  }
  Future<Uint8List?> _captureMarker(GlobalKey key) async {
    try {
      RenderRepaintBoundary? boundary = key.currentContext?.findRenderObject() as RenderRepaintBoundary?;
      if (boundary == null) {
        print('Boundary là null');
        return null;
      }

      ui.Image image = await boundary.toImage(pixelRatio: 3.0);
      ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png);
      return byteData?.buffer.asUint8List();
    } catch (e) {
      print('Lỗi khi chụp marker: $e');
      return null;
    }
  }

JasonEastar avatar May 15 '25 16:05 JasonEastar

Hey @JasonEastar,

Thanks for sharing your code! This confirms that the lag is very likely coming from the RenderRepaintBoundary.toImage() calls you're making every time the zoom level changes to switch marker appearances. Capturing Flutter widgets to Uint8List is an expensive operation.

The most effective optimization is to pre-capture all your necessary marker image variations (simple and detailed for each type) one time when your map or data loads.

Here's the core idea:

  • Use your _captureAllMarkers logic, but call it only once at the start.

  • Store the resulting Uint8List images.

  • Use the Mapbox SDK's addImage method to add all these captured images to the map's style, giving each a unique ID (e.g., 'simple_livestream_icon', 'detail_post_widget').

  • When you add your PointAnnotations, initially set their image property to the ID of the appropriate starting image (e.g., the simple icon ID).

  • In your move (or onCameraChanged) callback (with the timer), when the zoom threshold is crossed, find the relevant annotations and update their image property to the new required image ID (e.g., change from 'simple_livestream_icon' to 'detail_livestream_widget').

Benefit: Updating an annotation's image property using an image ID already in the style is extremely fast, as it's handled efficiently by the native Mapbox renderer. This avoids the repetitive, slow widget capture during map interaction.

This moves the heavy work (widget capture) upfront, leading to smooth zoom-based marker transitions.

fodedoumbouya avatar May 16 '25 07:05 fodedoumbouya

Great, thanks for the suggestion!

I am currently creating simple_livestream_icon with an available image, then using canvas to draw text on that image -> Store the resulting Uint8List image. The simple_livestream_icon part I have optimized to create as few images as possible. Which do you think is more optimal, using canvas or using widget?

For the detail_post_widget part: in case my detail_post_widget markers will be flexible with images and text in the widget. now the widget markers will be different 100 markers then there will be 100 different markers (containing images and text)... will taking a photo of all of them cause memory overflow... and is there a way to handle it more optimally.

Thank you very much.

JasonEastar avatar May 16 '25 09:05 JasonEastar