Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 6 additions & 3 deletions maps-compose-utils/api/maps-compose-utils.api
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ public final class com/google/maps/android/compose/clustering/ClusteringKt {
public static final fun ClusteringMarkerProperties-MqvHWfI (Landroidx/compose/ui/geometry/Offset;Ljava/lang/Float;Ljava/lang/Float;Ljava/lang/String;Landroidx/compose/runtime/Composer;II)V
public static final synthetic fun ClusteringMarkerProperties-ueCybiQ (Landroidx/compose/ui/geometry/Offset;Ljava/lang/Float;Ljava/lang/Float;Landroidx/compose/runtime/Composer;II)V
public static final fun getLocalClusteringMarkerProperties ()Landroidx/compose/runtime/ProvidableCompositionLocal;
public static final fun rememberClusterManager (Landroidx/compose/runtime/Composer;I)Lcom/google/maps/android/clustering/ClusterManager;
public static final fun rememberClusterRenderer (Lcom/google/maps/android/clustering/ClusterManager;Landroidx/compose/runtime/Composer;I)Lcom/google/maps/android/clustering/view/ClusterRenderer;
public static final fun rememberClusterRenderer-Cq5_NK8 (Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;JJFFFFLcom/google/maps/android/clustering/ClusterManager;Landroidx/compose/runtime/Composer;II)Lcom/google/maps/android/clustering/view/ClusterRenderer;
public static final synthetic fun rememberClusterManager (Landroidx/compose/runtime/Composer;I)Lcom/google/maps/android/clustering/ClusterManager;
public static final fun rememberClusterManager (Lkotlin/coroutines/CoroutineContext;Lkotlin/coroutines/CoroutineContext;Landroidx/compose/runtime/Composer;II)Lcom/google/maps/android/clustering/ClusterManager;
public static final synthetic fun rememberClusterRenderer (Lcom/google/maps/android/clustering/ClusterManager;Landroidx/compose/runtime/Composer;I)Lcom/google/maps/android/clustering/view/ClusterRenderer;
public static final fun rememberClusterRenderer (Lcom/google/maps/android/clustering/ClusterManager;Ljava/util/concurrent/Executor;Landroidx/compose/runtime/Composer;II)Lcom/google/maps/android/clustering/view/ClusterRenderer;
public static final synthetic fun rememberClusterRenderer-Cq5_NK8 (Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;JJFFFFLcom/google/maps/android/clustering/ClusterManager;Landroidx/compose/runtime/Composer;II)Lcom/google/maps/android/clustering/view/ClusterRenderer;
public static final fun rememberClusterRenderer-VpbBEnE (Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;JJFFFFLcom/google/maps/android/clustering/ClusterManager;Ljava/util/concurrent/Executor;Landroidx/compose/runtime/Composer;II)Lcom/google/maps/android/clustering/view/ClusterRenderer;
}

