diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/ComposeInfoWindowAdapter.kt b/maps-compose/src/main/java/com/google/maps/android/compose/ComposeInfoWindowAdapter.kt index 43cc2755..367bfd48 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/ComposeInfoWindowAdapter.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/ComposeInfoWindowAdapter.kt @@ -57,8 +57,9 @@ internal class ComposeInfoWindowAdapter( private fun renderToImageView( markerNode: MarkerNode, content: @Composable () -> Unit, - ): View { + ): View? { val bitmap = mapView.renderComposableToBitmap(markerNode.compositionContext, content) + ?: return null return ImageView(mapView.context).apply { layoutParams = ViewGroup.LayoutParams(bitmap.width, bitmap.height) scaleType = ImageView.ScaleType.FIT_XY diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 8b463fa1..4042c8eb 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -43,7 +43,11 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.findViewTreeLifecycleOwner +import androidx.lifecycle.setViewTreeLifecycleOwner +import androidx.savedstate.compose.LocalSavedStateRegistryOwner +import androidx.savedstate.setViewTreeSavedStateRegistryOwner import com.google.android.gms.maps.GoogleMapOptions import com.google.android.gms.maps.LocationSource import com.google.android.gms.maps.MapView @@ -117,6 +121,15 @@ public fun GoogleMap( return } + // The Maps SDK measures Compose info-window content from its own Handler, asynchronously. + // If that measure lands after Compose has unparented the MapView (e.g. on LazyColumn + // recycling), the info window's ComposeView can no longer resolve a ViewTreeLifecycleOwner + // via its ancestors, since that tag lives on this AndroidView's holder rather than on the + // MapView itself. Pinning the owners directly onto the MapView keeps them resolvable from + // its own subtree regardless of where Compose has parented it. + val lifecycleOwner = LocalLifecycleOwner.current + val savedStateRegistryOwner = LocalSavedStateRegistryOwner.current + // rememberUpdatedState and friends are used here to make these values observable to // the subcomposition without providing a new content function each recomposition val mapClickListeners = remember { MapClickListeners() }.also { @@ -166,6 +179,8 @@ public fun GoogleMap( cameraPositionState.isLiteMode = options.liteMode == true mapViewFactory(context, options).also { mapView -> mapView.applyFocusability(focusable) + mapView.setViewTreeLifecycleOwner(lifecycleOwner) + mapView.setViewTreeSavedStateRegistryOwner(savedStateRegistryOwner) val componentCallbacks = object : ComponentCallbacks2 { override fun onConfigurationChanged(newConfig: Configuration) {} @@ -217,6 +232,8 @@ public fun GoogleMap( }, update = { mapView -> mapView.applyFocusability(focusable) + mapView.setViewTreeLifecycleOwner(lifecycleOwner) + mapView.setViewTreeSavedStateRegistryOwner(savedStateRegistryOwner) if (subcompositionJob == null) { subcompositionJob = parentCompositionScope.launchSubcomposition( mapUpdaterState, diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/MapComposeViewRender.kt b/maps-compose/src/main/java/com/google/maps/android/compose/MapComposeViewRender.kt index f1b864a4..8ee56fa0 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/MapComposeViewRender.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/MapComposeViewRender.kt @@ -63,11 +63,18 @@ private val unspecifiedMeasureSpec = * [com.google.android.gms.maps.GoogleMap.InfoWindowAdapter]) that take ownership of the view they * receive and re-parent it into their own hierarchy — which would otherwise crash with * "The specified child already has a parent" if handed a view still attached elsewhere. + * + * Returns `null` if this [MapView] is no longer attached to a window by the time [content] is + * measured. That happens when the Maps SDK's own async render request (which drives this call) + * lands after Compose has already unparented the [MapView] (e.g. `LazyColumn` recycling/detach): + * the composition never gets a real layout pass, so it measures to zero size. There's no info + * window worth rendering for a map that's already being torn down, so this is treated as "nothing + * to show" rather than an error. */ internal fun MapView.renderComposableToBitmap( parentContext: CompositionContext, content: @Composable () -> Unit, -): Bitmap { +): Bitmap? { val containerView = ensureContainerView() val composeView = ComposeView(context).apply { layoutParams = ViewGroup.LayoutParams( @@ -80,13 +87,21 @@ internal fun MapView.renderComposableToBitmap( containerView.addView(composeView) composeView.measure(unspecifiedMeasureSpec, unspecifiedMeasureSpec) - check(composeView.measuredWidth > 0 && composeView.measuredHeight > 0) { - "The info window content was measured to have a width or height of zero. " + - "Make sure that the content has a non-zero size." + val width = composeView.measuredWidth + val height = composeView.measuredHeight + + if (width <= 0 || height <= 0) { + containerView.removeView(composeView) + check(!isAttachedToWindow) { + "The info window content was measured to have a width or height of zero. " + + "Make sure that the content has a non-zero size." + } + return null } - composeView.layout(0, 0, composeView.measuredWidth, composeView.measuredHeight) - val bitmap = createBitmap(composeView.measuredWidth, composeView.measuredHeight) + composeView.layout(0, 0, width, height) + + val bitmap = createBitmap(width, height) bitmap.applyCanvas { composeView.draw(this) } containerView.removeView(composeView)