Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8dee661
feat: implementing Marker clustering for Android
Samo8 Jan 20, 2026
3f5281d
feat: implementing Marker clustering for iOS
Samo8 Jan 20, 2026
a732d59
feat: updating Android dependencies and raising compileSdk to 36
Samo8 Jan 20, 2026
abe07a9
feat: implementing Marker clustering Flutter layer
Samo8 Jan 20, 2026
5fffe11
feat: adding ClusteringPage example for Marker clustering
Samo8 Jan 20, 2026
f7a266d
feat: using MarkerManager for handling Markers and Marker clustering
Samo8 Jan 20, 2026
412bc64
feat: removing unused methods
Samo8 Jan 21, 2026
f8b6a89
feat: replacing hardcoded false with consumeTapEvents
Samo8 Jan 21, 2026
387919c
feat: removing consumeTapEvents from example
Samo8 Jan 21, 2026
68cc9d5
feat: handling clustered markers in updating and removing markers for…
Samo8 Jan 21, 2026
e572534
feat: removing unused code from clustering example
Samo8 Jan 21, 2026
b6a09e6
feat: updating setOnClusterClickListener
Samo8 Jan 21, 2026
11979db
feat: combining markers
Samo8 Jan 21, 2026
34fde1e
feat: storing all markers in one place (Map)
Samo8 Jan 21, 2026
819f010
test: adding integration tests for markers clustering
Samo8 Jan 21, 2026
01017b0
feat: pin for google-maps-ios-utils
Samo8 Jan 21, 2026
429c403
refactor: simplifying android implementation
Samo8 Jan 21, 2026
09dd687
refactor: simplifying ios implementation
Samo8 Jan 21, 2026
88ba1c1
refactor: simplifying android implementation
Samo8 Jan 21, 2026
b75790f
feat: passing ClusterManager List to addClusterManagers, removing dum…
Samo8 Jan 21, 2026
e0f6088
test: fixing tests to call addClusterManagers with List<ClusterManager>
Samo8 Jan 21, 2026
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
11 changes: 5 additions & 6 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,6 @@ allprojects {
google()
mavenCentral()
}
configurations {
implementation {
exclude group: 'com.google.android.gms', module: 'play-services-maps'
}
}
}