public final class com/google/maps/android/compose/clustering/ClusteringMarkerProperties {
Expand Down
3 changes: 3 additions & 0 deletions maps-compose-utils/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -84,5 +84,8 @@ dependencies {
implementation(libs.kotlinx.coroutines.android)
api(libs.maps.utils)

testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.mockk)
testImplementation(libs.robolectric)
testImplementation(libs.test.junit)
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.model.BitmapDescriptor
import com.google.android.gms.maps.model.BitmapDescriptorFactory
import com.google.android.gms.maps.model.Marker
import com.google.android.gms.maps.model.MarkerOptions
import com.google.maps.android.clustering.Cluster
import com.google.maps.android.clustering.ClusterItem
Expand All @@ -56,6 +57,7 @@ import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import java.util.concurrent.Executor

internal interface ClusterRendererItemState<T : ClusterItem> {
val unclusteredItems: State<Set<T>>
Expand All @@ -66,27 +68,81 @@ internal interface ClusterRendererItemState<T : ClusterItem> {
* [clusterContentState] renders clusters, and [clusterItemContentState] renders non-clustered
* items.
*/
internal class ComposeUiClusterRenderer<T : ClusterItem>(
private val context: Context,
private val scope: CoroutineScope,
map: GoogleMap,
clusterManager: ClusterManager<T>,
private val viewRendererState: State<ComposeUiViewRenderer>,
private val clusterContentState: State<@Composable ((Cluster<T>) -> Unit)?>,
private val clusterItemContentState: State<@Composable ((T) -> Unit)?>,
private val clusterContentAnchorState: State<Offset>,
private val clusterItemContentAnchorState: State<Offset>,
private val clusterContentZIndexState: State<Float>,
private val clusterItemContentZIndexState: State<Float>,
private val clusterContentRotationState: State<Float>,
private val clusterItemContentRotationState: State<Float>,
) : DefaultClusterRenderer<T>(
context,
map,
clusterManager,
),
internal class ComposeUiClusterRenderer<T : ClusterItem> :
DefaultClusterRenderer<T>,
ClusterRendererItemState<T> {

private val context: Context
private val scope: CoroutineScope
private val viewRendererState: State<ComposeUiViewRenderer>
private val clusterContentState: State<@Composable ((Cluster<T>) -> Unit)?>
private val clusterItemContentState: State<@Composable ((T) -> Unit)?>
private val clusterContentAnchorState: State<Offset>
private val clusterItemContentAnchorState: State<Offset>
private val clusterContentZIndexState: State<Float>
private val clusterItemContentZIndexState: State<Float>
private val clusterContentRotationState: State<Float>
private val clusterItemContentRotationState: State<Float>

constructor(
context: Context,
scope: CoroutineScope,
map: GoogleMap,
clusterManager: ClusterManager<T>,
viewRendererState: State<ComposeUiViewRenderer>,
clusterContentState: State<@Composable ((Cluster<T>) -> Unit)?>,
clusterItemContentState: State<@Composable ((T) -> Unit)?>,
clusterContentAnchorState: State<Offset>,
clusterItemContentAnchorState: State<Offset>,
clusterContentZIndexState: State<Float>,
clusterItemContentZIndexState: State<Float>,
clusterContentRotationState: State<Float>,
clusterItemContentRotationState: State<Float>,
) : super(context, map, clusterManager) {
this.context = context
this.scope = scope
this.viewRendererState = viewRendererState
this.clusterContentState = clusterContentState
this.clusterItemContentState = clusterItemContentState
this.clusterContentAnchorState = clusterContentAnchorState
this.clusterItemContentAnchorState = clusterItemContentAnchorState
this.clusterContentZIndexState = clusterContentZIndexState
this.clusterItemContentZIndexState = clusterItemContentZIndexState
this.clusterContentRotationState = clusterContentRotationState
this.clusterItemContentRotationState = clusterItemContentRotationState
startObservingProperties()
}

constructor(
context: Context,
scope: CoroutineScope,
map: GoogleMap,
clusterManager: ClusterManager<T>,
viewRendererState: State<ComposeUiViewRenderer>,
clusterContentState: State<@Composable ((Cluster<T>) -> Unit)?>,
clusterItemContentState: State<@Composable ((T) -> Unit)?>,
clusterContentAnchorState: State<Offset>,
clusterItemContentAnchorState: State<Offset>,
clusterContentZIndexState: State<Float>,
clusterItemContentZIndexState: State<Float>,
clusterContentRotationState: State<Float>,
clusterItemContentRotationState: State<Float>,
executor: Executor,
) : super(context, map, clusterManager, executor) {
this.context = context
this.scope = scope
this.viewRendererState = viewRendererState
this.clusterContentState = clusterContentState
this.clusterItemContentState = clusterItemContentState
this.clusterContentAnchorState = clusterContentAnchorState
this.clusterItemContentAnchorState = clusterItemContentAnchorState
this.clusterContentZIndexState = clusterContentZIndexState
this.clusterItemContentZIndexState = clusterItemContentZIndexState
this.clusterContentRotationState = clusterContentRotationState
this.clusterItemContentRotationState = clusterItemContentRotationState
startObservingProperties()
}

override val unclusteredItems = mutableStateOf(emptySet<T>())

private val fakeCanvas = Canvas()
Expand All @@ -109,7 +165,7 @@ internal class ComposeUiClusterRenderer<T : ClusterItem>(
}
}

init {
private fun startObservingProperties() {
// Observe top-level Clustering property state updates across rotation, anchor, and zIndex.
// When these states update from a parent recomposition, actively push the updated values
// to any existing Marker objects currently on the map.
Expand All @@ -127,22 +183,26 @@ internal class ComposeUiClusterRenderer<T : ClusterItem>(
for ((key, viewInfo) in keysToViews) {
when (key) {
is ViewKey.Cluster -> {
getMarker(key.cluster)?.apply {
val props = viewInfo.view.properties
val anchor = props.anchor ?: clusterContentAnchorState.value
setAnchor(anchor.x, anchor.y)
zIndex = props.zIndex ?: clusterContentZIndexState.value
rotation = props.rotation ?: clusterContentRotationState.value
getMarker(key.cluster)?.let { marker ->
applyClusterMarkerProperties(
marker = marker,
properties = viewInfo.view.properties,
defaultAnchor = clusterContentAnchorState.value,
defaultZIndex = clusterContentZIndexState.value,
defaultRotation = clusterContentRotationState.value,
)
}
}

is ViewKey.Item -> {
getMarker(key.item)?.apply {
val props = viewInfo.view.properties
val anchor = props.anchor ?: clusterItemContentAnchorState.value
setAnchor(anchor.x, anchor.y)
zIndex = props.zIndex ?: clusterItemContentZIndexState.value
rotation = props.rotation ?: clusterItemContentRotationState.value
getMarker(key.item)?.let { marker ->
applyClusterMarkerProperties(
marker = marker,
properties = viewInfo.view.properties,
defaultAnchor = clusterItemContentAnchorState.value,
defaultZIndex = clusterItemContentZIndexState.value,
defaultRotation = clusterItemContentRotationState.value,
)
}
}
}
Expand Down Expand Up @@ -266,22 +326,28 @@ internal class ComposeUiClusterRenderer<T : ClusterItem>(
.collectLatest {
when (key) {
is ViewKey.Cluster -> {
getMarker(key.cluster)?.apply {
setIcon(renderViewToBitmapDescriptor(view))
val anchor = view.properties.anchor ?: clusterContentAnchorState.value
setAnchor(anchor.x, anchor.y)
zIndex = view.properties.zIndex ?: clusterContentZIndexState.value
rotation = view.properties.rotation ?: clusterContentRotationState.value
getMarker(key.cluster)?.let { marker ->
marker.setIcon(renderViewToBitmapDescriptor(view))
applyClusterMarkerProperties(
marker = marker,
properties = view.properties,
defaultAnchor = clusterContentAnchorState.value,
defaultZIndex = clusterContentZIndexState.value,
defaultRotation = clusterContentRotationState.value,
)
}
}

is ViewKey.Item -> {
getMarker(key.item)?.apply {
setIcon(renderViewToBitmapDescriptor(view))
val anchor = view.properties.anchor ?: clusterItemContentAnchorState.value
setAnchor(anchor.x, anchor.y)
zIndex = view.properties.zIndex ?: clusterItemContentZIndexState.value
rotation = view.properties.rotation ?: clusterItemContentRotationState.value
getMarker(key.item)?.let { marker ->
applyClusterItemMarkerProperties(
marker = marker,
icon = renderViewToBitmapDescriptor(view),
properties = view.properties,
defaultAnchor = clusterItemContentAnchorState.value,
defaultZIndex = clusterItemContentZIndexState.value,
defaultRotation = clusterItemContentRotationState.value,
)
}
}
}
Expand Down Expand Up @@ -321,7 +387,7 @@ internal class ComposeUiClusterRenderer<T : ClusterItem>(
}
}

override fun onBeforeClusterItemRendered(item: T, markerOptions: MarkerOptions) {
public override fun onBeforeClusterItemRendered(item: T, markerOptions: MarkerOptions) {
super.onBeforeClusterItemRendered(item, markerOptions)
if (!scope.isActive) return

Expand All @@ -344,6 +410,38 @@ internal class ComposeUiClusterRenderer<T : ClusterItem>(
}
}

public override fun onClusterUpdated(cluster: Cluster<T>, marker: Marker) {
super.onClusterUpdated(cluster, marker)
if (!scope.isActive) return
if (clusterContentState.value != null) {
val viewInfo = keysToViews[ViewKey.Cluster(cluster)]
applyClusterMarkerProperties(
marker = marker,
properties = viewInfo?.view?.properties,
defaultAnchor = clusterContentAnchorState.value,
defaultZIndex = clusterContentZIndexState.value,
defaultRotation = clusterContentRotationState.value,
)
}
}

public override fun onClusterItemUpdated(item: T, marker: Marker) {
super.onClusterItemUpdated(item, marker)
if (!scope.isActive) return
if (clusterItemContentState.value != null) {
val existingViewInfo = keysToViews[ViewKey.Item(item)]
val viewInfo = existingViewInfo ?: createAndAddView(ViewKey.Item(item))
applyClusterItemMarkerProperties(
marker = marker,
icon = if (existingViewInfo == null) renderViewToBitmapDescriptor(viewInfo.view) else null,
properties = viewInfo.view.properties,
defaultAnchor = clusterItemContentAnchorState.value,
defaultZIndex = clusterItemContentZIndexState.value,
defaultRotation = clusterItemContentRotationState.value,
)
}
}

private fun renderViewToBitmapDescriptor(view: AbstractComposeView): BitmapDescriptor {
/* AndroidComposeView triggers LayoutNode's layout phase in the View draw phase,
so trigger a draw to an empty canvas to force that */
Expand Down Expand Up @@ -431,3 +529,36 @@ internal class ComposeUiClusterRenderer<T : ClusterItem>(
*/
internal fun <T : ClusterItem> resolveItemContentDescription(item: T, customContentDescription: String?): String? =
customContentDescription ?: item.title

internal fun applyClusterMarkerProperties(
marker: Marker,
properties: ClusteringMarkerProperties?,
defaultAnchor: Offset,
defaultZIndex: Float,
defaultRotation: Float,
) {
val anchor = properties?.anchor ?: defaultAnchor
marker.setAnchor(anchor.x, anchor.y)
marker.zIndex = properties?.zIndex ?: defaultZIndex
marker.rotation = properties?.rotation ?: defaultRotation
}

internal fun applyClusterItemMarkerProperties(
marker: Marker,
icon: BitmapDescriptor? = null,
properties: ClusteringMarkerProperties,
defaultAnchor: Offset,
defaultZIndex: Float,
defaultRotation: Float,
) {
if (icon != null) {
marker.setIcon(icon)
}
applyClusterMarkerProperties(
marker = marker,
properties = properties,
defaultAnchor = defaultAnchor,
defaultZIndex = defaultZIndex,
defaultRotation = defaultRotation,
)
}
Loading
Loading