Skip to content
Merged
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 @@ -15,64 +15,55 @@
package com.google.maps.android.compose

import android.view.View
import androidx.compose.ui.platform.ComposeView
import android.view.ViewGroup
import android.widget.ImageView
import androidx.compose.runtime.Composable
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.MapView
import com.google.android.gms.maps.model.Marker

/**
* An InfoWindowAdapter that returns a [ComposeView] for drawing a marker's
* An InfoWindowAdapter that returns a [View] for drawing a marker's
* info window.
*
* Note: As of version 18.0.2 of the Maps SDK, info windows are drawn by
* creating a bitmap of the [View]s returned in the [GoogleMap.InfoWindowAdapter]
* interface methods. The returned views are never attached to a window,
* instead, they are drawn to a bitmap canvas. This breaks the assumption
* [ComposeView] makes where it must eventually be attached to a window. As a
* workaround, the contained window is temporarily attached to the MapView so
* that the contents of the ComposeViews are rendered.
* interface methods. For [getInfoContents], the Maps SDK also re-parents the
* returned view into its own default info-window frame, so the view handed back
* must never already have a parent.
*
* As of compose-ui 1.10, [ComposeView] skips drawing when detached (isShown == false).
* To work around this, the view is kept attached to the [MapView] until the info window
* is closed, at which point [disposeForMarker] must be called.
* [ComposeView][androidx.compose.ui.platform.ComposeView] content is therefore
* rendered into a plain [android.graphics.Bitmap] up front (while briefly attached
* to [mapView] to drive composition), and that bitmap is wrapped in a fresh,
* parent-less [ImageView] for the Maps SDK to use.
*/
internal class ComposeInfoWindowAdapter(
private val mapView: MapView,
private val markerNodeFinder: (Marker) -> MarkerNode?
) : GoogleMap.InfoWindowAdapter {

private val renderHandles = mutableMapOf<Marker, ComposeUiViewRenderer.RenderHandle>()

override fun getInfoContents(marker: Marker): View? {
val markerNode = markerNodeFinder(marker) ?: return null
val content = markerNode.infoContent
if (content == null) {
return null
}
val view = ComposeView(mapView.context).apply {
setContent { content(marker) }
}
renderHandles.remove(marker)?.dispose()
renderHandles[marker] = mapView.startRenderingComposeView(view, markerNode.compositionContext)
return view
val content = markerNode.infoContent ?: return null
return renderToImageView(markerNode) { content(marker) }
}

override fun getInfoWindow(marker: Marker): View? {
val markerNode = markerNodeFinder(marker) ?: return null
val infoWindow = markerNode.infoWindow
if (infoWindow == null) {
return null
}
val view = ComposeView(mapView.context).apply {
setContent { infoWindow(marker) }
}
renderHandles.remove(marker)?.dispose()
renderHandles[marker] = mapView.startRenderingComposeView(view, markerNode.compositionContext)
return view
val infoWindow = markerNode.infoWindow ?: return null
return renderToImageView(markerNode) { infoWindow(marker) }
}

fun disposeForMarker(marker: Marker) {
renderHandles.remove(marker)?.dispose()
private fun renderToImageView(
markerNode: MarkerNode,
content: @Composable () -> Unit,
): View {
val bitmap = mapView.renderComposableToBitmap(markerNode.compositionContext, content)
return ImageView(mapView.context).apply {
layoutParams = ViewGroup.LayoutParams(bitmap.width, bitmap.height)
scaleType = ImageView.ScaleType.FIT_XY
setImageBitmap(bitmap)
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -125,15 +125,7 @@ internal class MapApplier(
inputHandlerCallback = { onInfoWindowClick }
)
}
val composeInfoWindowAdapter = ComposeInfoWindowAdapter(
mapView,
markerNodeFinder = { marker ->
decorations.firstOrNull { it is MarkerNode && it.marker == marker }
as MarkerNode?
}
)
map.setOnInfoWindowCloseListener { marker ->
composeInfoWindowAdapter.disposeForMarker(marker)
decorations.findInputCallback<MarkerNode, Marker, Unit>(
nodeMatchPredicate = { it.marker == marker },
marker = marker,
Expand Down Expand Up @@ -217,7 +209,15 @@ internal class MapApplier(
)
}
})
map.setInfoWindowAdapter(composeInfoWindowAdapter)
map.setInfoWindowAdapter(
ComposeInfoWindowAdapter(
mapView,
markerNodeFinder = { marker ->
decorations.firstOrNull { it is MarkerNode && it.marker == marker }
as MarkerNode?
}
)
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.google.maps.android.compose

import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.view.View
import android.view.ViewGroup
Expand All @@ -27,6 +28,9 @@ import androidx.compose.runtime.currentComposer
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCompositionContext
import androidx.compose.ui.platform.AbstractComposeView
import androidx.compose.ui.platform.ComposeView
import androidx.core.graphics.applyCanvas
import androidx.core.graphics.createBitmap
import com.google.android.gms.maps.MapView
import java.io.Closeable

Expand All @@ -47,6 +51,49 @@ internal fun MapView.renderComposeViewOnce(
}
}

private val unspecifiedMeasureSpec =
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)

/**
* Renders [content] into a standalone [Bitmap] by temporarily attaching a [ComposeView] as a
* descendant of this [MapView], measuring, laying out and drawing it, then detaching it again.
*
* Unlike [startRenderingComposeView], the returned bitmap has no ties back to this [MapView] or
* its composition once this function returns, so it is safe to hand off to APIs (such as
* [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.
*/
internal fun MapView.renderComposableToBitmap(
parentContext: CompositionContext,
content: @Composable () -> Unit,
): Bitmap {
val containerView = ensureContainerView()
val composeView = ComposeView(context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
)
setParentCompositionContext(parentContext)
setContent(content)
}
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."
}
composeView.layout(0, 0, composeView.measuredWidth, composeView.measuredHeight)

val bitmap = createBitmap(composeView.measuredWidth, composeView.measuredHeight)
bitmap.applyCanvas { composeView.draw(this) }

containerView.removeView(composeView)

return bitmap
}

/**
* Prepares [view] for a rendering by attaching it as a descendant of this [MapView].
* This is a trick to enable [ComposeView] to start its composition, as it requires being attached
Expand Down