Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Icon blinks zooming in/out with PointAnnotationGroup/Compose #2332

Open
kevinMoonware opened this issue Apr 8, 2024 · 4 comments
Open

Icon blinks zooming in/out with PointAnnotationGroup/Compose #2332

kevinMoonware opened this issue Apr 8, 2024 · 4 comments
Labels
bug 🪲 Something isn't working compose

Comments

@kevinMoonware
Copy link

kevinMoonware commented Apr 8, 2024

Environment

  • Android OS version: 14
  • Devices affected: Not device specific but is seen on Samsung s21 ultra as well as Google Pixel 6.
  • Maps SDK Version: Can be observed from 10.x all the way to 11.2.2 (can not verify on 11.3 at this point)
  • Compose version: 1.6.2

Observed behavior and steps to reproduce

Icon blinks when zoom in/out. The blinking intensifies when zooming out and the icons are getting closer to each other. Blinking appears much less often when there are few than 3 icons visible within the map region (but it does still happen sometimes).

Please see video capture here:
https://youtube.com/shorts/es0iWngndEo?feature=share

Expected behavior

Icon(s) do not blink, or only blink when clustering ops are happening. Blinking should not happen during typical zoom in/out operation when zoom level is large enough that clustering is not a factor.

Notes / preliminary analysis

        MapboxMap(
            modifier = Modifier.fillMaxSize(),
            mapViewportState = mapViewportState,
            mapInitOptionsFactory = { context ->
                MapInitOptions(
                    context = context,
                    styleUri = "mapbox://styles/moonmapper/cloxllb7700q601qj462k2q76",
                )
            },
            gesturesSettings = GesturesSettings {
                quickZoomEnabled = true
                doubleTapToZoomInEnabled = true
                pitchEnabled = false
                this.build()
            },
            onMapClickListener = { point ->
            ....
            }
        ) {
            MapEffect(mapViewportState.cameraState.center) { mapView ->
                mapView.location.updateSettings {
                    enabled = true
                }
                val mapboxMap = mapView.mapboxMap
                mapView.scalebar.enabled = false
                .....
            }
            ...

            PointAnnotationGroup(
                iconOptional = true,
                iconAllowOverlap = true,
                iconIgnorePlacement = true,
                iconPitchAlignment = IconPitchAlignment.MAP,
                annotations = listOf(
                    flightWithPositions.toFlightAnnotationOptions(),
                    vehicleWithPositions.toVehicleAnnotationOptions(),
                    userWithPositions.toUserAnnotationOptions()
                ).flatten()
                    .filter {
                        it.getPoint()?.let { point ->
                            ComposeMapboxManager.bounds.value?.contains(point, true) == true
                        } ?: false },
                annotationConfig = AnnotationConfig(
                    annotationSourceOptions = AnnotationSourceOptions(
                        clusterOptions = ClusterOptions(
                            clusterMaxZoom = 16,
                            textColor = Color(0xFF000000).toArgb(),
                            textSize = 12.0,
                            circleRadiusExpression = literal(15.0),
                            colorLevels = listOf(
                                Pair(100, Color.Red.toArgb()),
                                Pair(50, Color.Blue.toArgb()),
                                Pair(0, Color(0xFFB0B7C3).toArgb())
                            )
                        )
                    )
                ),
                onClick = { annotation ->
                    ....
                }
            )
        }

Here is how I typically handle the creation of PointAnnotationOptions

