Search code examples
kotlingoogle-mapsandroid-jetpack-composegoogle-maps-compose

How to automatically adjust zoom to accommodate all markers in Google Maps Compose?


Google Maps Compose library provides cameraPositionState to accomodate 1 location with zoom using below code

val singapore = LatLng(1.35, 103.87)
val cameraPositionState = rememberCameraPositionState {
position = CameraPosition.fromLatLngZoom(singapore, 10f)
}
GoogleMap(
modifier = Modifier.fillMaxSize(),
cameraPositionState = cameraPositionState)

How to accomplish the same wit multiple locations ? I have a list of locations and I want to initialise GoogleMap to display all location markers.


Solution

  • Use LatLngBounds.builder() to create a minimum bound based on a set of LatLng points, then use cameraPositionState.move() to update the camera to the specified bounds

    Instead of using LatLngs to specify the camera position, you can use LatLngBounds instead as arguments to the CameraUpdateFactory.NewLatLngBounds() documented here: https://developers.google.com/android/reference/com/google/android/gms/maps/CameraUpdateFactory#public-static-cameraupdate-newlatlngbounds-latlngbounds-bounds,-int-padding

    But you would need to have the LatLngBounds value first. In that case, you can use the LatLngBounds.builder() method to create an instance of a bounds to be used in your camera update.

    So Let's say I have the following coordinates used to instantiate markers:

    private val santiago = LatLng(-33.4489, -70.6693)
    private val bogota = LatLng(-4.7110, -74.0721)
    private val lima = LatLng(-12.0464, -77.0428)
    private val salvador = LatLng(-12.9777, -38.5016)
    

    Once I have that, I'll need to instantiate the builder:

    val boundsBuilder = LatLngBounds.builder()
    

    Create a list for the coordinates:

    val coordinates = listOf(
                santiago,
                bogota,
                lima,
                salvador
            )
    

    Then loop through it and use the include() method of the builder() through each LatLng coordinate:

    for (coordinate in coordinates) {
        boundsBuilder.include(coordinate)
    }
    

    Then build it with the build() method and store its return value as the bounds to be used later on in updating your camera.

    val bounds = boundsBuilder.build()
    

    Then inside your setContent, if you want to update the camera on initial Load to include all the markers, you can just use the cameraPositionState.move() method within a LaunchedEffect composable. And it would look something like this:

    LaunchedEffect(key1 = true ){
        cameraPositionState.move(
            update = CameraUpdateFactory.newLatLngBounds(bounds, 100)
        )
    }
    

    Notice that the cameraPositionState.move() method has an update parameter. That's where you just have to put the bounds value obtained earlier by making the update value into CameraUpdateFactory.newLatLngBounds(BOUNDS_VALUE_HERE, PADDING_VALUE_HERE)

    Here's a sample implementation that you can try where I also added a button for you to test the camera update outside the initial load:

    private val santiago = LatLng(-33.4489, -70.6693)
    private val bogota = LatLng(-4.7110, -74.0721)
    private val lima = LatLng(-12.0464, -77.0428)
    private val salvador = LatLng(-12.9777, -38.5016)
    private val center = LatLng(-18.000, -58.000)
    private val defaultCameraPosition1 = CameraPosition.fromLatLngZoom(center, 2f)
    
    class StackOverflowSample : ComponentActivity(), OnMapsSdkInitializedCallback {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            MapsInitializer.initialize(applicationContext, MapsInitializer.Renderer.LATEST, this)
            val boundsBuilder = LatLngBounds.builder()
    
            val coordinates = listOf(
                santiago,
                bogota,
                lima,
                salvador
            )
    
            for (coordinate in coordinates) {
                boundsBuilder.include(coordinate)
            }
            val bounds = boundsBuilder.build()
            setContent {
                // Observing and controlling the camera's state can be done with a CameraPositionState
                val cameraPositionState = rememberCameraPositionState {
                    position = defaultCameraPosition1
                }
                val mapProperties by remember {
                    mutableStateOf(MapProperties(mapType = MapType.NORMAL))
                }
                val marker1State = rememberMarkerState(position = santiago)
                val marker2State = rememberMarkerState(position = bogota)
                val marker3State = rememberMarkerState(position = lima)
                val marker4State = rememberMarkerState(position = salvador)
    
                // Drawing on the map is accomplished with a child-based API
                val markerClick: (Marker) -> Boolean = {
                    Log.d(TAG, "${it.title} was clicked")
                    cameraPositionState.projection?.let { projection ->
                        Log.d(TAG, "The current projection is: $projection")
                    }
                    false
                }
                Column(
                    modifier = Modifier.fillMaxSize()
                ) {
                    Button(onClick = {
                        cameraPositionState.move(
                            update = CameraUpdateFactory.newLatLngBounds(bounds, 100)
                        )
                    }) {
                        Text("Update Camera")
                    }
                    Box(Modifier.fillMaxSize()) {
                        LaunchedEffect(key1 = true ){
                            cameraPositionState.move(
                                update = CameraUpdateFactory.newLatLngBounds(bounds, 100)
                            )
                        }
                        GoogleMap(
                            modifier = Modifier.matchParentSize(),
                            googleMapOptionsFactory = {
                                GoogleMapOptions().mapId("DEMO_MAP_ID")
                            },
                            cameraPositionState = cameraPositionState,
                            properties = mapProperties,
                            onPOIClick = {
                                Log.d(TAG, "POI clicked: ${it.name}")
                            }
                        ) {
    
                            val textView = TextView(this@AdvancedMarkersActivity)
                            textView.text = "Hello!!"
                            textView.setBackgroundColor(Color.BLACK)
                            textView.setTextColor(Color.YELLOW)
    
                            AdvancedMarker(
                                state = marker4State,
                                onClick = markerClick,
                                collisionBehavior = 1,
                                iconView = textView,
                                title="Marker 4"
                            )
    
                            val pinConfig = PinConfig.builder()
                                .setBackgroundColor(Color.MAGENTA)
                                .setBorderColor(Color.WHITE)
                                .build()
    
                            AdvancedMarker(
                                state = marker1State,
                                onClick = markerClick,
                                collisionBehavior = 1,
                                pinConfig = pinConfig,
                                title="Marker 1"
                            )
    
                            val glyphOne = PinConfig.Glyph("A", Color.BLACK)
                            val pinConfig2 = PinConfig.builder()
                                .setGlyph(glyphOne)
                                .build()
    
                            AdvancedMarker(
                                state = marker2State,
                                onClick = markerClick,
                                collisionBehavior = 1,
                                pinConfig = pinConfig2,
                                title="Marker 2"
                            )
    
                            val glyphImage: Int = ic_menu_myplaces
                            val descriptor = BitmapDescriptorFactory.fromResource(glyphImage)
                            val pinConfig3 = PinConfig.builder()
                                .setGlyph(PinConfig.Glyph(descriptor))
                                .build()
    
                            AdvancedMarker(
                                state = marker3State,
                                onClick = markerClick,
                                collisionBehavior = 1,
                                pinConfig = pinConfig3,
                                title="Marker 3"
                            )
    
                        }
                    }
                }
    
            }
        }
    
        override fun onMapsSdkInitialized(renderer: MapsInitializer.Renderer) {
            when (renderer) {
                MapsInitializer.Renderer.LATEST -> Log.d("MapsDemo", "The latest version of the renderer is used.")
                MapsInitializer.Renderer.LEGACY -> Log.d("MapsDemo", "The legacy version of the renderer is used.")
                else -> {}
            }
        }
    }