Skip to content

Commit aa5793a

Browse files
authored
feat: added Clustering decoration (#848)
1 parent 62f4e43 commit aa5793a

File tree

3 files changed

+94
-6
lines changed

3 files changed

+94
-6
lines changed

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

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import com.google.maps.android.compose.clustering.rememberClusterManager
5454
import com.google.maps.android.compose.clustering.rememberClusterRenderer
5555
import com.google.maps.android.compose.rememberCameraPositionState
5656
import com.google.maps.android.compose.rememberUpdatedMarkerState
57+
import com.google.maps.android.compose.Circle
5758
import com.google.maps.android.compose.singapore
5859
import com.google.maps.android.compose.singapore2
5960
import kotlin.random.Random
@@ -98,7 +99,7 @@ fun GoogleMapClustering(items: List<MyItem>) {
9899
GoogleMap(
99100
modifier = Modifier.fillMaxSize(),
100101
cameraPositionState = rememberCameraPositionState {
101-
position = CameraPosition.fromLatLngZoom(singapore, 6f)
102+
position = CameraPosition.fromLatLngZoom(singapore2, 6f)
102103
}
103104
) {
104105
when (clusteringType) {
@@ -119,10 +120,16 @@ fun GoogleMapClustering(items: List<MyItem>) {
119120
items = items,
120121
)
121122
}
123+
124+
ClusteringType.Decorations -> {
125+
DecorationsClustering(
126+
items = items,
127+
)
128+
}
122129
}
123130

124131
MarkerInfoWindow(
125-
state = rememberUpdatedMarkerState(position = singapore),
132+
state = rememberUpdatedMarkerState(position = singapore2),
126133
onClick = {
127134
Log.d(TAG, "Non-cluster marker clicked! $it")
128135
true
@@ -272,6 +279,23 @@ fun CustomRendererClustering(items: List<MyItem>) {
272279

273280
}
274281

282+
@OptIn(MapsComposeExperimentalApi::class)
283+
@Composable
284+
private fun DecorationsClustering(items: List<MyItem>) {
285+
Clustering(
286+
items = items,
287+
clusterItemDecoration = { item ->
288+
Circle(
289+
center = item.position,
290+
radius = 10000.0,
291+
fillColor = Color.Blue.copy(alpha = 0.2f),
292+
strokeColor = Color.Blue,
293+
strokeWidth = 2f
294+
)
295+
}
296+
)
297+
}
298+
275299
@Composable
276300
private fun CircleContent(
277301
color: Color,
@@ -313,6 +337,7 @@ private fun ClusteringTypeControls(
313337
ClusteringType.Default -> "Default"
314338
ClusteringType.CustomUi -> "Custom UI"
315339
ClusteringType.CustomRenderer -> "Custom Renderer"
340+
ClusteringType.Decorations -> "Decorations"
316341
},
317342
onClick = { onClusteringTypeClick(it) }
318343
)
@@ -338,6 +363,7 @@ private enum class ClusteringType {
338363
Default,
339364
CustomUi,
340365
CustomRenderer,
366+
Decorations,
341367
}
342368

343369
data class MyItem(

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package com.google.maps.android.compose.clustering
22

3+
import androidx.compose.runtime.State
4+
import androidx.compose.runtime.mutableStateOf
5+
36
import android.content.Context
47
import android.graphics.Bitmap
58
import android.graphics.Canvas
69
import android.view.View
710
import android.view.ViewGroup
811
import androidx.compose.runtime.Composable
9-
import androidx.compose.runtime.State
1012
import androidx.compose.ui.platform.AbstractComposeView
1113
import androidx.core.graphics.applyCanvas
1214
import androidx.core.view.doOnAttach
@@ -29,6 +31,10 @@ import kotlinx.coroutines.flow.callbackFlow
2931
import kotlinx.coroutines.flow.collectLatest
3032
import kotlinx.coroutines.launch
3133

34+
internal interface ClusterRendererItemState<T : ClusterItem> {
35+
val unclusteredItems: State<Set<T>>
36+
}
37+
3238
/**
3339
* Implementation of [ClusterRenderer] that renders marker bitmaps from Compose UI content.
3440
* [clusterContentState] renders clusters, and [clusterItemContentState] renders non-clustered
@@ -50,13 +56,19 @@ internal class ComposeUiClusterRenderer<T : ClusterItem>(
5056
context,
5157
map,
5258
clusterManager
53-
) {
59+
), ClusterRendererItemState<T> {
60+
61+
override val unclusteredItems = mutableStateOf(emptySet<T>())
5462

5563
private val fakeCanvas = Canvas()
5664
private val keysToViews = mutableMapOf<ViewKey<T>, ViewInfo>()
5765

5866
override fun onClustersChanged(clusters: Set<Cluster<T>>) {
5967
super.onClustersChanged(clusters)
68+
unclusteredItems.value = clusters.filter { !shouldRenderAsCluster(it) }
69+
.flatMap { it.items }
70+
.toSet()
71+
6072
val keys = clusters.flatMap { it.computeViewKeys() }
6173

6274
with(keysToViews.iterator()) {

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

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.google.maps.android.compose.clustering
22

3+
import android.content.Context
34
import android.os.Handler
45
import android.os.Looper
56
import androidx.compose.runtime.Composable
@@ -138,6 +139,7 @@ public fun <T : ClusterItem> Clustering(
138139
clusterContentZIndex: Float = 0.0f,
139140
clusterItemContentZIndex: Float = 0.0f,
140141
clusterRenderer: ClusterRenderer<T>? = null,
142+
clusterItemDecoration: @Composable @GoogleMapComposable (T) -> Unit = {},
141143
) {
142144
val clusterManager = rememberClusterManager(
143145
clusterContent,
@@ -158,6 +160,8 @@ public fun <T : ClusterItem> Clustering(
158160
Clustering(
159161
items = items,
160162
clusterManager = clusterManager,
163+
clusterItemDecoration = clusterItemDecoration,
164+
renderer = clusterManager.renderer,
161165
)
162166
}
163167

@@ -193,6 +197,7 @@ public fun <T : ClusterItem> Clustering(
193197
clusterItemContentAnchor: Offset = Offset(0.5f, 1.0f),
194198
clusterContentZIndex: Float = 0.0f,
195199
clusterItemContentZIndex: Float = 0.0f,
200+
clusterItemDecoration: @Composable @GoogleMapComposable (T) -> Unit = {},
196201
) {
197202
Clustering(
198203
items = items,
@@ -206,6 +211,7 @@ public fun <T : ClusterItem> Clustering(
206211
clusterItemContentAnchor = clusterItemContentAnchor,
207212
clusterContentZIndex = clusterContentZIndex,
208213
clusterItemContentZIndex = clusterItemContentZIndex,
214+
clusterItemDecoration = clusterItemDecoration,
209215
onClusterManager = null,
210216
)
211217
}
@@ -244,6 +250,7 @@ public fun <T : ClusterItem> Clustering(
244250
clusterItemContentAnchor: Offset = Offset(0.5f, 1.0f),
245251
clusterContentZIndex: Float = 0.0f,
246252
clusterItemContentZIndex: Float = 0.0f,
253+
clusterItemDecoration: @Composable @GoogleMapComposable (T) -> Unit = {},
247254
onClusterManager: ((ClusterManager<T>) -> Unit)? = null,
248255
) {
249256
val clusterManager = rememberClusterManager<T>()
@@ -277,6 +284,8 @@ public fun <T : ClusterItem> Clustering(
277284
Clustering(
278285
items = items,
279286
clusterManager = clusterManager,
287+
clusterItemDecoration = clusterItemDecoration,
288+
renderer = renderer,
280289
)
281290
}
282291
}
@@ -293,6 +302,24 @@ public fun <T : ClusterItem> Clustering(
293302
public fun <T : ClusterItem> Clustering(
294303
items: Collection<T>,
295304
clusterManager: ClusterManager<T>,
305+
clusterItemDecoration: @Composable @GoogleMapComposable (T) -> Unit = {},
306+
) {
307+
Clustering(
308+
items = items,
309+
clusterManager = clusterManager,
310+
clusterItemDecoration = clusterItemDecoration,
311+
renderer = null
312+
)
313+
}
314+
315+
@Composable
316+
@GoogleMapComposable
317+
@MapsComposeExperimentalApi
318+
internal fun <T : ClusterItem> Clustering(
319+
items: Collection<T>,
320+
clusterManager: ClusterManager<T>,
321+
clusterItemDecoration: @Composable @GoogleMapComposable (T) -> Unit = {},
322+
renderer: ClusterRenderer<T>? = null,
296323
) {
297324
ResetMapListeners(clusterManager)
298325
InputHandler(
@@ -327,6 +354,13 @@ public fun <T : ClusterItem> Clustering(
327354
clusterManager.cluster()
328355
}
329356
}
357+
358+
val actualRenderer = renderer ?: clusterManager.renderer
359+
val unclusteredItems by (actualRenderer as? ClusterRendererItemState<T>)?.unclusteredItems
360+
?: remember { mutableStateOf(emptySet()) }
361+
unclusteredItems.forEach { item ->
362+
clusterItemDecoration(item)
363+
}
330364
}
331365

332366

@@ -341,7 +375,7 @@ public fun <T : ClusterItem> rememberClusterRenderer(
341375

342376
clusterManager ?: return null
343377
MapEffect(context) { map ->
344-
val renderer = DefaultClusterRenderer(context, map, clusterManager)
378+
val renderer = ReportingDefaultClusterRenderer(context, map, clusterManager)
345379
clusterRendererState.value = renderer
346380
}
347381

@@ -457,7 +491,7 @@ private fun <T : ClusterItem> rememberClusterManager(
457491
clusterItemContentZIndexState,
458492
)
459493
} else {
460-
DefaultClusterRenderer(context, map, clusterManager)
494+
ReportingDefaultClusterRenderer(context, map, clusterManager)
461495
}
462496
clusterManager.renderer = renderer
463497
}
@@ -488,3 +522,19 @@ private fun ResetMapListeners(
488522
}
489523
}
490524
}
525+
526+
private class ReportingDefaultClusterRenderer<T : ClusterItem>(
527+
context: Context,
528+
map: GoogleMap,
529+
clusterManager: ClusterManager<T>
530+
) : DefaultClusterRenderer<T>(context, map, clusterManager), ClusterRendererItemState<T> {
531+
532+
override val unclusteredItems = mutableStateOf(emptySet<T>())
533+
534+
override fun onClustersChanged(clusters: Set<Cluster<T>>) {
535+
super.onClustersChanged(clusters)
536+
unclusteredItems.value = clusters.filter { !shouldRenderAsCluster(it) }
537+
.flatMap { it.items }
538+
.toSet()
539+
}
540+
}

0 commit comments

Comments
 (0)