diff --git a/maps-app/src/main/java/com/google/maps/android/compose/markerexamples/MarkerClusteringActivity.kt b/maps-app/src/main/java/com/google/maps/android/compose/markerexamples/MarkerClusteringActivity.kt index 86ba4a61..c0cd4873 100644 --- a/maps-app/src/main/java/com/google/maps/android/compose/markerexamples/MarkerClusteringActivity.kt +++ b/maps-app/src/main/java/com/google/maps/android/compose/markerexamples/MarkerClusteringActivity.kt @@ -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 @@ -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 @@ -160,6 +162,7 @@ private fun DefaultClustering(items: List) { @OptIn(MapsComposeExperimentalApi::class) @Composable private fun CustomUiClustering(items: List) { + var selectedItem by remember { mutableStateOf(null) } Clustering( items = items, // Optional: Handle clicks on clusters, cluster items, and cluster item info windows @@ -169,6 +172,7 @@ private fun CustomUiClustering(items: List) { }, onClusterItemClick = { Log.d(TAG, "Cluster item clicked! $it") + selectedItem = if (selectedItem == it) null else it false }, onClusterItemInfoWindowClick = { @@ -183,7 +187,21 @@ private fun CustomUiClustering(items: List) { ) }, // 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 diff --git a/maps-compose-utils/src/main/java/com/google/maps/android/compose/clustering/ClusterRenderer.kt b/maps-compose-utils/src/main/java/com/google/maps/android/compose/clustering/ClusterRenderer.kt index b96cf469..f7994963 100644 --- a/maps-compose-utils/src/main/java/com/google/maps/android/compose/clustering/ClusterRenderer.kt +++ b/maps-compose-utils/src/main/java/com/google/maps/android/compose/clustering/ClusterRenderer.kt @@ -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 @@ -41,6 +42,10 @@ internal class ComposeUiClusterRenderer( private val viewRendererState: State, private val clusterContentState: State<@Composable ((Cluster) -> Unit)?>, private val clusterItemContentState: State<@Composable ((T) -> Unit)?>, + private val clusterContentAnchorState: State, + private val clusterItemContentAnchorState: State, + private val clusterContentZIndexState: State, + private val clusterItemContentZIndexState: State, ) : DefaultClusterRenderer( context, map, @@ -139,11 +144,25 @@ internal class ComposeUiClusterRenderer( 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, 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): BitmapDescriptor { return if (clusterContentState.value != null) { val viewInfo = keysToViews.entries @@ -165,6 +184,10 @@ internal class ComposeUiClusterRenderer( ?.value ?: createAndAddView(ViewKey.Item(item)) markerOptions.icon(renderViewToBitmapDescriptor(viewInfo.view)) + + val anchor = clusterItemContentAnchorState.value + markerOptions.anchor(anchor.x, anchor.y) + markerOptions.zIndex(clusterItemContentZIndexState.value) } } @@ -216,10 +239,20 @@ internal class ComposeUiClusterRenderer( 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) diff --git a/maps-compose-utils/src/main/java/com/google/maps/android/compose/clustering/Clustering.kt b/maps-compose-utils/src/main/java/com/google/maps/android/compose/clustering/Clustering.kt index 01968f23..a96ccaaf 100644 --- a/maps-compose-utils/src/main/java/com/google/maps/android/compose/clustering/Clustering.kt +++ b/maps-compose-utils/src/main/java/com/google/maps/android/compose/clustering/Clustering.kt @@ -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 @@ -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 = + 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. * @@ -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 @@ -85,10 +133,21 @@ public fun Clustering( onClusterItemInfoWindowLongClick: (T) -> Unit = { }, clusterContent: @[UiComposable Composable] ((Cluster) -> 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? = null, ) { - val clusterManager = rememberClusterManager(clusterContent, clusterItemContent, clusterRenderer) - ?: return + val clusterManager = rememberClusterManager( + clusterContent, + clusterItemContent, + clusterContentAnchor, + clusterItemContentAnchor, + clusterContentZIndex, + clusterItemContentZIndex, + clusterRenderer + ) ?: return SideEffect { clusterManager.setOnClusterClickListener(onClusterClick) @@ -114,6 +173,10 @@ public fun 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 @@ -126,6 +189,10 @@ public fun Clustering( onClusterItemInfoWindowLongClick: (T) -> Unit = { }, clusterContent: @[UiComposable Composable] ((Cluster) -> 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, @@ -135,6 +202,10 @@ public fun Clustering( onClusterItemInfoWindowLongClick = onClusterItemInfoWindowLongClick, clusterContent = clusterContent, clusterItemContent = clusterItemContent, + clusterContentAnchor = clusterContentAnchor, + clusterItemContentAnchor = clusterItemContentAnchor, + clusterContentZIndex = clusterContentZIndex, + clusterItemContentZIndex = clusterItemContentZIndex, onClusterManager = null, ) } @@ -151,6 +222,10 @@ public fun 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. */ @@ -165,10 +240,22 @@ public fun Clustering( onClusterItemInfoWindowLongClick: (T) -> Unit = { }, clusterContent: @[UiComposable Composable] ((Cluster) -> 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) -> Unit)? = null, ) { val clusterManager = rememberClusterManager() - val renderer = rememberClusterRenderer(clusterContent, clusterItemContent, clusterManager) + val renderer = rememberClusterRenderer( + clusterContent, + clusterItemContent, + clusterContentAnchor, + clusterItemContentAnchor, + clusterContentZIndex, + clusterItemContentZIndex, + clusterManager + ) SideEffect { clusterManager ?: return@SideEffect @@ -266,6 +353,10 @@ public fun 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 @@ -273,10 +364,18 @@ public fun rememberClusterRenderer( public fun rememberClusterRenderer( clusterContent: @Composable ((Cluster) -> 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?, ): ClusterRenderer? { 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?> = remember { mutableStateOf(null) } @@ -291,6 +390,10 @@ public fun rememberClusterRenderer( viewRendererState, clusterContentState, clusterItemContentState, + clusterContentAnchorState, + clusterItemContentAnchorState, + clusterContentZIndexState, + clusterItemContentZIndexState, ) clusterRendererState.value = renderer awaitCancellation() @@ -315,10 +418,18 @@ public fun rememberClusterManager(): ClusterManager? { private fun rememberClusterManager( clusterContent: @Composable ((Cluster) -> 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? = null, ): ClusterManager? { 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?> = remember { mutableStateOf(null) } @@ -332,7 +443,7 @@ private fun rememberClusterManager( .collect { hasCustomContent -> val renderer = clusterRenderer ?: if (hasCustomContent) { - ComposeUiClusterRenderer( + ComposeUiClusterRenderer( context, scope = this, map, @@ -340,6 +451,10 @@ private fun rememberClusterManager( viewRendererState, clusterContentState, clusterItemContentState, + clusterContentAnchorState, + clusterItemContentAnchorState, + clusterContentZIndexState, + clusterItemContentZIndexState, ) } else { DefaultClusterRenderer(context, map, clusterManager)