From 882a5ff66c2b71057a4aa6b14f6ba9f20e4f22dc Mon Sep 17 00:00:00 2001 From: Justin Phan Date: Tue, 18 Aug 2026 19:39:22 +0700 Subject: [PATCH] fix(images): register image views mounted after the map is ready (Android) RNMBXImages only connects its children during addToMap and never reconciles afterwards, so an image view that arrives at a different moment is silently left unwired. Two places on Android: - RNMBXImagesManager.addView: a child mounted after the component was added to the map is appended to mImageViews but never given the map, because addToMap only visits the views that were present when it ran. It can then never place its own child view. - RNMBXImage.addToMap: the reverse order, and the common one. With Fabric the child view is mounted into RNMBXImage before RNMBXImage is mounted into RNMBXImages, so addView() runs while mMapView is still null - and since addView() does not call super, the child is not placed anywhere at all. A view in no hierarchy is never attached to a window, and a Drawee controller only submits its request on attach, so an inside that child never loads. Drawing a view into a bitmap does not require attachment, which is why such a marker snapshots complete except for its image, with no onLoad, no onLoadStart and no onError to show for it. Because a later child never registers, applications remount the whole block whenever a new image appears, which tears down and re-snapshots every image already on the map to add one. Measured in a production app: 90 reloads to add 2 images, markers blank for up to 5.7 seconds; afterwards 1 image costs 1 load. Placement is guarded on childView.parent == null, so addToMap stays idempotent - it runs again on style changes. --- .../rnmapbox/rnmbx/components/images/RNMBXImage.kt | 11 +++++++++++ .../rnmapbox/rnmbx/components/images/RNMBXImages.kt | 11 +++++++++++ .../rnmbx/components/images/RNMBXImagesManager.kt | 1 + 3 files changed, 23 insertions(+) diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/images/RNMBXImage.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/images/RNMBXImage.kt index 70bc703102..1060096c96 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/components/images/RNMBXImage.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/images/RNMBXImage.kt @@ -76,6 +76,17 @@ class RNMBXImage(private val mContext: ReactApplicationContext, private val mMan // region add/remove to Map fun addToMap(mapView: RNMBXMapView) { mMapView = mapView + + // The child view is usually inserted before we know the map: the child is mounted into + // this view before this view is mounted into RNMBXImages, so addView() above ran while + // mMapView was still null and the child was not placed anywhere. A view that is in no + // hierarchy is never attached to a window, and a Drawee controller only submits its + // request on attach - so an inside the child never loads, and the bitmap we + // capture shows everything except that image. Place the child now that the map is known. + val childView = mChildView + if (childView != null && childView.parent == null) { + mapView.offscreenAnnotationViewContainer?.addView(childView) + } } // endregion } \ No newline at end of file diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/images/RNMBXImages.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/images/RNMBXImages.kt index c6940963be..58e8a355ce 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/components/images/RNMBXImages.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/images/RNMBXImages.kt @@ -177,6 +177,17 @@ class RNMBXImages(context: Context, private val mManager: RNMBXImagesManager) : return style != null && imageId?.let { style.getStyleImage(it) } != null } + /** + * Give the map to an image view that was mounted after this component was added to the map. + * + * addToMap only visits the views present at that moment, and the view manager does not pass + * the map to a later child, so such a child would never place its own child view and never + * register an image. + */ + fun attachImageViewIfOnMap(image: RNMBXImage) { + mMapView?.let { image.addToMap(it) } + } + override fun addToMap(mapView: RNMBXMapView) { super.addToMap(mapView) diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/images/RNMBXImagesManager.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/images/RNMBXImagesManager.kt index 88e466646e..9eb4003f75 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/components/images/RNMBXImagesManager.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/images/RNMBXImagesManager.kt @@ -239,6 +239,7 @@ class RNMBXImagesManager(private val mContext: ReactApplicationContext) : parent.mImageViews.add(childPosition, childView) childView.nativeImageUpdater = parent + parent.attachImageViewIfOnMap(childView) } override fun removeView(parent: RNMBXImages, view: View) {