Google Map

Jump to Current Location

Let's jump to the current location when it's first available and use a custom icon instead of the default marker.

First, our custom icon. We need to get a BitmapDescriptor. There's no way to do that yet from Compose, so we'll define a drawable vector image resource for this.

You can create these using File->New->Vector Asset, and select an icon or pull in an SVG. This icon is just a blue circle with a white border. This icon uses a custom color, which must be defined in a resource file, rather than through Compose for now.

We then set up state for the icon and load the icon in a coroutine once the map has loaded.

Note

The factory for the BitmapDescriptor sometimes isn't available until the map has loaded. It's best to wait until the map is successfully loaded before trying to load the icons.

Finally, we set the icon on the Marker.

Note

The anchor parameter defines how the location matches up with the icon. Using an Offset(0.5, 0.5) means the center of the icon is pinned to the location. For the default icons, the bottom center (the pointy tip) marks the location, so we'd use Offset(0.5, 1.0).

Mea culpa: I noticed that I accidentally copied the Offset(0.5, 0.5) when I first created the location marker in a previous step. It was using the default marker, so it should have used Offset(0.5, 1.0).

Running the application now shows

Custom Location Icon

Jumping to the current location on startup is done with a LaunchedEffect keyed off the currentLocation. The first time this LaunchedEffect is run, the currentLocation is null, and it doesn't do anything. When currentLocation changes, the LaunchedEffect is restarted, and, seeing a non-null currentLocation, asks the cameraPositionState to change via a CameraUpdate command. We set initialBoundsSet to true so we won't move again.

Note

I'm using the name initialBoundsSet because later we'll be setting a bounding box based on the current location and set car location.

Code Changes

ADDED: /app/src/main/java/com/androidbyexample/googlemap/DrawableHelpers.kt
package com.androidbyexample.googlemap// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at////     https://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License.import android.content.Contextimport android.content.res.Resourcesimport android.graphics.Bitmapimport android.graphics.Canvasimport android.graphics.drawable.BitmapDrawableimport android.graphics.drawable.Drawableimport androidx.annotation.DrawableResimport androidx.core.content.ContextCompatimport com.google.android.gms.maps.model.BitmapDescriptorimport com.google.android.gms.maps.model.BitmapDescriptorFactory// NOTE: I wanted to show an example of a set of extensions nicely-documented// for KDoc generation using the Dokka tool. There's an example of the setup// in the build scripts. Note that Dokka is still pretty young and doesn't// generate the prettiest documentation... But you can use it to generate// markdown and use another tool to generate something prettier.../** * Returns a [Bitmap] for the intrinsic size of any [Drawable]. If the * drawable is a [BitmapDrawable], we just return it. Otherwise, we create a * new [Bitmap] of the intrinsic size of the [Drawable], draw the [Drawable] * to it, and return the [Bitmap]. * * @sample loadBitmap * @receiver Any [Drawable] * @return a [Bitmap] representation of the [Drawable] */fun Drawable.toBitmap(): Bitmap =    when {        // if it's already a bitmap; just return it        this is BitmapDrawable -> bitmap        // otherwise, create a bitmap and draw the drawable to it        intrinsicWidth == 0 || intrinsicHeight == 0 ->            throw IllegalArgumentException(                "Drawable cannot be converted to a Bitmap; it must have " +                        "non-zero intrinsic width and height")        else ->            Bitmap.createBitmap(                intrinsicWidth,                intrinsicHeight,                Bitmap.Config.ARGB_8888            ).apply {                val canvas = Canvas(this)                setBounds(0, 0, canvas.width, canvas.height)                draw(canvas)            }    }/** * Loads a [Bitmap] by the resource id of a [Drawable]. * * @sample loadBitmapDescriptor * @receiver [Context] to load the [Drawable] * @param id [Int] the resource id of the [Drawable] * @return a [Bitmap] representation of the [Drawable] */fun Context.loadBitmap(@DrawableRes id: Int): Bitmap =    ContextCompat.getDrawable(this, id)?.toBitmap()        ?: throw Resources.NotFoundException(resources.getResourceName(id))/** * Loads a [BitmapDescriptor] by the resource id of a [Drawable]. * * @sample com.javadude.maps.samples.SampleActivity.onCreate * @receiver [Context] to load the [Drawable] * @param id [Int] the resource id of the [Drawable] * @return a [Bitmap] representation of the [Drawable] */@Suppress("KDocUnresolvedReference")// they actually are resolved by dokka 'samples' configfun Context.loadBitmapDescriptor(@DrawableRes id: Int): BitmapDescriptor =    loadBitmap(id).toBitmapDescriptor()/** * Converts a [Bitmap] to a [BitmapDescriptor] * * @sample loadBitmapDescriptor */fun Bitmap.toBitmapDescriptor(): BitmapDescriptor =    BitmapDescriptorFactory.fromBitmap(this)
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.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.LaunchedEffectimport androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScopeimport androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.LocalContextimport 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.CameraUpdateFactoryimport com.google.android.gms.maps.model.BitmapDescriptorimport 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.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
//private val googleHQ = LatLng(37.42423291057923, -122.08811454627153)//private val defaultCameraPosition = CameraPosition.fromLatLngZoom(googleHQ, 11f)import kotlinx.coroutines.Dispatchersimport 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 {
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() }
} @SuppressLint("MissingPermission") fun startLocationAndMap() {
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 {// position = defaultCameraPosition// } val cameraPositionState = rememberCameraPositionState()
val currentLocation by viewModel.currentLocation.collectAsStateWithLifecycle( initialValue = null )
GoogleMapDisplay( currentLocation = currentLocation, // place = googleHQ,// placeDescription = "Google HQ", cameraPositionState = cameraPositionState, modifier = Modifier.fillMaxSize(), )
} } } } }
@Composable fun GoogleMapDisplay( currentLocation: Location?, // 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 currentLocationState = remember(currentLocation) { currentLocation?.let { MarkerState( LatLng( it.latitude, it.longitude ) ) } }
val context = LocalContext.current var currentLocationIcon 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 ) } } }
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
scope.launch(Dispatchers.IO) { currentLocationIcon = context.loadBitmapDescriptor( R.drawable.ic_current_location ) }
},
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 ), ) }
} } }
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/res/drawable/ic_current_location.xml
<?xml version="1.0" encoding="utf-8"?>
<shape android:shape="oval" xmlns:android="http://schemas.android.com/apk/res/android"> <size android:height="24dp" android:width="24dp" /> <stroke android:color="@android:color/white" android:width="3dp"/> <solid android:color="@color/my_location" /></shape>
CHANGED: /app/src/main/res/values/colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="my_location">#FF0000FF</color>
<color name="purple_200">#FFBB86FC</color> <color name="purple_500">#FF6200EE</color> <color name="purple_700">#FF3700B3</color> <color name="teal_200">#FF03DAC5</color> <color name="teal_700">#FF018786</color> <color name="black">#FF000000</color> <color name="white">#FFFFFFFF</color> </resources>