Google Map
Map Types
Google Map provides several types of tiles:
- Normal (Streets)
- Satellite Images
- Hybrid of Streets/Satellite
- Terrain
- No visible map
By adding a dropdown list at the top of the screen, the user can select from these types.
We create a new file to host the map-type selector. We're using the experimental ExposedDropdownMenuBox
, so we need to declare we're ok with using experimental API.
Caution
Using experimental APIs can get you access to cool new features, but that API is not yet stable. If you use these in your application, there's a chance the API will change and you'll have to rewrite parts of your application. Use with care (if at all)!
The standard unidirectional data flow and externalized state patterns of Composable functions is again visible, as we need to declare expansion state and manage it in the call to ExposedDropdownMenuBox
. The expansion state is not needed outside of MapTypeSelector
so we keep it defined locally.
We're using a read-only TextField
to display the current map type. This gives us the nice little label at the top while showing the current value in a larger font. Note the use of the trailingIcon
parameter to add the expansion indicator for the drop-down list.
Setting the up the expansion menu should start to look similar to other controls. We tell it if it's expanded
or not, and it tells us if the user wanted to change the expansion state. Inside, we loop through the possible values of MapType
(it's an enumeration), creating each drop-down item.
When the user clicks an item, we tell the caller which MapType
was clicked and close the drop-down menu.
Back in our GoogleMapDisplay
, we keep track of the MapType
. We need to share that MapType
between the GoogleMap
and the MapTypeSelector
; defining it here makes it available to both.
The GoogleMap
uses a MapProperties
state holder to track the map type (among other options). We'll initialize it with the "normal" (streets-only) map type, and pass it to the GoogleMap
.
For our layout, we'll wrap the GoogleMap
and MapTypeSelector
in a Column
. The MapTypeSelector
stays a fixed size, while we set the GoogleMap
weight to fill the remaining height (and tell it to fill the available width).
The resulting application now looks like this (when "Normal" map type is selected)
and this (when "Satellite" map type is selected)
Code Changes
CHANGED: /app/src/main/java/com/androidbyexample/googlemap/MainActivity.kt
package com.androidbyexample.googlemap import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.EnterTransition import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import com.androidbyexample.googlemap.ui.theme.GoogleMapTheme import com.google.android.gms.maps.model.CameraPosition import com.google.android.gms.maps.model.LatLng import com.google.maps.android.compose.CameraPositionState import com.google.maps.android.compose.GoogleMap import com.google.maps.android.compose.MapPropertiesimport com.google.maps.android.compose.MapTypeimport com.google.maps.android.compose.MarkerInfoWindowContent import com.google.maps.android.compose.rememberCameraPositionState import com.google.maps.android.compose.rememberMarkerState private val googleHQ = LatLng(37.42423291057923, -122.08811454627153)private val defaultCameraPosition = CameraPosition.fromLatLngZoom(googleHQ, 11f)class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { GoogleMapTheme { // A surface container using the 'background' color from the theme Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) {val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition }GoogleMapDisplay( place = googleHQ, placeDescription = "Google HQ", cameraPositionState = cameraPositionState, modifier = Modifier.fillMaxSize(), )} } } } }@Composable fun GoogleMapDisplay( place: LatLng, placeDescription: String, cameraPositionState: CameraPositionState, modifier: Modifier, ) {var mapLoaded by remember { mutableStateOf(false) }var currentMapType by remember { mutableStateOf(MapType.NORMAL) } var mapProperties by remember { mutableStateOf(MapProperties(mapType = MapType.NORMAL)) }val placeState = rememberMarkerState(key = place.toString(), position = place)Box(modifier = modifier,) {Column( modifier = Modifier.fillMaxSize() ) { MapTypeSelector( currentValue = currentMapType, modifier = Modifier.fillMaxWidth(), ) { mapProperties = mapProperties.copy(mapType = it) currentMapType = it }GoogleMap(cameraPositionState = cameraPositionState,onMapLoaded = { mapLoaded = true },properties = mapProperties,) {// modifier = Modifier.fillMaxSize(),modifier = Modifier .fillMaxWidth() .weight(1f),MarkerInfoWindowContent( state = placeState, title = placeDescription, onClick = { placeState.showInfoWindow() true } )} } }if (!mapLoaded) { AnimatedVisibility( visible = true, modifier = Modifier.fillMaxSize(), enter = EnterTransition.None, exit = fadeOut() ) { CircularProgressIndicator( modifier = Modifier .background(MaterialTheme.colorScheme.background) .wrapContentSize() ) } }}
ADDED: /app/src/main/java/com/androidbyexample/googlemap/MapTypeSelector.kt
@file:OptIn(ExperimentalMaterial3Api::class)package com.androidbyexample.googlemapimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.material3.DropdownMenuItemimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.ExposedDropdownMenuBoximport androidx.compose.material3.ExposedDropdownMenuDefaultsimport androidx.compose.material3.Textimport androidx.compose.material3.TextFieldimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Modifierimport androidx.compose.ui.res.stringResourceimport com.google.maps.android.compose.MapType@Composablefun MapTypeSelector( currentValue: MapType, modifier: Modifier, onMapTypeClick: (MapType) -> Unit,) {var expanded by remember { mutableStateOf(false) }ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = !expanded },modifier = modifier, ) {TextField( value = currentValue.name, label = { Text(text = stringResource(id = R.string.map_type)) }, readOnly = true, // don't allow user to type onValueChange = {}, // unused trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, colors = ExposedDropdownMenuDefaults.textFieldColors(), modifier = Modifier.menuAnchor().fillMaxWidth() )}}
CHANGED: /app/src/main/res/values/strings.xml
<resources> <string name="app_name">Google map</string> <string name="map_type">Map Type</string></resources>