@Composable
fun List<VehicleWithPosition>.toVehicleAnnotationOptions(): List<PointAnnotationOptions> {
    return this.map { vehicle ->
        val vehiclePoint = Point.fromLngLat(
            vehicle.positions[0].longitude,
            vehicle.positions[0].latitude
        )
        var options by remember {
            mutableStateOf(
                PointAnnotationOptions()
                    .withPoint(vehiclePoint)
                    .withData(
                        gson.toJsonTree(
                            AnnotationItem(
                                vehicleLasId = vehicle.vehicle.lasId
                            ), AnnotationItem::class.java
                        )
                    )
            )
        }

        LaunchedEffect(
            vehicle.positions[0].heading,
            vehicle.positions[0].latitude,
            vehicle.positions[0].longitude,
            vehicle.vehicle.getGseStatus(),
            vehicle.matchSelectedVehicle(),
            if (!ComposeMapboxManager.mapMoving.value) ComposeMapboxManager.cameraOptions.value?.bearing else null
        ) {
            val selected = vehicle.matchSelectedVehicle()
            val relativeHeading = vehicle.positions[0].heading - (ComposeMapboxManager.cameraOptions.value?.bearing?.toInt() ?: 0)
            val key = VehicleKey(
                vehicle.vehicle.name,
                vehicle.vehicle.getGseStatus(),
                relativeHeading,
                selected
            )
            withContext(Dispatchers.IO) {
                val iconImage = if (vehicleIconMap.get(key) == null) {
                    vehicleMarkerView(
                        heading = relativeHeading,
                        name = vehicle.vehicle.name,
                        type = vehicle.vehicle.type,
                        status = vehicle.vehicle.getGseStatus(),
                        batteryLevel = vehicle.positions[0].battery,
                        selected = selected
                    ).also {
                        vehicleIconMap.put(key, it)
                    }
                } else {
                    vehicleIconMap[key]!!
                }
                val updatedVehicleMarker = PointAnnotationOptions()
                    .withData(
                        gson.toJsonTree(
                            AnnotationItem(
                                vehicleLasId = vehicle.vehicle.lasId
                            ), AnnotationItem::class.java
                        )
                    )
                    .withIconImage(iconImage)
                    .withPoint(vehiclePoint)
                options = updatedVehicleMarker
            }
        }
        options.symbolSortKey = 3.0
        options.iconAnchor = IconAnchor.CENTER
        options
    }
}`

Additional links and references

@kevinMoonware kevinMoonware added the bug 🪲 Something isn't working label Apr 8, 2024
@olle-cpac
Copy link

olle-cpac commented Apr 24, 2024

I have the same issue on 11.3.0. The Icons even blink sometimes when not zooming, but less frequently.

@olle-cpac
Copy link

olle-cpac commented Apr 24, 2024

The blinking with no zooming ongoing can be reproduced by applying this diff

index 2b4d49016..1633def9e 100644
--- a/compose-app/src/main/java/com/mapbox/maps/compose/testapp/examples/annotation/PointAnnotationClusterActivity.kt
+++ b/compose-app/src/main/java/com/mapbox/maps/compose/testapp/examples/annotation/PointAnnotationClusterActivity.kt
@@ -2,6 +2,7 @@ package com.mapbox.maps.compose.testapp.examples.annotation
 
 import android.graphics.Color
 import android.os.Bundle
+import android.util.Log
 import android.widget.Toast
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
@@ -35,6 +36,7 @@ import com.mapbox.maps.plugin.annotation.ClusterOptions
 import com.mapbox.maps.plugin.annotation.generated.PointAnnotationOptions
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlinx.coroutines.withContext
 
@@ -54,6 +56,8 @@ public class PointAnnotationClusterActivity : ComponentActivity() {
         mutableStateOf<List<Point>>(listOf())
       }
 
+      var counter by remember { mutableStateOf(0) }
+
       MapboxMapComposeTheme {
         ExampleScaffold {
           MapboxMap(
@@ -68,6 +72,7 @@ public class PointAnnotationClusterActivity : ComponentActivity() {
               MapStyle(style = Style.LIGHT)
             }
           ) {
+            Log.d("TAG", "counter: $counter")
             PointAnnotationGroup(
               annotations = points.map {
                 PointAnnotationOptions()
@@ -99,6 +104,14 @@ public class PointAnnotationClusterActivity : ComponentActivity() {
               }
             )
           }
+          LaunchedEffect(Unit) {
+            withContext(Dispatchers.IO) {
+              while (true) {
+                counter++
+                delay(3000)
+              }
+            }
+          }
           LaunchedEffect(Unit) {
             withContext(Dispatchers.IO) {
               points = loadData()

It basically just forces recompose every 3 seconds

gj-loitp pushed a commit to gj-loitp/lib_mapbox-maps-android that referenced this issue May 4, 2024
* Expose MapInitOptions.mapName property

* PR fixes
@pengdev
Copy link
Member

pengdev commented May 7, 2024

Hey @kevinMoonware, the blink of view annotations is likely due to recomposition of your PointAnnotationGroup, due to the state changes in the annotations , I see you constructed annotations using

                annotations = listOf(
                    flightWithPositions.toFlightAnnotationOptions(),
                    vehicleWithPositions.toVehicleAnnotationOptions(),
                    userWithPositions.toUserAnnotationOptions()
                ).flatten()
                    .filter {
                        it.getPoint()?.let { point ->
                            ComposeMapboxManager.bounds.value?.contains(point, true) == true
                        } ?: false },

The recomposition of the parent composable will trigger the recalculation of the annotations, and since new annotation option instances are created, it will trigger the annotations being removed and added again.

To fix this issue, you can extract the calculation logic to a remember mutable state, so that new annotation options wouldn't be created each time PointAnnotationGroup is recomposed.

@olle-cpac
Copy link

If the points change they need to be recomposed. Remembering the annotation options will prevent that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug 🪲 Something isn't working compose
Projects
None yet
Development

No branches or pull requests

3 participants