Skip to content

Commit 7502a23

Browse files
authored
Merge branch 'main' into fix/clustering-idle-timeout
2 parents bce2c9d + 4481829 commit 7502a23

9 files changed

Lines changed: 147 additions & 11 deletions

File tree

.gemini/config.yaml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Gemini Code Assist Configuration
2+
# See: https://developers.google.com/gemini-code-assist/docs/customize-gemini-behavior-github
3+
4+
# Feature settings
5+
have_fun: false
6+
7+
code_review:
8+
disable: false
9+
comment_severity_threshold: MEDIUM
10+
max_review_comments: -1
11+
12+
pull_request_opened:
13+
summary: true
14+
code_review: true
15+
include_drafts: true
16+
17+
# Files to ignore in Gemini analysis
18+
ignore_patterns:
19+
- "**/*.bin"
20+
- "**/*.exe"
21+
- "**/build/**"
22+
- "**/.gradle/**"
23+
- "**/secrets.properties"

.gemini/styleguide.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Gemini Code Assist Style Guide: android-maps-compose
2+
3+
This guide defines the custom code review and generation rules for the `android-maps-compose` project.
4+
5+
## Jetpack Compose Guidelines
6+
- **API Guidelines**: Strictly follow the [Jetpack Compose API guidelines](https://github.com/androidx/androidx/blob/androidx-main/compose/docs/compose-api-guidelines.md).
7+
- **Naming**: Composable functions must be PascalCase.
8+
- **State Management**: Prefer library-provided state holders like `rememberCameraPositionState` or `MarkerState`.
9+
- **Modifiers**: The first optional parameter of any Composable should be `modifier: Modifier = Modifier`.
10+
11+
## Kotlin Style
12+
- **Naming**: Use camelCase for variables and functions.
13+
- **Documentation**: Provide KDoc for all public classes, properties, and functions.
14+
- **Safety**: Use null-safe operators and avoid `!!`.
15+
16+
## Project Specifics
17+
- **Secrets**: Never commit API keys. Ensure they are read from `secrets.properties` via `BuildConfig` or similar.
18+
- **Maps SDK**: Use the components provided in `maps-compose`, `maps-compose-utils`, and `maps-compose-widgets` rather than raw `GoogleMap` objects.

.release-please-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "8.1.0"
2+
".": "8.2.0"
33
}

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## [8.2.0](https://github.com/googlemaps/android-maps-compose/compare/v8.1.0...v8.2.0) (2026-02-24)
4+
5+
6+
### Features
7+
8+
* added Clustering decoration ([#848](https://github.com/googlemaps/android-maps-compose/issues/848)) ([aa5793a](https://github.com/googlemaps/android-maps-compose/commit/aa5793a920f92c1efb0b90287092a33661acac4c))
9+
310
## [8.1.0](https://github.com/googlemaps/android-maps-compose/compare/v8.0.1...v8.1.0) (2026-02-06)
411

512

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@ You no longer need to specify the Maps SDK for Android or its Utility Library as
2929

3030
```groovy
3131
dependencies {
32-
implementation 'com.google.maps.android:maps-compose:8.1.0' // {x-release-please-version}
32+
implementation 'com.google.maps.android:maps-compose:8.2.0' // {x-release-please-version}
3333
// Optionally, you can include the Compose utils library for Clustering,
3434
// Street View metadata checks, etc.
35-
implementation 'com.google.maps.android:maps-compose-utils:8.1.0' // {x-release-please-version}
35+
implementation 'com.google.maps.android:maps-compose-utils:8.2.0' // {x-release-please-version}
3636
// Optionally, you can include the widgets library for ScaleBar, etc.
37-
implementation 'com.google.maps.android:maps-compose-widgets:8.1.0' // {x-release-please-version}
37+
implementation 'com.google.maps.android:maps-compose-widgets:8.2.0' // {x-release-please-version}
3838
}
3939
```
4040

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ val projectArtifactId by extra { project: Project ->
3333
allprojects {
3434
group = "com.google.maps.android"
3535
// {x-release-please-start-version}
36-
version = "8.1.0"
36+
version = "8.2.0"
3737
// {x-release-please-end}
3838
}
3939

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)