Google Map
Navigating
The Google Map SDK for Android's terms of use forbids using Google's navigation data for real-time navigation in your own application. If you want to show the user how to get from point A to B in real time, you need to launch the Google Maps application itself.
We launch navigation using an Android Intent
. An Intent
describes something you would like to
do, typically with a different application or system service. This intent contains a URI that
represents the navigation request:
show in full file app/src/main/java/com/androidbyexample/compose/google/google/maps/GoogleMapDisplay.kt
// ...
@Composable
fun GoogleMapDisplay(
// ...
) {
// ...
Scaffold(
topBar = {
CarTopBar(
// ...
onSetCarLocation = onSetCarLocation,
onClearCarLocation = onClearCarLocation,
// onWalkToCar = { TODO() },
onWalkToCar = {
currentLocation?.let { curr ->
carLatLng?.let { car ->
val uri =
Uri.parse(
"https://www.google.com/maps/dir/" +
"?api=1&origin=${curr.latitude}," +
"${curr.longitude}&" +
"destination=${car.latitude}," +
"${car.longitude}&travelmode=walking")
context.startActivity(
Intent(
Intent.ACTION_VIEW,
uri
).apply {
setPackage("com.google.android.apps.maps")
})
} ?: Toast.makeText(
context,
"Cannot navigate; no car location available",
Toast.LENGTH_LONG
).show()
} ?: Toast.makeText(
context,
"Cannot navigate; no current location available",
Toast.LENGTH_LONG
).show()
},
onGoToCurrentLocation = {
currentLocation?.let { curr ->
// ...
)
},
// ...
)
}
https://www.google.com/maps/dir/?api=1&origin=${curr.latitude},${curr.longitude}&destination=${car.latitude},${car.longitude}&travelmode=walking
The Google Maps application registers IntentFilters
that watch for URIs starting with
"https://www.google.com/maps". The Android platform directs this Intent
to Google Maps, and it
presents navigation options:
All code changes
CHANGED: app/src/main/java/com/androidbyexample/compose/google/google/maps/GoogleMapDisplay.kt
package com.androidbyexample.compose.google.google.maps
import android.content.Intent
import android.location.Location
import android.net.Uri
import android.widget.Toast
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.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.model.BitmapDescriptor
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.MarkerState
import com.google.maps.android.compose.rememberMarkerState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun GoogleMapDisplay(
currentLocation: Location?,
carLatLng: LatLng?,
cameraPositionState: CameraPositionState,
onSetCarLocation: () -> Unit,
onClearCarLocation: () -> Unit,
onMoveCar: (LatLng) -> Unit,
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 currentLocationState = remember(currentLocation) {
currentLocation?.let {
MarkerState(
LatLng(
it.latitude,
it.longitude
)
)
}
}
val context = LocalContext.current
var currentLocationIcon by
remember { mutableStateOf<BitmapDescriptor?>(null) }
val carState = rememberMarkerState("car")
var carIcon by remember { mutableStateOf<BitmapDescriptor?>(null) }
val scope = rememberCoroutineScope()
var initialBoundsSet by remember { mutableStateOf(false) }
LaunchedEffect(key1 = mapLoaded, key2 = currentLocation) {
if (mapLoaded) {
if (currentLocation != null) {
if (!initialBoundsSet) {
initialBoundsSet = true
val current =
LatLng(currentLocation.latitude, currentLocation.longitude)
cameraPositionState.animate(
CameraUpdateFactory.newLatLngZoom(
current,
16f
), 1000
)
}
}
}
}
LaunchedEffect(true) {
var draggedAtLeastOnce = false
snapshotFlow { carState.isDragging }
.collect { dragging ->
// Make sure we've seen at least one drag state before updating
// the view model. Otherwise we'll see the initial (0.0, 0.0)
// value that was set when the MarkerState was created
if (dragging) {
draggedAtLeastOnce = true
} else if (draggedAtLeastOnce) {
draggedAtLeastOnce = false
onMoveCar(carState.position)
}
}
}
Scaffold(
topBar = {
CarTopBar(
currentLocation = currentLocation,
carLatLng = carLatLng,
onSetCarLocation = onSetCarLocation,
onClearCarLocation = onClearCarLocation,
onGoToCurrentLocation = {
currentLocation?.let { curr ->
scope.launch {
cameraPositionState.animate(
CameraUpdateFactory.newLatLngZoom(
LatLng(curr.latitude, curr.longitude),
16f
), 1000
)
}
} ?: Toast.makeText(
context,
"No current location available",
Toast.LENGTH_LONG
).show()
},
)
},
content = { paddingValues ->
Box(
modifier = modifier.padding(paddingValues),
) {
Column(
modifier = Modifier.fillMaxSize()
) {
MapTypeSelector(
currentValue = currentMapType,
modifier = Modifier.fillMaxWidth(),
) {
mapProperties = mapProperties.copy(mapType = it)
currentMapType = it
}
GoogleMap(
cameraPositionState = cameraPositionState,
onMapLoaded = {
mapLoaded = true
scope.launch(Dispatchers.IO) {
currentLocationIcon =
context.loadBitmapDescriptor(
R.drawable.ic_current_location
)
carIcon =
context.loadBitmapDescriptor(
R.drawable.ic_car
)
}
},
properties = mapProperties,
modifier = Modifier
.fillMaxWidth()
.weight(1f),
) {
currentLocationState?.let {
MarkerInfoWindowContent(
state = it,
icon = currentLocationIcon,
anchor = Offset(0.5f, 0.5f),
title = stringResource(
id = R.string.current_location
),
)
}
carLatLng?.let {
carState.position = it
MarkerInfoWindowContent(
state = carState,
draggable = true,
icon = carIcon,
anchor = Offset(0.5f, 0.5f),
title = stringResource(
id = R.string.car_location
),
)
}
}
}
if (!mapLoaded) {
AnimatedVisibility(
visible = true,
modifier = Modifier.fillMaxSize(),
enter = EnterTransition.None,
exit = fadeOut()
) {
CircularProgressIndicator(
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.wrapContentSize()
)
}
}
}
}
)
}