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)

Normal Map Type

and this (when "Satellite" map type is selected)

Satellite Map Type

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() )
ExposedDropdownMenu( expanded = expanded, onDismissRequest = { expanded = false }, modifier = Modifier.fillMaxWidth(), ) { MapType.entries.forEach { DropdownMenuItem( text = { Text(text = it.name) }, onClick = { onMapTypeClick(it) expanded = false }, contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding ) } }
}}
CHANGED: /app/src/main/res/values/strings.xml
<resources>
    <string name="app_name">Google map</string>
    <string name="map_type">Map Type</string></resources>