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. Not that because we're using the experimental ExposedDropdownMenuBox, so we need to declare we're ok with using experimental API.

show in full file app/src/main/java/com/androidbyexample/compose/google/google/maps/MapTypeSelector.kt
// ...
import com.google.maps.android.compose.MapType

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun 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
                )
            }
        }
    }
}

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.

show in full file app/src/main/java/com/androidbyexample/compose/google/google/maps/MapTypeSelector.kt
// ...

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MapTypeSelector(
    // ...
    onMapTypeClick: (MapType) -> Unit,
) {
    var expanded by remember {
        mutableStateOf(false)
    }
    ExposedDropdownMenuBox(
        expanded = expanded,
        onExpandedChange = { expanded = !expanded },
        modifier = modifier,
    ) {
        // ...
    }
}

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.

show in full file app/src/main/java/com/androidbyexample/compose/google/google/maps/MapTypeSelector.kt
// ...

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MapTypeSelector(
    // ...
) {
    // ...
    ExposedDropdownMenuBox(
        // ...
        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,
            // ...
    }
}

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.

show in full file app/src/main/java/com/androidbyexample/compose/google/google/maps/MapTypeSelector.kt
// ...

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MapTypeSelector(
    // ...
) {
    // ...
    ExposedDropdownMenuBox(
        // ...
    ) {
        // ...
            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
                )
            }
        }
    }
}

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.

show in full file app/src/main/java/com/androidbyexample/compose/google/google/maps/GoogleMapDisplay.kt
// ...

@Composable
fun GoogleMapDisplay(
    // ...
) {
    var mapLoaded by remember { mutableStateOf(false) }

    var currentMapType by remember { mutableStateOf(MapType.NORMAL) }
    var mapProperties by remember {
        mutableStateOf(MapProperties(mapType = MapType.NORMAL))
    }

    val placeState =
        // ...
}

For our layout, we'll wrap the GoogleMap and MapTypeSelector in a Column. The MapTypeSelector stays a fixed size,

show in full file app/src/main/java/com/androidbyexample/compose/google/google/maps/GoogleMapDisplay.kt
// ...

@Composable
fun GoogleMapDisplay(
    // ...
) {
    // ...
    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()
                )
            }
        }
    }
}

while we set the GoogleMap weight to fill the remaining height (and tell it to fill the available width).

show in full file app/src/main/java/com/androidbyexample/compose/google/google/maps/GoogleMapDisplay.kt
// ...

@Composable
fun GoogleMapDisplay(
    // ...
) {
    // ...
    Box(
        // ...
    ) {
        Column(
            // ...
        ) {
            // ...
            GoogleMap(
                // ...
                },
                properties = mapProperties,
//          modifier = Modifier.fillMaxSize(),
                modifier = Modifier
                    .fillMaxWidth()
                    .weight(1f),
            ) {
                MarkerInfoWindowContent(
                    // ...
            }
        }
        // ...
    }
}

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.

show in full file app/src/main/java/com/androidbyexample/compose/google/google/maps/GoogleMapDisplay.kt
// ...

@Composable
fun GoogleMapDisplay(
    // ...
) {
    // ...
    Box(
        // ...
    ) {
        Column(
            // ...
        ) {
            // ...
            GoogleMap(
                cameraPositionState = cameraPositionState,
                onMapLoaded = {
                    mapLoaded = true
                },
                properties = mapProperties,
//          modifier = Modifier.fillMaxSize(),
                modifier = Modifier
                    // ...
            ) {
                // ...
            }
        }
        // ...
    }
}

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


All code changes

CHANGED: app/src/main/java/com/androidbyexample/compose/google/google/maps/GoogleMapDisplay.kt
package com.androidbyexample.compose.google.google.maps

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.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
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.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.MapProperties
import com.google.maps.android.compose.MapType
import com.google.maps.android.compose.MarkerInfoWindowContent
import com.google.maps.android.compose.rememberMarkerState

@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/compose/google/google/maps/MapTypeSelector.kt
package com.androidbyexample.compose.google.google.maps

import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
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 androidx.compose.ui.res.stringResource
import com.google.maps.android.compose.MapType

@OptIn(ExperimentalMaterial3Api::class) @Composable fun 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 Maps</string>
    <string name="map_type">Map Type</string>
</resources>