Google Map
Navigating
The Google Map SDK for Android's terms of use forbid 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:
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:
Code Changes
CHANGED: /app/src/main/java/com/androidbyexample/googlemap/MainActivity.kt
package com.androidbyexample.googlemap import android.Manifest import android.annotation.SuppressLint import android.app.AlertDialog import android.content.Intent import android.content.pm.PackageManager import android.location.Location import android.net.Uri import android.os.Bundle import android.os.Looper import android.provider.Settings import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels 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.material3.Surface 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 androidx.core.app.ActivityCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.androidbyexample.googlemap.ui.theme.GoogleMapTheme import com.google.android.gms.common.GoogleApiAvailability import com.google.android.gms.location.FusedLocationProviderClient import com.google.android.gms.location.LocationCallback import com.google.android.gms.location.LocationRequest import com.google.android.gms.location.LocationResult import com.google.android.gms.location.LocationServices import com.google.android.gms.location.Priority import com.google.android.gms.maps.CameraUpdateFactory import com.google.android.gms.maps.model.BitmapDescriptor 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.DragState 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.rememberCameraPositionState import com.google.maps.android.compose.rememberMarkerState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch class MainActivity : ComponentActivity() { private val viewModel: CarViewModel by viewModels()private lateinit var fusedLocationProviderClient: FusedLocationProviderClient private val locationCallback = object : LocationCallback() { override fun onLocationResult(locationResult: LocationResult) { viewModel.updateLocation(locationResult.lastLocation) } }private val getLocationPermission = registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions() ) { isGranted -> if (isGranted.values.any { it }) { startLocationAndMap() } else { // if the user denied permissions, tell them they // cannot use the app without them. In general, // you should try to just reduce function and let the // user continue, but location is a key part of this // application. // (Note that a real version of this application // might allow the user to manually click on the map // to set their current location, and we wouldn't // show this dialog, or perhaps only show it once) // NOTE: This is a normal Android-View-based dialog, not a compose one! AlertDialog.Builder(this) .setTitle("Permissions Needed") .setMessage( "We need coarse-location or fine-location permission " + "to locate a car (fine location is highly " + "recommended for accurate car locating). " + "Please allow these permissions via App Info " + "settings") .setCancelable(false) .setNegativeButton("Quit") { _, _ -> finish() } .setPositiveButton("App Info") { _, _ -> startActivity( Intent( Settings.ACTION_APPLICATION_DETAILS_SETTINGS ).apply { data = Uri.parse("package:$packageName") } ) finish() } .show() } }override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)GoogleApiAvailability.getInstance() .makeGooglePlayServicesAvailable(this) .addOnSuccessListener {} @SuppressLint("MissingPermission") fun startLocationAndMap() {if (ActivityCompat.checkSelfPermission( this, Manifest.permission.ACCESS_FINE_LOCATION ) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission( this, Manifest.permission.ACCESS_COARSE_LOCATION ) != PackageManager.PERMISSION_GRANTED ) { getLocationPermission.launch( arrayOf( Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION ) ) } else { startLocationAndMap() }}.addOnFailureListener(this) { Toast.makeText( this, "Google Play services required (or upgrade required)", Toast.LENGTH_SHORT ).show() finish() }val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 5000) .setWaitForAccurateLocation(false) .setMinUpdateIntervalMillis(0) .setMaxUpdateDelayMillis(5000) .build() fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this) fusedLocationProviderClient.requestLocationUpdates( locationRequest, locationCallback, Looper.getMainLooper() )setContent { GoogleMapTheme { // A surface container using the 'background' color from the theme Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) {val cameraPositionState = rememberCameraPositionState()val currentLocation by viewModel.currentLocation.collectAsStateWithLifecycle( initialValue = null )val carLatLng by viewModel.carLatLng.collectAsStateWithLifecycle(initialValue = null)GoogleMapDisplay( currentLocation = currentLocation, carLatLng = carLatLng, cameraPositionState = cameraPositionState, onSetCarLocation = viewModel::setCarLocation, onClearCarLocation = viewModel::clearCarLocation, onMoveCar = viewModel::setCarLocation, modifier = Modifier.fillMaxSize(), )} } } } }@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 carState = rememberMarkerState("car")val context = LocalContext.current var currentLocationIcon by remember { mutableStateOf<BitmapDescriptor?>(null) } var carIcon by remember { mutableStateOf<BitmapDescriptor?>(null) } val scope = rememberCoroutineScope()var initialBoundsSet by remember { mutableStateOf(false) } LaunchedEffect(key1 = currentLocation) { if (currentLocation != null) { if (!initialBoundsSet) { initialBoundsSet = true val current = LatLng(currentLocation.latitude, currentLocation.longitude) cameraPositionState.animate( CameraUpdateFactory.newLatLngZoom( current, 16f ), 1000 ) } } }LaunchedEffect(true) { var dragged = false snapshotFlow { carState.dragState } .collect { // 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 (it == DragState.DRAG) { dragged = true } else if (it == DragState.END && dragged) { dragged = 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 = truescope.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,carLatLng?.let { carState.position = it MarkerInfoWindowContent( state = carState,icon = currentLocationIcon, anchor = Offset(0.5f, 0.5f),title = stringResource( id = R.string.current_location ), ) }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() ) } }} ) }