apply plugin: 'com.android.library'
Expand All @@ -53,7 +48,7 @@ ktfmt {
android {
namespace 'com.google.maps.flutter.navigation'

compileSdk = 35
compileSdk = 36

compileOptions {
sourceCompatibility JavaVersion.VERSION_11
Expand All @@ -78,6 +73,10 @@ android {
implementation 'androidx.car.app:app:1.7.0'
implementation 'androidx.car.app:app-projected:1.7.0'
implementation 'com.google.android.libraries.navigation:navigation:7.3.0'
implementation('com.google.maps.android:android-maps-utils:3.20.1') {
exclude group: 'com.google.android.gms', module: 'play-services-maps'
}
implementation 'com.google.android.gms:play-services-base:18.5.0'
testImplementation 'org.jetbrains.kotlin:kotlin-test'
testImplementation 'io.mockk:mockk:1.13.8'
testImplementation 'junit:junit:4.13.2'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ open class AndroidAutoBaseScreen(carContext: CarContext) :
mViewRegistry = viewRegistry
mAutoMapView =
GoogleMapsAutoMapView(
carContext,
MapOptions(GoogleMapOptions(), null),
viewRegistry,
imageRegistry,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.maps.flutter.navigation

import android.content.Context
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.model.Marker
import com.google.maps.android.clustering.Cluster
import com.google.maps.android.clustering.ClusterManager
import com.google.maps.android.clustering.view.DefaultClusterRenderer
import com.google.maps.android.collections.MarkerManager

/** Controller for managing a single ClusterManager instance. */
class ClusterManagerController(
val clusterManagerId: String,
private val context: Context,
private val googleMap: GoogleMap,
private val markerManager: MarkerManager,
private val viewEventApi: ViewEventApi,
private val viewId: Int,
) {
private val clusterManager: ClusterManager<MarkerClusterItem>
private val clusterRenderer: ClusterRenderer
private val markerToClusterItem = mutableMapOf<Marker, MarkerClusterItem>()

init {
clusterManager = ClusterManager(context, googleMap, markerManager)
clusterRenderer = ClusterRenderer(context, googleMap, clusterManager)
clusterManager.renderer = clusterRenderer

// Set up click listeners
clusterManager.setOnClusterClickListener(::onClusterClick)
clusterManager.setOnClusterItemClickListener(::onClusterItemClick)
}

/** Adds a marker item to the cluster. */
fun addItem(item: MarkerClusterItem) {
clusterManager.addItem(item)
}

/** Removes a marker item from the cluster. */
fun removeItem(item: MarkerClusterItem) {
clusterManager.removeItem(item)
}

/** Clears all items from the cluster. */
fun clearItems() {
clusterManager.clearItems()
markerToClusterItem.clear()
}

/** Triggers clustering calculation. */
fun cluster() {
clusterManager.cluster()
}

/** Called when camera stops moving to refresh clusters. */
fun onCameraIdle() {
clusterManager.onCameraIdle()
}

/** Returns the underlying ClusterManager instance. */
fun getClusterManager(): ClusterManager<MarkerClusterItem> {
return clusterManager
}

/** Gets all items in this cluster manager. */
fun getItems(): Collection<MarkerClusterItem> {
return clusterManager.algorithm.items
}

/** Gets all current clusters. */
fun getClusters(): List<ClusterDto> {
val clusters = mutableListOf<ClusterDto>()

// Get all clusters from the cluster manager algorithm
clusterManager.algorithm.getClusters(googleMap.cameraPosition.zoom).forEach { cluster ->
val markerIds = cluster.items.map { (it as MarkerClusterItem).markerId }
val position =
LatLngDto(latitude = cluster.position.latitude, longitude = cluster.position.longitude)

clusters.add(
ClusterDto(clusterManagerId = clusterManagerId, position = position, markerIds = markerIds)
)
}

return clusters
}

private fun onClusterClick(cluster: Cluster<MarkerClusterItem>): Boolean {
if (cluster.size > 0) {
val markerIds = cluster.items.map { it.markerId }
val position =
LatLngDto(latitude = cluster.position.latitude, longitude = cluster.position.longitude)

val clusterDto =
ClusterDto(clusterManagerId = clusterManagerId, position = position, markerIds = markerIds)

viewEventApi.onClusterEvent(
viewId.toLong(),
clusterManagerId,
ClusterEventTypeDto.CLICKED,
clusterDto,
) {}
}
// Return false to allow the default behavior of the cluster click event to occur.
return false
}

private fun onClusterItemClick(item: MarkerClusterItem): Boolean {
viewEventApi.onMarkerEvent(viewId.toLong(), item.markerId, MarkerEventTypeDto.CLICKED) {}
return item.consumeTapEvents
}

/**
* Custom renderer for cluster items. Handles marker rendering and stores marker-to-item mapping.
*/
inner class ClusterRenderer(
context: Context,
map: GoogleMap,
clusterManager: ClusterManager<MarkerClusterItem>,
) : DefaultClusterRenderer<MarkerClusterItem>(context, map, clusterManager) {

override fun onBeforeClusterItemRendered(
item: MarkerClusterItem,
markerOptions: com.google.android.gms.maps.model.MarkerOptions,
) {
// Apply marker options from the cluster item
val itemOptions = item.getMarkerDto().options
markerOptions.apply {
position(Convert.convertLatLngFromDto(itemOptions.position))
title(itemOptions.infoWindow.title)
snippet(itemOptions.infoWindow.snippet)
alpha(itemOptions.alpha.toFloat())
draggable(itemOptions.draggable)
flat(itemOptions.flat)
rotation(itemOptions.rotation.toFloat())
visible(itemOptions.visible)
zIndex(itemOptions.zIndex.toFloat())
anchor(itemOptions.anchor.u.toFloat(), itemOptions.anchor.v.toFloat())
infoWindowAnchor(
itemOptions.infoWindow.anchor.u.toFloat(),
itemOptions.infoWindow.anchor.v.toFloat(),
)

// Set custom icon if available
item.registeredImage?.let { icon(it.bitmapDescriptor) }
}
}

override fun onClusterItemRendered(item: MarkerClusterItem, marker: Marker) {
// Store the mapping between marker and cluster item
markerToClusterItem[marker] = item
}
}

/** Finds the cluster item associated with a marker. */
fun findClusterItem(marker: Marker): MarkerClusterItem? {
return markerToClusterItem[marker]
}
}
Loading
Loading