Skip to content

Commit 516a3be

Browse files
authored
Merge branch 'main' into build/automatically-triage-issues
2 parents a7f1f09 + c2f19e4 commit 516a3be

File tree

3 files changed

+173
-7
lines changed

3 files changed

+173
-7
lines changed

maps-app/src/main/java/com/google/maps/android/compose/markerexamples/MarkerClusteringActivity.kt

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import androidx.compose.runtime.mutableStateOf
3232
import androidx.compose.runtime.remember
3333
import androidx.compose.runtime.setValue
3434
import androidx.compose.ui.Alignment
35+
import androidx.compose.ui.geometry.Offset
3536
import androidx.compose.ui.Modifier
3637
import androidx.compose.ui.graphics.Color
3738
import androidx.compose.ui.platform.LocalConfiguration
@@ -48,6 +49,7 @@ import com.google.maps.android.compose.GoogleMap
4849
import com.google.maps.android.compose.MapsComposeExperimentalApi
4950
import com.google.maps.android.compose.MarkerInfoWindow
5051
import com.google.maps.android.compose.clustering.Clustering
52+
import com.google.maps.android.compose.clustering.ClusteringMarkerProperties
5153
import com.google.maps.android.compose.clustering.rememberClusterManager
5254
import com.google.maps.android.compose.clustering.rememberClusterRenderer
5355
import com.google.maps.android.compose.rememberCameraPositionState
@@ -160,6 +162,7 @@ private fun DefaultClustering(items: List<MyItem>) {
160162
@OptIn(MapsComposeExperimentalApi::class)
161163
@Composable
162164
private fun CustomUiClustering(items: List<MyItem>) {
165+
var selectedItem by remember { mutableStateOf<MyItem?>(null) }
163166
Clustering(
164167
items = items,
165168
// Optional: Handle clicks on clusters, cluster items, and cluster item info windows
@@ -169,6 +172,7 @@ private fun CustomUiClustering(items: List<MyItem>) {
169172
},
170173
onClusterItemClick = {
171174
Log.d(TAG, "Cluster item clicked! $it")
175+
selectedItem = if (selectedItem == it) null else it
172176
false
173177
},
174178
onClusterItemInfoWindowClick = {
@@ -183,7 +187,21 @@ private fun CustomUiClustering(items: List<MyItem>) {
183187
)
184188
},
185189
// Optional: Custom rendering for non-clustered items
186-
clusterItemContent = null,
190+
clusterItemContent = { item ->
191+
val isSelected = item == selectedItem
192+
if (isSelected) {
193+
ClusteringMarkerProperties(
194+
anchor = Offset(0.5f, 0.5f),
195+
zIndex = 1.0f
196+
)
197+
}
198+
CircleContent(
199+
modifier = Modifier.size(if (isSelected) 40.dp else 20.dp),
200+
text = "",
201+
color = if (isSelected) Color.Red else Color.Green,
202+
)
203+
},
204+
clusterContentAnchor = Offset(0.5f, 0.5f),
187205
// Optional: Customization hook for clusterManager and renderer when they're ready
188206
onClusterManager = { clusterManager ->
189207
(clusterManager.renderer as DefaultClusterRenderer).minClusterSize = 2

maps-compose-utils/src/main/java/com/google/maps/android/compose/clustering/ClusterRenderer.kt

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import androidx.compose.ui.platform.AbstractComposeView
1111
import androidx.core.graphics.applyCanvas
1212
import androidx.core.view.doOnAttach
1313
import androidx.core.view.doOnDetach
14+
import androidx.compose.ui.geometry.Offset
1415
import com.google.android.gms.maps.GoogleMap
1516
import com.google.android.gms.maps.model.BitmapDescriptor
1617
import com.google.android.gms.maps.model.BitmapDescriptorFactory
@@ -41,6 +42,10 @@ internal class ComposeUiClusterRenderer<T : ClusterItem>(
4142
private val viewRendererState: State<ComposeUiViewRenderer>,
4243
private val clusterContentState: State<@Composable ((Cluster<T>) -> Unit)?>,
4344
private val clusterItemContentState: State<@Composable ((T) -> Unit)?>,
45+
private val clusterContentAnchorState: State<Offset>,
46+
private val clusterItemContentAnchorState: State<Offset>,
47+
private val clusterContentZIndexState: State<Float>,
48+
private val clusterItemContentZIndexState: State<Float>,
4449
) : DefaultClusterRenderer<T>(
4550
context,
4651
map,
@@ -139,11 +144,25 @@ internal class ComposeUiClusterRenderer<T : ClusterItem>(
139144
when (key) {
140145
is ViewKey.Cluster -> getMarker(key.cluster)
141146
is ViewKey.Item -> getMarker(key.item)
142-
}?.setIcon(renderViewToBitmapDescriptor(view))
147+
}?.apply {
148+
setIcon(renderViewToBitmapDescriptor(view))
149+
view.properties.anchor?.let { setAnchor(it.x, it.y) }
150+
view.properties.zIndex?.let { zIndex = it }
151+
}
143152
}
144153

145154
}
146155

156+
override fun onBeforeClusterRendered(cluster: Cluster<T>, markerOptions: MarkerOptions) {
157+
super.onBeforeClusterRendered(cluster, markerOptions)
158+
159+
if (clusterContentState.value != null) {
160+
val anchor = clusterContentAnchorState.value
161+
markerOptions.anchor(anchor.x, anchor.y)
162+
markerOptions.zIndex(clusterContentZIndexState.value)
163+
}
164+
}
165+
147166
override fun getDescriptorForCluster(cluster: Cluster<T>): BitmapDescriptor {
148167
return if (clusterContentState.value != null) {
149168
val viewInfo = keysToViews.entries
@@ -165,6 +184,10 @@ internal class ComposeUiClusterRenderer<T : ClusterItem>(
165184
?.value
166185
?: createAndAddView(ViewKey.Item(item))
167186
markerOptions.icon(renderViewToBitmapDescriptor(viewInfo.view))
187+
188+
val anchor = clusterItemContentAnchorState.value
189+
markerOptions.anchor(anchor.x, anchor.y)
190+
markerOptions.zIndex(clusterItemContentZIndexState.value)
168191
}
169192
}
170193

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

242+
val properties = ClusteringMarkerProperties()
219243
var onInvalidate: (() -> Unit)? = null
220244

221245
@Composable
222-
override fun Content() = content()
246+
override fun Content() {
247+
androidx.compose.runtime.LaunchedEffect(properties.anchor, properties.zIndex) {
248+
invalidate()
249+
}
250+
androidx.compose.runtime.CompositionLocalProvider(
251+
LocalClusteringMarkerProperties provides properties
252+
) {
253+
content()
254+
}
255+
}
223256

224257
override fun onDescendantInvalidated(child: View, target: View) {
225258
super.onDescendantInvalidated(child, target)

maps-compose-utils/src/main/java/com/google/maps/android/compose/clustering/Clustering.kt

Lines changed: 119 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@ import androidx.compose.runtime.mutableStateOf
1111
import androidx.compose.runtime.remember
1212
import androidx.compose.runtime.rememberUpdatedState
1313
import androidx.compose.runtime.snapshotFlow
14+
import androidx.compose.runtime.CompositionLocalProvider
15+
import androidx.compose.runtime.getValue
16+
import androidx.compose.runtime.mutableStateOf
17+
import androidx.compose.runtime.remember
18+
import androidx.compose.runtime.setValue
19+
import androidx.compose.runtime.staticCompositionLocalOf
1420
import androidx.compose.ui.UiComposable
21+
import androidx.compose.ui.geometry.Offset
1522
import androidx.compose.ui.platform.LocalContext
1623
import com.google.android.gms.maps.GoogleMap
1724
import com.google.maps.android.clustering.Cluster
@@ -30,6 +37,43 @@ import com.google.maps.android.compose.rememberReattachClickListenersHandle
3037
import kotlinx.coroutines.awaitCancellation
3138
import kotlinx.coroutines.launch
3239

40+
/**
41+
* Properties for a marker in [Clustering].
42+
*/
43+
public class ClusteringMarkerProperties {
44+
public var anchor: Offset? by mutableStateOf(null)
45+
internal set
46+
public var zIndex: Float? by mutableStateOf(null)
47+
internal set
48+
}
49+
50+
/**
51+
* [CompositionLocal] used to provide [ClusteringMarkerProperties] to the content of a cluster or
52+
* cluster item.
53+
*/
54+
public val LocalClusteringMarkerProperties: androidx.compose.runtime.ProvidableCompositionLocal<ClusteringMarkerProperties> =
55+
staticCompositionLocalOf { ClusteringMarkerProperties() }
56+
57+
/**
58+
* Helper function to specify properties for the marker representing a cluster or cluster item.
59+
*
60+
* @param anchor the anchor for the marker image. If null, the default anchor specified in
61+
* [Clustering] will be used.
62+
* @param zIndex the z-index of the marker. If null, the default z-index specified in [Clustering]
63+
* will be used.
64+
*/
65+
@Composable
66+
public fun ClusteringMarkerProperties(
67+
anchor: Offset? = null,
68+
zIndex: Float? = null,
69+
) {
70+
val properties = LocalClusteringMarkerProperties.current
71+
SideEffect {
72+
properties.anchor = anchor
73+
properties.zIndex = zIndex
74+
}
75+
}
76+
3377
/**
3478
* Groups many items on a map based on zoom level.
3579
*
@@ -42,6 +86,10 @@ import kotlinx.coroutines.launch
4286
* window of a non-clustered item
4387
* @param clusterContent an optional Composable that is rendered for each [Cluster].
4488
* @param clusterItemContent an optional Composable that is rendered for each non-clustered item.
89+
* @param clusterContentAnchor the anchor for the cluster image
90+
* @param clusterItemContentAnchor the anchor for the non-clustered item image
91+
* @param clusterContentZIndex the z-index of the cluster
92+
* @param clusterItemContentZIndex the z-index of the non-clustered item
4593
* @param clusterRenderer an optional ClusterRenderer that can be used to specify the algorithm used by the rendering.
4694
*/
4795
@Composable
@@ -85,10 +133,21 @@ public fun <T : ClusterItem> Clustering(
85133
onClusterItemInfoWindowLongClick: (T) -> Unit = { },
86134
clusterContent: @[UiComposable Composable] ((Cluster<T>) -> Unit)? = null,
87135
clusterItemContent: @[UiComposable Composable] ((T) -> Unit)? = null,
136+
clusterContentAnchor: Offset = Offset(0.5f, 1.0f),
137+
clusterItemContentAnchor: Offset = Offset(0.5f, 1.0f),
138+
clusterContentZIndex: Float = 0.0f,
139+
clusterItemContentZIndex: Float = 0.0f,
88140
clusterRenderer: ClusterRenderer<T>? = null,
89141
) {
90-
val clusterManager = rememberClusterManager(clusterContent, clusterItemContent, clusterRenderer)
91-
?: return
142+
val clusterManager = rememberClusterManager(
143+
clusterContent,
144+
clusterItemContent,
145+
clusterContentAnchor,
146+
clusterItemContentAnchor,
147+
clusterContentZIndex,
148+
clusterItemContentZIndex,
149+
clusterRenderer
150+
) ?: return
92151

93152
SideEffect {
94153
clusterManager.setOnClusterClickListener(onClusterClick)
@@ -114,6 +173,10 @@ public fun <T : ClusterItem> Clustering(
114173
* window of a non-clustered item
115174
* @param clusterContent an optional Composable that is rendered for each [Cluster].
116175
* @param clusterItemContent an optional Composable that is rendered for each non-clustered item.
176+
* @param clusterContentAnchor the anchor for the cluster image
177+
* @param clusterItemContentAnchor the anchor for the non-clustered item image
178+
* @param clusterContentZIndex the z-index of the cluster
179+
* @param clusterItemContentZIndex the z-index of the non-clustered item
117180
*/
118181
@Composable
119182
@GoogleMapComposable
@@ -126,6 +189,10 @@ public fun <T : ClusterItem> Clustering(
126189
onClusterItemInfoWindowLongClick: (T) -> Unit = { },
127190
clusterContent: @[UiComposable Composable] ((Cluster<T>) -> Unit)? = null,
128191
clusterItemContent: @[UiComposable Composable] ((T) -> Unit)? = null,
192+
clusterContentAnchor: Offset = Offset(0.5f, 1.0f),
193+
clusterItemContentAnchor: Offset = Offset(0.5f, 1.0f),
194+
clusterContentZIndex: Float = 0.0f,
195+
clusterItemContentZIndex: Float = 0.0f,
129196
) {
130197
Clustering(
131198
items = items,
@@ -135,6 +202,10 @@ public fun <T : ClusterItem> Clustering(
135202
onClusterItemInfoWindowLongClick = onClusterItemInfoWindowLongClick,
136203
clusterContent = clusterContent,
137204
clusterItemContent = clusterItemContent,
205+
clusterContentAnchor = clusterContentAnchor,
206+
clusterItemContentAnchor = clusterItemContentAnchor,
207+
clusterContentZIndex = clusterContentZIndex,
208+
clusterItemContentZIndex = clusterItemContentZIndex,
138209
onClusterManager = null,
139210
)
140211
}
@@ -151,6 +222,10 @@ public fun <T : ClusterItem> Clustering(
151222
* window of a non-clustered item
152223
* @param clusterContent an optional Composable that is rendered for each [Cluster].
153224
* @param clusterItemContent an optional Composable that is rendered for each non-clustered item.
225+
* @param clusterContentAnchor the anchor for the cluster image
226+
* @param clusterItemContentAnchor the anchor for the non-clustered item image
227+
* @param clusterContentZIndex the z-index of the cluster
228+
* @param clusterItemContentZIndex the z-index of the non-clustered item
154229
* @param onClusterManager an optional lambda invoked with the clusterManager as a param when both
155230
* the clusterManager and renderer are set up, allowing callers a customization hook.
156231
*/
@@ -165,10 +240,22 @@ public fun <T : ClusterItem> Clustering(
165240
onClusterItemInfoWindowLongClick: (T) -> Unit = { },
166241
clusterContent: @[UiComposable Composable] ((Cluster<T>) -> Unit)? = null,
167242
clusterItemContent: @[UiComposable Composable] ((T) -> Unit)? = null,
243+
clusterContentAnchor: Offset = Offset(0.5f, 1.0f),
244+
clusterItemContentAnchor: Offset = Offset(0.5f, 1.0f),
245+
clusterContentZIndex: Float = 0.0f,
246+
clusterItemContentZIndex: Float = 0.0f,
168247
onClusterManager: ((ClusterManager<T>) -> Unit)? = null,
169248
) {
170249
val clusterManager = rememberClusterManager<T>()
171-
val renderer = rememberClusterRenderer(clusterContent, clusterItemContent, clusterManager)
250+
val renderer = rememberClusterRenderer(
251+
clusterContent,
252+
clusterItemContent,
253+
clusterContentAnchor,
254+
clusterItemContentAnchor,
255+
clusterContentZIndex,
256+
clusterItemContentZIndex,
257+
clusterManager
258+
)
172259

173260
SideEffect {
174261
clusterManager ?: return@SideEffect
@@ -266,17 +353,29 @@ public fun <T : ClusterItem> rememberClusterRenderer(
266353
*
267354
* @param clusterContent an optional Composable that is rendered for each [Cluster].
268355
* @param clusterItemContent an optional Composable that is rendered for each non-clustered item.
356+
* @param clusterContentAnchor the anchor for the cluster image
357+
* @param clusterItemContentAnchor the anchor for the non-clustered item image
358+
* @param clusterContentZIndex the z-index of the cluster
359+
* @param clusterItemContentZIndex the z-index of the non-clustered item
269360
*/
270361
@Composable
271362
@GoogleMapComposable
272363
@MapsComposeExperimentalApi
273364
public fun <T : ClusterItem> rememberClusterRenderer(
274365
clusterContent: @Composable ((Cluster<T>) -> Unit)?,
275366
clusterItemContent: @Composable ((T) -> Unit)?,
367+
clusterContentAnchor: Offset = Offset(0.5f, 1.0f),
368+
clusterItemContentAnchor: Offset = Offset(0.5f, 1.0f),
369+
clusterContentZIndex: Float = 0.0f,
370+
clusterItemContentZIndex: Float = 0.0f,
276371
clusterManager: ClusterManager<T>?,
277372
): ClusterRenderer<T>? {
278373
val clusterContentState = rememberUpdatedState(clusterContent)
279374
val clusterItemContentState = rememberUpdatedState(clusterItemContent)
375+
val clusterContentAnchorState = rememberUpdatedState(clusterContentAnchor)
376+
val clusterItemContentAnchorState = rememberUpdatedState(clusterItemContentAnchor)
377+
val clusterContentZIndexState = rememberUpdatedState(clusterContentZIndex)
378+
val clusterItemContentZIndexState = rememberUpdatedState(clusterItemContentZIndex)
280379
val context = LocalContext.current
281380
val viewRendererState = rememberUpdatedState(rememberComposeUiViewRenderer())
282381
val clusterRendererState: MutableState<ClusterRenderer<T>?> = remember { mutableStateOf(null) }
@@ -291,6 +390,10 @@ public fun <T : ClusterItem> rememberClusterRenderer(
291390
viewRendererState,
292391
clusterContentState,
293392
clusterItemContentState,
393+
clusterContentAnchorState,
394+
clusterItemContentAnchorState,
395+
clusterContentZIndexState,
396+
clusterItemContentZIndexState,
294397
)
295398
clusterRendererState.value = renderer
296399
awaitCancellation()
@@ -315,10 +418,18 @@ public fun <T : ClusterItem> rememberClusterManager(): ClusterManager<T>? {
315418
private fun <T : ClusterItem> rememberClusterManager(
316419
clusterContent: @Composable ((Cluster<T>) -> Unit)?,
317420
clusterItemContent: @Composable ((T) -> Unit)?,
421+
clusterContentAnchor: Offset = Offset(0.5f, 1.0f),
422+
clusterItemContentAnchor: Offset = Offset(0.5f, 1.0f),
423+
clusterContentZIndex: Float = 0.0f,
424+
clusterItemContentZIndex: Float = 0.0f,
318425
clusterRenderer: ClusterRenderer<T>? = null,
319426
): ClusterManager<T>? {
320427
val clusterContentState = rememberUpdatedState(clusterContent)
321428
val clusterItemContentState = rememberUpdatedState(clusterItemContent)
429+
val clusterContentAnchorState = rememberUpdatedState(clusterContentAnchor)
430+
val clusterItemContentAnchorState = rememberUpdatedState(clusterItemContentAnchor)
431+
val clusterContentZIndexState = rememberUpdatedState(clusterContentZIndex)
432+
val clusterItemContentZIndexState = rememberUpdatedState(clusterItemContentZIndex)
322433
val context = LocalContext.current
323434
val viewRendererState = rememberUpdatedState(rememberComposeUiViewRenderer())
324435
val clusterManagerState: MutableState<ClusterManager<T>?> = remember { mutableStateOf(null) }
@@ -332,14 +443,18 @@ private fun <T : ClusterItem> rememberClusterManager(
332443
.collect { hasCustomContent ->
333444
val renderer = clusterRenderer
334445
?: if (hasCustomContent) {
335-
ComposeUiClusterRenderer(
446+
ComposeUiClusterRenderer<T>(
336447
context,
337448
scope = this,
338449
map,
339450
clusterManager,
340451
viewRendererState,
341452
clusterContentState,
342453
clusterItemContentState,
454+
clusterContentAnchorState,
455+
clusterItemContentAnchorState,
456+
clusterContentZIndexState,
457+
clusterItemContentZIndexState,
343458
)
344459
} else {
345460
DefaultClusterRenderer(context, map, clusterManager)

0 commit comments

Comments
 (0)