Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {}
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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)
Expand Down