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

Cancelled animation corrupts CameraPositionState and prevents drag gesture #502

Open
jeffphp opened this issue Jan 5, 2024 · 3 comments
Labels
triage me I really want to be triaged. type: bug Error or flaw in code with unintended results or allowing sub-optimal usage patterns.

Comments

@jeffphp
Copy link

jeffphp commented Jan 5, 2024

android-maps-compose 4.3.0

When an animation occurs and is cancelled (and in other circumstances that I did not manage to reproduce), the CameraPositionState gets corrupted and a drag gesture on the map doesn't work anymore as intended. Here is a code to reproduce the bug (click several times on the button and try to move the map, zoom is not affected somehow).

Environment details

  1. android-maps-compose 4.3.0
  2. doesn't seem to occur on the simulator, please use a real device.

Steps to reproduce

  1. Click several times on the button that animates the card, causing cancellation exceptions (caught and displayed)
  2. Try to move the map, it doesn't move as intended (zoom is not affected somehow)

Code example

class RecompositionActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val cameraPositionState = rememberCameraPositionState {
                position = defaultCameraPosition
            }
            Box(Modifier.fillMaxSize()) {
                MapsComposeSampleTheme {
                    GoogleMapView(
                        modifier = Modifier.matchParentSize(),
                        cameraPositionState = cameraPositionState
                    )
                }
            }
        }
    }


    @Composable
    fun GoogleMapView(
        modifier: Modifier = Modifier,
        cameraPositionState: CameraPositionState = rememberCameraPositionState(),
        content: @Composable () -> Unit = {},
    ) {
        var location by remember { mutableStateOf(singapore) }

        var zoom by remember { mutableStateOf(5f) }

        val context = LocalContext.current
        val singaporeState = MarkerState(position = location)


        val mapProperties = remember {
            MapProperties(
                isBuildingEnabled = false,
                isIndoorEnabled = false,
                isMyLocationEnabled = false,
                isTrafficEnabled = false,
                mapType = MapType.SATELLITE,
            )
        }

        val mapOptions = remember {
            GoogleMapOptions()
                .ambientEnabled(false)
                .mapToolbarEnabled(false)
                .compassEnabled(false)
        }

        val uiSettings = remember {
            MapUiSettings(
                compassEnabled = false,
                indoorLevelPickerEnabled = false,
                mapToolbarEnabled = false,
                myLocationButtonEnabled = false,
                rotationGesturesEnabled = false,
                scrollGesturesEnabledDuringRotateOrZoom = false,
                tiltGesturesEnabled = false,
                zoomControlsEnabled = false,
            )
        }


        val mapVisible by remember { mutableStateOf(true) }
        if (mapVisible) {

            LaunchedEffect(zoom) {
                try {
                    cameraPositionState.animate(
                        update = CameraUpdateFactory.newCameraPosition(
                            CameraPosition(
                                cameraPositionState.position.target,
                                zoom,
                                0f,
                                0f
                            ),

                            ), durationMs = 1000
                    )
                } catch(e: CancellationException) {
                    Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
                } catch(e: Exception) {
                    Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
                }
            }


            // Detect when the map starts moving and print the reason
            LaunchedEffect(cameraPositionState.isMoving) {
                if (cameraPositionState.isMoving) {
                    Log.d(TAG, "Map camera started moving due to ${cameraPositionState.cameraMoveStartedReason.name}")
                }
            }

            GoogleMap(
                modifier = modifier.fillMaxSize(),
                cameraPositionState = cameraPositionState,
                googleMapOptionsFactory = {mapOptions},
                onMapLoaded = {},
                properties = mapProperties,
                uiSettings = uiSettings,
                onPOIClick = {
                    Log.d(TAG, "POI clicked: ${it.name}")
                }
            ) {
                val markerClick: (Marker) -> Boolean = {
                    Log.d(TAG, "${it.title} was clicked")
                    cameraPositionState.projection?.let { projection ->
                        Log.d(TAG, "The current projection is: $projection")
                    }
                    false
                }

                Marker(
                    state = singaporeState,
                    title = "Marker in Singapore",
                    onClick = markerClick
                )

                content()
            }
            Column {

                Button(onClick = {
                    val randomValue = Random.nextInt(3)

                    location = when (randomValue) {
                        0 -> singapore
                        1 -> singapore2
                        2 -> singapore3
                        else -> singapore
                    }
                    zoom = when (randomValue) {
                        0 -> 3f
                        1 -> 6f
                        2 -> 15f
                        else -> 16f
                    }
                }) {
                    Text("Change Location & Zoom")
                }
            }
        }
    }
}

Thanks by advance!

@jeffphp jeffphp added triage me I really want to be triaged. type: bug Error or flaw in code with unintended results or allowing sub-optimal usage patterns. labels Jan 5, 2024
@wangela
Copy link
Member

wangela commented Jan 5, 2024

If you would like to upvote the priority of this issue, please comment below or react on the original post above with 👍 so we can see what is popular when we triage.

@jeffphp Thank you for opening this issue. 🙏
Please check out these other resources that might help you get to a resolution in the meantime:

This is an automated message, feel free to ignore.

@jeffphp
Copy link
Author

jeffphp commented Jan 5, 2024

A workaround fixing some problems consists in catching CancellationException and throw it again when it happens, then there won't be a discrepancy between cameraPositionState and drag gesture listener :

LaunchedEffect(****) {
        try {
            cameraPositionState.animate(
                update = CameraUpdateFactory.newCameraPosition(
                    CameraPosition(viewModel.mapPosition, viewModel.mapZoom, 0f, 0f)
                ),
                durationMs = 100
            )
        } catch(e: CancellationException) {
            throw e      
        } catch (e: Exception) {
            // ***        
        }
    }
}

@kikoso
Copy link
Collaborator

kikoso commented Jan 8, 2024

Hi @jeffphp ,

This does not seem to happen on my device, but did you try to call the animate function within a Coroutine scope?

coroutineScope.launch {
                        cameraPositionState.animate(CameraUpdateFactory.zoomOut())
                    }

From the documentation of LaunchEffect:

This function should not be used to (re-)launch ongoing tasks in response to callback events by way of storing callback data in MutableState passed to key. Instead, see rememberCoroutineScope to obtain a CoroutineScope that may be used to launch ongoing jobs scoped to the composition in response to event callbacks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
triage me I really want to be triaged. type: bug Error or flaw in code with unintended results or allowing sub-optimal usage patterns.
Projects
None yet
Development

No branches or pull requests

3 participants