Skip to content
Draft
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 @@ -32,6 +32,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
Expand All @@ -48,6 +49,7 @@ import com.google.maps.android.compose.GoogleMap
import com.google.maps.android.compose.MapsComposeExperimentalApi
import com.google.maps.android.compose.MarkerInfoWindow
import com.google.maps.android.compose.clustering.Clustering
import com.google.maps.android.compose.clustering.ClusteringMarkerProperties
import com.google.maps.android.compose.clustering.rememberClusterManager
import com.google.maps.android.compose.clustering.rememberClusterRenderer
import com.google.maps.android.compose.rememberCameraPositionState
Expand Down Expand Up @@ -160,6 +162,7 @@ private fun DefaultClustering(items: List<MyItem>) {
@OptIn(MapsComposeExperimentalApi::class)
@Composable
private fun CustomUiClustering(items: List<MyItem>) {
var selectedItem by remember { mutableStateOf<MyItem?>(null) }
Clustering(
items = items,
// Optional: Handle clicks on clusters, cluster items, and cluster item info windows
Expand All @@ -169,6 +172,7 @@ private fun CustomUiClustering(items: List<MyItem>) {
},
onClusterItemClick = {
Log.d(TAG, "Cluster item clicked! $it")
selectedItem = if (selectedItem == it) null else it
false
},
onClusterItemInfoWindowClick = {
Expand All @@ -183,7 +187,21 @@ private fun CustomUiClustering(items: List<MyItem>) {
)
},
// Optional: Custom rendering for non-clustered items
clusterItemContent = null,
clusterItemContent = { item ->
val isSelected = item == selectedItem
if (isSelected) {
ClusteringMarkerProperties(
anchor = Offset(0.5f, 0.5f),
zIndex = 1.0f
)
}
CircleContent(
modifier = Modifier.size(if (isSelected) 40.dp else 20.dp),
text = "",
color = if (isSelected) Color.Red else Color.Green,
)
},
clusterContentAnchor = Offset(0.5f, 0.5f),
// Optional: Customization hook for clusterManager and renderer when they're ready
onClusterManager = { clusterManager ->
(clusterManager.renderer as DefaultClusterRenderer).minClusterSize = 2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import androidx.compose.ui.platform.AbstractComposeView
import androidx.core.graphics.applyCanvas
import androidx.core.view.doOnAttach
import androidx.core.view.doOnDetach
import androidx.compose.ui.geometry.Offset
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.model.BitmapDescriptor
import com.google.android.gms.maps.model.BitmapDescriptorFactory
Expand Down Expand Up @@ -41,6 +42,10 @@ internal class ComposeUiClusterRenderer<T : ClusterItem>(
private val viewRendererState: State<ComposeUiViewRenderer>,
private val clusterContentState: State<@Composable ((Cluster<T>) -> Unit)?>,
private val clusterItemContentState: State<@Composable ((T) -> Unit)?>,
private val clusterContentAnchorState: State<Offset>,
private val clusterItemContentAnchorState: State<Offset>,
private val clusterContentZIndexState: State<Float>,
private val clusterItemContentZIndexState: State<Float>,
) : DefaultClusterRenderer<T>(
context,
map,
Expand Down Expand Up @@ -139,11 +144,25 @@ internal class ComposeUiClusterRenderer<T : ClusterItem>(
when (key) {
is ViewKey.Cluster -> getMarker(key.cluster)
is ViewKey.Item -> getMarker(key.item)
}?.setIcon(renderViewToBitmapDescriptor(view))
}?.apply {
setIcon(renderViewToBitmapDescriptor(view))
view.properties.anchor?.let { setAnchor(it.x, it.y) }
view.properties.zIndex?.let { zIndex = it }
}
}

}

override fun onBeforeClusterRendered(cluster: Cluster<T>, markerOptions: MarkerOptions) {
super.onBeforeClusterRendered(cluster, markerOptions)

if (clusterContentState.value != null) {
val anchor = clusterContentAnchorState.value
markerOptions.anchor(anchor.x, anchor.y)
markerOptions.zIndex(clusterContentZIndexState.value)
}
}

override fun getDescriptorForCluster(cluster: Cluster<T>): BitmapDescriptor {
return if (clusterContentState.value != null) {
val viewInfo = keysToViews.entries
Expand All @@ -165,6 +184,10 @@ internal class ComposeUiClusterRenderer<T : ClusterItem>(
?.value
?: createAndAddView(ViewKey.Item(item))
markerOptions.icon(renderViewToBitmapDescriptor(viewInfo.view))

val anchor = clusterItemContentAnchorState.value
markerOptions.anchor(anchor.x, anchor.y)
markerOptions.zIndex(clusterItemContentZIndexState.value)
}
}

Expand Down Expand Up @@ -216,10 +239,20 @@ internal class ComposeUiClusterRenderer<T : ClusterItem>(
private val content: @Composable () -> Unit,
) : AbstractComposeView(context) {

val properties = ClusteringMarkerProperties()
var onInvalidate: (() -> Unit)? = null

@Composable
override fun Content() = content()
override fun Content() {
androidx.compose.runtime.LaunchedEffect(properties.anchor, properties.zIndex) {
invalidate()
}
androidx.compose.runtime.CompositionLocalProvider(
LocalClusteringMarkerProperties provides properties
) {
content()
}
}

override fun onDescendantInvalidated(child: View, target: View) {
super.onDescendantInvalidated(child, target)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.UiComposable
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.LocalContext
import com.google.android.gms.maps.GoogleMap
import com.google.maps.android.clustering.Cluster
Expand All @@ -30,6 +37,43 @@ import com.google.maps.android.compose.rememberReattachClickListenersHandle
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.launch

/**
* Properties for a marker in [Clustering].
*/
public class ClusteringMarkerProperties {
public var anchor: Offset? by mutableStateOf(null)
internal set
public var zIndex: Float? by mutableStateOf(null)
internal set
}

/**
* [CompositionLocal] used to provide [ClusteringMarkerProperties] to the content of a cluster or
* cluster item.
*/
public val LocalClusteringMarkerProperties: androidx.compose.runtime.ProvidableCompositionLocal<ClusteringMarkerProperties> =
staticCompositionLocalOf { ClusteringMarkerProperties() }

/**
* Helper function to specify properties for the marker representing a cluster or cluster item.
*
* @param anchor the anchor for the marker image. If null, the default anchor specified in
* [Clustering] will be used.
* @param zIndex the z-index of the marker. If null, the default z-index specified in [Clustering]
* will be used.
*/
@Composable
public fun ClusteringMarkerProperties(
anchor: Offset? = null,
zIndex: Float? = null,
) {
val properties = LocalClusteringMarkerProperties.current
SideEffect {
properties.anchor = anchor
properties.zIndex = zIndex
}
}

/**
* Groups many items on a map based on zoom level.
*
Expand All @@ -42,6 +86,10 @@ import kotlinx.coroutines.launch
* window of a non-clustered item
* @param clusterContent an optional Composable that is rendered for each [Cluster].
* @param clusterItemContent an optional Composable that is rendered for each non-clustered item.
* @param clusterContentAnchor the anchor for the cluster image
* @param clusterItemContentAnchor the anchor for the non-clustered item image
* @param clusterContentZIndex the z-index of the cluster
* @param clusterItemContentZIndex the z-index of the non-clustered item
* @param clusterRenderer an optional ClusterRenderer that can be used to specify the algorithm used by the rendering.
*/
@Composable
Expand Down Expand Up @@ -85,10 +133,21 @@ public fun <T : ClusterItem> Clustering(
onClusterItemInfoWindowLongClick: (T) -> Unit = { },
clusterContent: @[UiComposable Composable] ((Cluster<T>) -> Unit)? = null,
clusterItemContent: @[UiComposable Composable] ((T) -> Unit)? = null,
clusterContentAnchor: Offset = Offset(0.5f, 1.0f),
clusterItemContentAnchor: Offset = Offset(0.5f, 1.0f),
clusterContentZIndex: Float = 0.0f,
clusterItemContentZIndex: Float = 0.0f,
clusterRenderer: ClusterRenderer<T>? = null,
) {
val clusterManager = rememberClusterManager(clusterContent, clusterItemContent, clusterRenderer)
?: return
val clusterManager = rememberClusterManager(
clusterContent,
clusterItemContent,
clusterContentAnchor,
clusterItemContentAnchor,
clusterContentZIndex,
clusterItemContentZIndex,
clusterRenderer
) ?: return

SideEffect {
clusterManager.setOnClusterClickListener(onClusterClick)
Expand All @@ -114,6 +173,10 @@ public fun <T : ClusterItem> Clustering(
* window of a non-clustered item
* @param clusterContent an optional Composable that is rendered for each [Cluster].
* @param clusterItemContent an optional Composable that is rendered for each non-clustered item.
* @param clusterContentAnchor the anchor for the cluster image
* @param clusterItemContentAnchor the anchor for the non-clustered item image
* @param clusterContentZIndex the z-index of the cluster
* @param clusterItemContentZIndex the z-index of the non-clustered item
*/
@Composable
@GoogleMapComposable
Expand All @@ -126,6 +189,10 @@ public fun <T : ClusterItem> Clustering(
onClusterItemInfoWindowLongClick: (T) -> Unit = { },
clusterContent: @[UiComposable Composable] ((Cluster<T>) -> Unit)? = null,
clusterItemContent: @[UiComposable Composable] ((T) -> Unit)? = null,
clusterContentAnchor: Offset = Offset(0.5f, 1.0f),
clusterItemContentAnchor: Offset = Offset(0.5f, 1.0f),
clusterContentZIndex: Float = 0.0f,
clusterItemContentZIndex: Float = 0.0f,
) {
Clustering(
items = items,
Expand All @@ -135,6 +202,10 @@ public fun <T : ClusterItem> Clustering(
onClusterItemInfoWindowLongClick = onClusterItemInfoWindowLongClick,
clusterContent = clusterContent,
clusterItemContent = clusterItemContent,
clusterContentAnchor = clusterContentAnchor,
clusterItemContentAnchor = clusterItemContentAnchor,
clusterContentZIndex = clusterContentZIndex,
clusterItemContentZIndex = clusterItemContentZIndex,
onClusterManager = null,
)
}
Expand All @@ -151,6 +222,10 @@ public fun <T : ClusterItem> Clustering(
* window of a non-clustered item
* @param clusterContent an optional Composable that is rendered for each [Cluster].
* @param clusterItemContent an optional Composable that is rendered for each non-clustered item.
* @param clusterContentAnchor the anchor for the cluster image
* @param clusterItemContentAnchor the anchor for the non-clustered item image
* @param clusterContentZIndex the z-index of the cluster
* @param clusterItemContentZIndex the z-index of the non-clustered item
* @param onClusterManager an optional lambda invoked with the clusterManager as a param when both
* the clusterManager and renderer are set up, allowing callers a customization hook.
*/
Expand All @@ -165,10 +240,22 @@ public fun <T : ClusterItem> Clustering(
onClusterItemInfoWindowLongClick: (T) -> Unit = { },
clusterContent: @[UiComposable Composable] ((Cluster<T>) -> Unit)? = null,
clusterItemContent: @[UiComposable Composable] ((T) -> Unit)? = null,
clusterContentAnchor: Offset = Offset(0.5f, 1.0f),
clusterItemContentAnchor: Offset = Offset(0.5f, 1.0f),
clusterContentZIndex: Float = 0.0f,
clusterItemContentZIndex: Float = 0.0f,
onClusterManager: ((ClusterManager<T>) -> Unit)? = null,
) {
val clusterManager = rememberClusterManager<T>()
val renderer = rememberClusterRenderer(clusterContent, clusterItemContent, clusterManager)
val renderer = rememberClusterRenderer(
clusterContent,
clusterItemContent,
clusterContentAnchor,
clusterItemContentAnchor,
clusterContentZIndex,
clusterItemContentZIndex,
clusterManager
)

SideEffect {
clusterManager ?: return@SideEffect
Expand Down Expand Up @@ -266,17 +353,29 @@ public fun <T : ClusterItem> rememberClusterRenderer(
*
* @param clusterContent an optional Composable that is rendered for each [Cluster].
* @param clusterItemContent an optional Composable that is rendered for each non-clustered item.
* @param clusterContentAnchor the anchor for the cluster image
* @param clusterItemContentAnchor the anchor for the non-clustered item image
* @param clusterContentZIndex the z-index of the cluster
* @param clusterItemContentZIndex the z-index of the non-clustered item
*/
@Composable
@GoogleMapComposable
@MapsComposeExperimentalApi
public fun <T : ClusterItem> rememberClusterRenderer(
clusterContent: @Composable ((Cluster<T>) -> Unit)?,
clusterItemContent: @Composable ((T) -> Unit)?,
clusterContentAnchor: Offset = Offset(0.5f, 1.0f),
clusterItemContentAnchor: Offset = Offset(0.5f, 1.0f),
clusterContentZIndex: Float = 0.0f,
clusterItemContentZIndex: Float = 0.0f,
clusterManager: ClusterManager<T>?,
): ClusterRenderer<T>? {
val clusterContentState = rememberUpdatedState(clusterContent)
val clusterItemContentState = rememberUpdatedState(clusterItemContent)
val clusterContentAnchorState = rememberUpdatedState(clusterContentAnchor)
val clusterItemContentAnchorState = rememberUpdatedState(clusterItemContentAnchor)
val clusterContentZIndexState = rememberUpdatedState(clusterContentZIndex)
val clusterItemContentZIndexState = rememberUpdatedState(clusterItemContentZIndex)
val context = LocalContext.current
val viewRendererState = rememberUpdatedState(rememberComposeUiViewRenderer())
val clusterRendererState: MutableState<ClusterRenderer<T>?> = remember { mutableStateOf(null) }
Expand All @@ -291,6 +390,10 @@ public fun <T : ClusterItem> rememberClusterRenderer(
viewRendererState,
clusterContentState,
clusterItemContentState,
clusterContentAnchorState,
clusterItemContentAnchorState,
clusterContentZIndexState,
clusterItemContentZIndexState,
)
clusterRendererState.value = renderer
awaitCancellation()
Expand All @@ -315,10 +418,18 @@ public fun <T : ClusterItem> rememberClusterManager(): ClusterManager<T>? {
private fun <T : ClusterItem> rememberClusterManager(
clusterContent: @Composable ((Cluster<T>) -> Unit)?,
clusterItemContent: @Composable ((T) -> Unit)?,
clusterContentAnchor: Offset = Offset(0.5f, 1.0f),
clusterItemContentAnchor: Offset = Offset(0.5f, 1.0f),
clusterContentZIndex: Float = 0.0f,
clusterItemContentZIndex: Float = 0.0f,
clusterRenderer: ClusterRenderer<T>? = null,
): ClusterManager<T>? {
val clusterContentState = rememberUpdatedState(clusterContent)
val clusterItemContentState = rememberUpdatedState(clusterItemContent)
val clusterContentAnchorState = rememberUpdatedState(clusterContentAnchor)
val clusterItemContentAnchorState = rememberUpdatedState(clusterItemContentAnchor)
val clusterContentZIndexState = rememberUpdatedState(clusterContentZIndex)
val clusterItemContentZIndexState = rememberUpdatedState(clusterItemContentZIndex)
val context = LocalContext.current
val viewRendererState = rememberUpdatedState(rememberComposeUiViewRenderer())
val clusterManagerState: MutableState<ClusterManager<T>?> = remember { mutableStateOf(null) }
Expand All @@ -332,14 +443,18 @@ private fun <T : ClusterItem> rememberClusterManager(
.collect { hasCustomContent ->
val renderer = clusterRenderer
?: if (hasCustomContent) {
ComposeUiClusterRenderer(
ComposeUiClusterRenderer<T>(
context,
scope = this,
map,
clusterManager,
viewRendererState,
clusterContentState,
clusterItemContentState,
clusterContentAnchorState,
clusterItemContentAnchorState,
clusterContentZIndexState,
clusterItemContentZIndexState,
)
} else {
DefaultClusterRenderer(context, map, clusterManager)
Expand Down