1. Before You Begin
This codelab teaches you how to integrate Maps SDK for Android with your app and use its core features by building an app that displays a map of bicycle shops in San Francisco, CA, USA.
Prerequisites
- Basic knowledge of Kotlin and Android development
What you'll do
- Enable and use the Maps SDK for Android to add Google Maps to an Android app.
- Add, customize, and cluster markers.
- Draw polylines and polygons on the map.
- Control the viewpoint of the camera programmatically.
What you'll need
2. Get set up
For the following enablement step , you need to enable
Maps SDK for Android
.
If you do not already have a Google Cloud Platform account and a project with billing enabled, please see the
Getting Started with Google Maps Platform
guide to create a billing account and a project.
- In the
Cloud Console
, click the project drop-down menu and select the project that you want to use for this codelab.
- Enable the Google Maps Platform APIs and SDKs required for this codelab in the
Google Cloud Marketplace
. To do so, follow the steps in
this video
or
this documentation
.
- Generate an API key in the
Credentials
page of Cloud Console. You can follow the steps in
this video
or
this documentation
. All requests to Google Maps Platform require an API key.
3. Quick start
To get you started as quickly as possible, here's some starter code to help you follow along with this codelab. You're welcomed to jump to the solution, but if you want to follow along with all the steps to build it yourself, keep reading.
- Clone the repository if you have
git
installed.
git clone https://github.com/googlecodelabs/maps-platform-101-android.git
Alternatively, you can click the following button to download the source code.
- Upon getting the code, go ahead and open the project found inside the
starter
directory in Android Studio.
4. Add Google Maps
In this section, you will add Google Maps so that it loads when you launch the app.
Add your API key
The API key that you created in an earlier step needs to be provided to the app so that Maps SDK for Android can associate your key with your app.
- To provide this, open the file called
local.properties
in the root directory of your project (the same level where
gradle.properties
and
settings.gradle
are).
- In that file, define a new key
GOOGLE_MAPS_API_KEY
with its value being the API key that you created.
local.properties
GOOGLE_MAPS_API_KEY=YOUR_KEY_HERE
Notice that
local.properties
is listed in the
.gitignore
file in the Git repository. This is because your API key is considered sensitive information and should not be checked in to source control, if possible.
- Next, to expose your API so it can be used throughout your app, include the
Secrets Gradle Plugin for Android
plugin in your app's
build.gradle
file located in the
app/
directory and add the following line within the
plugins
block:
plugins {
// ...
id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
}
You will also need to modify your project-level
build.gradle
file to include the following classpath:
buildscript {
dependencies {
// ...
classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:1.3.0"
}
}
This plugin will make keys you have defined within your
local.properties
file available as build variables in the Android manifest file and as variables in the Gradle-generated
BuildConfig
class at build time. Using this plugin removes the boilerplate code that would otherwise be needed to read properties from
local.properties
so that it can be accessed throughout your app.
Add Google Maps dependency
- Now that your API key can be accessed inside the app, the next step is to add the Maps SDK for Android dependency to your app's
build.gradle
file.
In the starter project that comes with this codelab, this dependency has already been added for you.
dependencies {
// Dependency to include Maps SDK for Android
implementation 'com.google.android.gms:play-services-maps:17.0.0'
}
- Next, add a new
meta-data
tag in
AndroidManifest.xml
to pass in the API key that you created in an earlier step. To do so, go ahead and open this file in Android Studio and add the following
meta-data
tag inside the
application
object in your
AndroidManifest.xml
file, located in
app/src/main
.
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${GOOGLE_MAPS_API_KEY}" />
- Next, create a new layout file called
activity_main.xml
in the
app/src/main/res/layout/
directory and define it as follows:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<fragment
class="com.google.android.gms.maps.SupportMapFragment"
android:id="@+id/map_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
This layout has a single
FrameLayout
containing a
SupportMapFragment
. This fragment contains the underlying
GoogleMaps
object that you use in later steps.
- Lastly, update the
MainActivity
class located in
app/src/main/java/com/google/codelabs/buildyourfirstmap
by adding the following code to override the
onCreate
method so you can set its contents with the new layout you just created.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
- Now go ahead and run the app. You should now see the map load on your device's screen.
5. Cloud-based map styling (Optional)
You can customize the style of your map using
Cloud-based map styling
.
Create a Map ID
If you have not yet created a map ID with a map style associated to it, see the
Map IDs
guide to complete the following steps:
- Create a map ID.
- Associate a map ID to a map style.
Adding the Map ID to your app
To use the map ID you created, modify the
activity_main.xml
file and pass your map ID in the
map:mapId
attribute of the
SupportMapFragment
.
<fragment xmlns:map="http://schemas.android.com/apk/res-auto"
class="com.google.android.gms.maps.SupportMapFragment"
<!-- ... -->
map:mapId="YOUR_MAP_ID" />
Once you've completed this, go ahead and run the app to see your map in the style that you selected!
6. Add markers
In this task, you add markers to the map that represent points of interest that you want to highlight on the map. First, you retrieve a list of places that have been provided in the starter project for you, then add those places to the map. In this example, these are bicycle shops.
Get a reference to GoogleMap
First, you need to obtain a reference to the
GoogleMap
object so that you can use its methods. To do that, add the following code in your
MainActivity.onCreate()
method right after the call to
setContentView()
:
val mapFragment = supportFragmentManager.findFragmentById(
R.id.map_fragment
) as? SupportMapFragment
mapFragment?.getMapAsync { googleMap ->
addMarkers(googleMap)
}
The implementation first finds the
SupportMapFragment
that you added in the previous step by using the
findFragmentById()
method on the
SupportFragmentManager
object. Once a reference has been obtained, the
getMapAsync()
call is invoked followed by passing in a lambda. This lambda is where the
GoogleMap
object is passed. Inside this lambda, the
addMarkers()
method call is invoked, which is defined shortly.
Provided class: PlacesReader
In the starter project, the class
PlacesReader
has been provided for you. This class reads a list of 49 places that are stored in a JSON file called
places.json
and returns these as a
List<Place>
. The places themselves represent a list of bicycle shops around San Francisco, CA, USA.
If you are curious about the implementation of this class, you can access it on GitHub or open the
PlacesReader
class in Android Studio.
package com.google.codelabs.buildyourfirstmap.place
import android.content.Context
import com.google.codelabs.buildyourfirstmap.R
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import java.io.InputStream
import java.io.InputStreamReader
/**
* Reads a list of place JSON objects from the file places.json
*/
class PlacesReader(private val context: Context) {
// GSON object responsible for converting from JSON to a Place object
private val gson = Gson()
// InputStream representing places.json
private val inputStream: InputStream
get() = context.resources.openRawResource(R.raw.places)
/**
* Reads the list of place JSON objects in the file places.json
* and returns a list of Place objects
*/
fun read(): List<Place> {
val itemType = object : TypeToken<List<PlaceResponse>>() {}.type
val reader = InputStreamReader(inputStream)
return gson.fromJson<List<PlaceResponse>>(reader, itemType).map {
it.toPlace()
}
}
Load places
To load the list of bicycle shops, add a property in
MainActivity
called
places
and define it as follows:
private val places: List<Place> by lazy {
PlacesReader(this).read()
}
This code invokes the
read()
method on a
PlacesReader
, which returns a
List<Place>
. A
Place
has a property called
name
, the name of the place, and a
latLng
?the coordinates where the place is located.
data class Place(
val name: String,
val latLng: LatLng,
val address: LatLng,
val rating: Float
)
Add markers to map
Now that the list of places have been loaded to memory, the next step is to represent these places on the map.
- Create a method in
MainActivity
called
addMarkers()
and define it as follows:
/**
* Adds marker representations of the places list on the provided GoogleMap object
*/
private fun addMarkers(googleMap: GoogleMap) {
places.forEach { place ->
val marker = googleMap.addMarker(
MarkerOptions()
.title(place.name)
.position(place.latLng)
)
}
}
This method iterates through the list of
places
followed by invoking the
addMarker()
method on the provided
GoogleMap
object. The marker is created by instantiating a
MarkerOptions
object, which allows you to customize the marker itself. In this case, the title and position of the marker is provided, which represents the bicycle shop name and its coordinates, respectively.
- Go ahead and run the app, and head over to San Francisco to see the markers that you just added!
7. Customize markers
There are several customization options for markers you have just added to help them stand out and convey useful information to users. In this task, you'll explore some of those by customizing the image of each marker as well as the information window displayed when a marker is tapped.
Adding an info window
By default, the info window when you tap on a marker displays its title and snippet (if set). You customize this so that it can display additional information, such as the place's address and rating.
Create marker_info_contents.xml
First, create a new layout file called
marker_info_contents.xml
.
- To do so, right click on the
app/src/main/res/layout
folder in the project view in Android Studio and select
New
>
Layout Resource File
.
- In the dialog, type
marker_info_contents
in the
File name
field and
LinearLayout
in the
Root element
field, then click
OK
.
This layout file is later inflated to represent the contents within the info window.
- Copy the contents in the following code snippet, which adds three
TextViews
within a vertical
LinearLayout
view group, and overwrite the default code in the file.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:padding="8dp">
<TextView
android:id="@+id/text_view_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/black"
android:textSize="18sp"
android:textStyle="bold"
tools:text="Title"/>
<TextView
android:id="@+id/text_view_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/black"
android:textSize="16sp"
tools:text="123 Main Street"/>
<TextView
android:id="@+id/text_view_rating"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/black"
android:textSize="16sp"
tools:text="Rating: 3"/>
</LinearLayout>
Create an implementation of an InfoWindowAdapter
After creating the layout file for the custom info window, the next step is to implement the
GoogleMap.InfoWindowAdapter
interface. This interface contains two methods,
getInfoWindow()
and
getInfoContents()
. Both methods return an optional
View
object wherein the former is used to customize the window itself, while the latter is to customize its contents. In your case, you implement both and customize the return of
getInfoContents()
while returning null in
getInfoWindow()
, which indicates that the default window should be used.
- Create a new Kotlin file called
MarkerInfoWindowAdapter
in the same package as
MainActivity
by right-clicking the
app/src/main/java/com/google/codelabs/buildyourfirstmap
folder in the project view in Android Studio, then select
New
>
Kotlin File/Class
.
- In the dialog, type
MarkerInfoWindowAdapter
and keep
File
highlighted.
- Once you have the file created, copy the contents in the following code snippet in to your new file.
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.model.Marker
import com.google.codelabs.buildyourfirstmap.place.Place
class MarkerInfoWindowAdapter(
private val context: Context
) : GoogleMap.InfoWindowAdapter {
override fun getInfoContents(marker: Marker?): View? {
// 1. Get tag
val place = marker?.tag as? Place ?: return null
// 2. Inflate view and set title, address, and rating
val view = LayoutInflater.from(context).inflate(
R.layout.marker_info_contents, null
)
view.findViewById<TextView>(
R.id.text_view_title
).text = place.name
view.findViewById<TextView>(
R.id.text_view_address
).text = place.address
view.findViewById<TextView>(
R.id.text_view_rating
).text = "Rating: %.2f".format(place.rating)
return view
}
override fun getInfoWindow(marker: Marker?): View? {
// Return null to indicate that the
// default window (white bubble) should be used
return null
}
}
In the contents of the
getInfoContents()
method, the provided Marker in the method is casted to a
Place
type, and if casting is not possible, the method returns null (you haven't set the tag property on the
Marker
yet, but you do that in the next step).
Next, the layout
marker_info_contents.xml
is inflated followed by setting the text on containing
TextViews
to the
Place
tag.
Update MainActivity
To glue all the components you have created so far, you need to add two lines in your
MainActivity
class.
First, to pass the custom
InfoWindowAdapter
,
MarkerInfoWindowAdapter
, inside the
getMapAsync
method call, invoke the
setInfoWindowAdapter()
method on the
GoogleMap
object and create a new instance of
MarkerInfoWindowAdapter
.
- Do this by adding the following code after the
addMarkers()
method call inside the
getMapAsync()
lambda.
// Set custom info window adapter
googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
Lastly, you'll need to set each Place as the tag property on every Marker that's added to the map.
- To do that, modify the
places.forEach{}
call in the
addMarkers()
function with the following:
places.forEach { place ->
val marker = googleMap.addMarker(
MarkerOptions()
.title(place.name)
.position(place.latLng)
.icon(bicycleIcon)
)
// Set place as the tag on the marker object so it can be referenced within
// MarkerInfoWindowAdapter
marker.tag = place
}
Add a custom marker image
Customizing the marker image is one of the fun ways to communicate the type of place the marker represents on your map. For this step, you display bicycles instead of the default red markers to represent each shop on the map. The starter project includes the bicycle icon
ic_directions_bike_black_24dp.xml
in
app/src/res/drawable
, which you use.
Set custom bitmap on marker
With the vector drawable bicycle icon at your disposal, the next step is to set that drawable as each markers' icon on the map.
MarkerOptions
has a method
icon
, which takes in a
BitmapDescriptor
that you use to accomplish this.
First, you need to convert the vector drawable you just added into a
BitmapDescriptor
. A file called
BitMapHelper
included in the starter project contains a helper function called
vectorToBitmap()
, which does just that.
package com.google.codelabs.buildyourfirstmap
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.util.Log
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.DrawableCompat
import com.google.android.gms.maps.model.BitmapDescriptor
import com.google.android.gms.maps.model.BitmapDescriptorFactory
object BitmapHelper {
/**
* Demonstrates converting a [Drawable] to a [BitmapDescriptor],
* for use as a marker icon. Taken from ApiDemos on GitHub:
* https://github.com/googlemaps/android-samples/blob/main/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/MarkerDemoActivity.kt
*/
fun vectorToBitmap(
context: Context,
@DrawableRes id: Int,
@ColorInt color: Int
): BitmapDescriptor {
val vectorDrawable = ResourcesCompat.getDrawable(context.resources, id, null)
if (vectorDrawable == null) {
Log.e("BitmapHelper", "Resource not found")
return BitmapDescriptorFactory.defaultMarker()
}
val bitmap = Bitmap.createBitmap(
vectorDrawable.intrinsicWidth,
vectorDrawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
vectorDrawable.setBounds(0, 0, canvas.width, canvas.height)
DrawableCompat.setTint(vectorDrawable, color)
vectorDrawable.draw(canvas)
return BitmapDescriptorFactory.fromBitmap(bitmap)
}
}
This method takes in a
Context
, a drawable resource ID, as well as a color integer, and creates a
BitmapDescriptor
representation of it.
Using the helper method, declare a new property called
bicycleIcon
and give it the following definition:
MainActivity.bicycleIcon
private val bicycleIcon: BitmapDescriptor by lazy {
val color = ContextCompat.getColor(this, R.color.colorPrimary)
BitmapHelper.vectorToBitmap(this, R.drawable.ic_directions_bike_black_24dp, color)
}
This property uses the predefined color
colorPrimary
in your app, and uses that to tint the bicycle icon and return it as a
BitmapDescriptor
.
- Using this property, go ahead and invoke the
icon
method of
MarkerOptions
in the
addMarkers()
method to complete your icon customization. Doing this, the marker property should look like this:
val marker = googleMap.addMarker(
MarkerOptions()
.title(place.name)
.position(place.latLng)
.icon(bicycleIcon)
)
- Run the app to see the updated markers!
8. Cluster markers
Depending on how far you zoom into the map, you may have noticed that the markers you added overlap. Overlapping markers are very hard to interact with and create a lot of noise, which affects the usability of your app.
To improve the user experience for this, whenever you have a large dataset that is clustered closely, it's best practice to implement marker clustering. With clustering, as you zoom in and out of the map, markers that are in close proximity are clustered together like this:
To implement this, you need the help of the
Maps SDK for Android Utility Library
.
Maps SDK for Android Utility Library
The Maps SDK for Android Utility Library was created as a way to extend the functionality of the Maps SDK for Android. It offers advanced features, such as marker clustering, heatmaps, KML and GeoJson support, polyline encoding anddecoding, and a handful of helper functions around spherical geometry.
Update your build.gradle
Because the utility library is packaged separately from Maps SDK for Android, you need to add an additional dependency to your
build.gradle
file.
- Go ahead and update the
dependencies
section of your
app/build.gradle
file.
implementation 'com.google.maps.android:android-maps-utils:1.1.0'
- Upon adding this line, you have to perform a project sync to fetch the new dependencies.
Implement clustering
To implement clustering on your app, follow these three steps:
- Implement the
ClusterItem
interface.
- Subclass the
DefaultClusterRenderer
class.
- Create a
ClusterManager
and add items.
Implement the ClusterItem interface
All objects that represent a clusterable marker on the map need to implement the
ClusterItem
interface. In your case, that means that the
Place
model needs to conform to
ClusterItem
. Go ahead and open the
Place.kt
file and make the following modifications to it:
data class Place(
val name: String,
val latLng: LatLng,
val address: String,
val rating: Float
) : ClusterItem {
override fun getPosition(): LatLng =
latLng
override fun getTitle(): String =
name
override fun getSnippet(): String =
address
}
The ClusterItem defines these three methods:
getPosition()
, which represents the place's
LatLng
.
getTitle()
, which represents the place's name
getSnippet()
, which represents the place's address.
Subclass the DefaultClusterRenderer class
The class in charge of implementing clustering,
ClusterManager
, internally uses a
ClusterRenderer
class to handle creating the clusters as you pan and zoom around the map. By default, it comes with the default renderer,
DefaultClusterRenderer
, which implements
ClusterRenderer
. For simple cases, this should suffice. In your case, however, because markers need to be customized, you need to extend this class and add the customizations in there.
Go ahead and create the Kotlin file
PlaceRenderer.kt
in the package
com.google.codelabs.buildyourfirstmap.place
and define it as follows:
package com.google.codelabs.buildyourfirstmap.place
import android.content.Context
import androidx.core.content.ContextCompat
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.model.BitmapDescriptor
import com.google.android.gms.maps.model.Marker
import com.google.android.gms.maps.model.MarkerOptions
import com.google.codelabs.buildyourfirstmap.BitmapHelper
import com.google.codelabs.buildyourfirstmap.R
import com.google.maps.android.clustering.ClusterManager
import com.google.maps.android.clustering.view.DefaultClusterRenderer
/**
* A custom cluster renderer for Place objects.
*/
class PlaceRenderer(
private val context: Context,
map: GoogleMap,
clusterManager: ClusterManager<Place>
) : DefaultClusterRenderer<Place>(context, map, clusterManager) {
/**
* The icon to use for each cluster item
*/
private val bicycleIcon: BitmapDescriptor by lazy {
val color = ContextCompat.getColor(context,
R.color.colorPrimary
)
BitmapHelper.vectorToBitmap(
context,
R.drawable.ic_directions_bike_black_24dp,
color
)
}
/**
* Method called before the cluster item (the marker) is rendered.
* This is where marker options should be set.
*/
override fun onBeforeClusterItemRendered(
item: Place,
markerOptions: MarkerOptions
) {
markerOptions.title(item.name)
.position(item.latLng)
.icon(bicycleIcon)
}
/**
* Method called right after the cluster item (the marker) is rendered.
* This is where properties for the Marker object should be set.
*/
override fun onClusterItemRendered(clusterItem: Place, marker: Marker) {
marker.tag = clusterItem
}
}
This class overrides these two functions:
onBeforeClusterItemRendered()
, which is called before the cluster is rendered on the map. Here, you can provide customizations through
MarkerOptions
?in this case, it sets the marker's title, position, and icon.
onClusterItemRenderer()
, which is called right after the marker is rendered on the map. This is where you can access the created
Marker
object?in this case, it sets the marker's tag property.
Create a ClusterManager and add items
Lastly, to get clustering working, you need to modify
MainActivity
to instantiate a
ClusterManager
and provide the necessary dependencies to it.
ClusterManager
handles adding the markers (the
ClusterItem
objects) internally, so instead of adding markers directly on the map, this responsibility is delegated to
ClusterManager
. Additionally,
ClusterManager
also calls
setInfoWindowAdapter()
internally so setting a custom info window will have to be done on
ClusterManger
's
MarkerManager.Collection
object.
- To start, modify the contents of the lambda in the
getMapAsync()
call in
MainActivity.onCreate()
. Go ahead and comment out the call to
addMarkers()
and
setInfoWindowAdapter()
, and instead invoke a method called
addClusteredMarkers()
, which you define next.
mapFragment?.getMapAsync { googleMap ->
//addMarkers(googleMap)
addClusteredMarkers(googleMap)
// Set custom info window adapter.
// googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
}
- Next, in
MainActivity
, define
addClusteredMarkers()
.
/**
* Adds markers to the map with clustering support.
*/
private fun addClusteredMarkers(googleMap: GoogleMap) {
// Create the ClusterManager class and set the custom renderer.
val clusterManager = ClusterManager<Place>(this, googleMap)
clusterManager.renderer =
PlaceRenderer(
this,
googleMap,
clusterManager
)
// Set custom info window adapter
clusterManager.markerCollection.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
// Add the places to the ClusterManager.
clusterManager.addItems(places)
clusterManager.cluster()
// Set ClusterManager as the OnCameraIdleListener so that it
// can re-cluster when zooming in and out.
googleMap.setOnCameraIdleListener {
clusterManager.onCameraIdle()
}
}
This method instantiates a
ClusterManager
, passes the custom renderer
PlacesRenderer
to it, adds all the places, and invokes the
cluster()
method. Also, since
ClusterManager
uses the
setInfoWindowAdapter()
method on the map object, setting the custom info window will have to be done on
ClusterManager.markerCollection
object. Lastly, because you want clustering to change as the user pans and zooms around the map, an
OnCameraIdleListener
is provided to
googleMap
, such that when the camera goes idle,
clusterManager.onCameraIdle()
is invoked.
- Go ahead and run the app to see the new clustered shops!
9. Draw on the map
While you have already explored one way to draw on the map (by adding markers), the Maps SDK for Android supports numerous other ways you can draw to display useful information on the map.
For example, if you wanted to represent routes and areas on the map, you can use
polylines and polygons
to display these on the map. Or, if you wanted to fix an image to the ground's surface, you can use
ground overlays
.
In this task, you learn how to draw shapes, specifically a circle, around a marker whenever it is tapped.
Add click listener
Typically, the way you would add a click listener to a marker is by passing in a click listener directly on the
GoogleMap
object via
setOnMarkerClickListener()
. However, because you're using clustering, the click listener needs to be provided to
ClusterManager
instead.
- In the
addClusteredMarkers()
method in
MainActivity
, go ahead and add the following line right after the invocation to
cluster()
.
// Show polygon
clusterManager.setOnClusterItemClickListener { item ->
addCircle(googleMap, item)
return@setOnClusterItemClickListener false
}
This method adds a listener and invokes the method
addCircle()
, which you define next. Lastly,
false
is returned from this method to indicate that this method has not consumed this event.
- Next, you need to define the property
circle
and the method
addCircle()
in
MainActivity
.
private var circle: Circle? = null
/**
* Adds a [Circle] around the provided [item]
*/
private fun addCircle(googleMap: GoogleMap, item: Place) {
circle?.remove()
circle = googleMap.addCircle(
CircleOptions()
.center(item.latLng)
.radius(1000.0)
.fillColor(ContextCompat.getColor(this, R.color.colorPrimaryTranslucent))
.strokeColor(ContextCompat.getColor(this, R.color.colorPrimary))
)
}
The
circle
property is set so that whenever a new marker is tapped, the previous circle is removed and a new one is added. Notice that the API for adding a circle is quite similar to adding a marker.
- Go ahead now and run the app to see the changes.
10. Camera Control
As your last task, you look at some
camera controls
so that you can focus the view around a certain region.
Camera and view
If you noticed when you run the app, the camera displays the continent of Africa, and you have to painstakingly pan and zoom to San Francisco to find the markers you added. While it can be a fun way to explore the world, it's not useful if you want to display the markers right away.
To help with that, you can set the camera's position programmatically so that the view is centered where you want it.
- Go ahead and add the following code to the
getMapAsync()
call to adjust the camera view so that it is initialized to San Francisco when the app is launched.
mapFragment?.getMapAsync { googleMap ->
// Ensure all places are visible in the map.
googleMap.setOnMapLoadedCallback {
val bounds = LatLngBounds.builder()
places.forEach { bounds.include(it.latLng) }
googleMap.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds.build(), 20))
}
}
First, the
setOnMapLoadedCallback()
is called so that the camera update is only performed after the map is loaded. This step is necessary because the map properties, such as dimensions, need to be computed before making a camera update call.
In the lambda, a new
LatLngBounds
object is constructed, which defines a rectangular region on the map. This is incrementally built by including all the place
LatLng
values in it to ensure all places are inside the bounds. Once this object has been built, the
moveCamera()
method on
GoogleMap
is invoked and a
CameraUpdate
is provided to it through
CameraUpdateFactory.newLatLngBounds(bounds.build(), 20)
.
- Run the app and notice that the camera is now initialized in San Francisco.
Listening to camera changes
In addition to modifying the camera position, you can also listen to camera updates as the user moves around the map. This could be useful if you wanted to modify the UI as the camera moves around.
Just for fun, you modify the code to make the markers translucent whenever the camera is moved.
- In the
addClusteredMarkers()
method, go ahead and add the following lines toward the bottom of the method:
// When the camera starts moving, change the alpha value of the marker to translucent.
googleMap.setOnCameraMoveStartedListener {
clusterManager.markerCollection.markers.forEach { it.alpha = 0.3f }
clusterManager.clusterMarkerCollection.markers.forEach { it.alpha = 0.3f }
}
This adds an
OnCameraMoveStartedListener
so that, whenever the camera starts moving, all the markers' (both clusters and markers) alpha values are modified to
0.3f
so that the markers appear translucent.
- Lastly, to modify the translucent markers back to opaque when the camera stops, modify the contents of the
setOnCameraIdleListener
in the
addClusteredMarkers()
method to the following:
googleMap.setOnCameraIdleListener {
// When the camera stops moving, change the alpha value back to opaque.
clusterManager.markerCollection.markers.forEach { it.alpha = 1.0f }
clusterManager.clusterMarkerCollection.markers.forEach { it.alpha = 1.0f }
// Call clusterManager.onCameraIdle() when the camera stops moving so that reclustering
// can be performed when the camera stops moving.
clusterManager.onCameraIdle()
}
- Go ahead and run the app to see the results!
11. Maps KTX
For Kotlin apps using one or more Google Maps Platform Android SDKs, Kotlin extension or KTX libraries are available to enable you to take advantage of Kotlin language features such as coroutines, extension properties/functions, and more. Each Google Maps SDK has a corresponding KTX library as shown below:
In this task, you will use the Maps KTX and Maps Utils KTX libraries to your app and refactor previous tasks' implementations so that you can use Kotlin-specific language features in your app.
- Include KTX dependencies in your app-level build.gradle file
Since the app uses both the Maps SDK for Android and the Maps SDK for Android Utility Library, you will need to include the corresponding KTX libraries for these libraries. You will also be using a feature found in the AndroidX Lifecycle KTX library in this task so include that dependency as well in your app-level
build.gradle
file as well.
dependencies {
// ...
// Maps SDK for Android KTX Library
implementation 'com.google.maps.android:maps-ktx:3.0.0'
// Maps SDK for Android Utility Library KTX Library
implementation 'com.google.maps.android:maps-utils-ktx:3.0.0'
// Lifecycle Runtime KTX Library
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
}
- Use GoogleMap.addMarker() and GoogleMap.addCircle() extension functions
The Maps KTX library provides a DSL-style API alternative for the
GoogleMap.addMarker(MarkerOptions)
and
GoogleMap.addCircle(CircleOptions)
used in previous steps. To use the aforementioned APIs, construction of a class containing options for a marker or circle is necessary whereas with the KTX alternatives you are able to set the marker or circle options in the lambda you provide.
To use these APIs, update the
MainActivity.addMarkers(GoogleMap)
and
MainActivity.addCircle(GoogleMap)
methods:
/**
* Adds markers to the map. These markers won't be clustered.
*/
private fun addMarkers(googleMap: GoogleMap) {
places.forEach { place ->
val marker = googleMap.addMarker {
title(place.name)
position(place.latLng)
icon(bicycleIcon)
}
// Set place as the tag on the marker object so it can be referenced within
// MarkerInfoWindowAdapter
marker.tag = place
}
}
/**
* Adds a [Circle] around the provided [item]
*/
private fun addCircle(googleMap: GoogleMap, item: Place) {
circle?.remove()
circle = googleMap.addCircle {
center(item.latLng)
radius(1000.0)
fillColor(ContextCompat.getColor(this@MainActivity, R.color.colorPrimaryTranslucent))
strokeColor(ContextCompat.getColor(this@MainActivity, R.color.colorPrimary))
}
}
Rewriting the above methods in this way is a lot more concise to read which is made possible using Kotlin's
function literal with receiver
.
- Use SupportMapFragment.awaitMap() and GoogleMap.awaitMapLoad() extension suspending functions
The Maps KTX library also provides suspending function extensions to be used within coroutines. Specifically, there are suspending function alternatives for
SupportMapFragment.getMapAsync(OnMapReadyCallback)
and
GoogleMap.setOnMapLoadedCallback(OnMapLoadedCallback)
. Using these alternative APIs removes the need for passing callbacks and instead allows you to receive the response of these methods in a serial and synchronous way.
Since these methods are suspending functions, their usage will need to occur within a coroutine. The
Lifecycle Runtime KTX
library offers an extension to provide lifecycle-aware coroutine scopes so that coroutines are run and stopped at the appropriate lifecycle event.
Combining these concepts, update the
MainActivity.onCreate(Bundle)
method:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val mapFragment =
supportFragmentManager.findFragmentById(R.id.map_fragment) as SupportMapFragment
lifecycleScope.launchWhenCreated {
// Get map
val googleMap = mapFragment.awaitMap()
// Wait for map to finish loading
googleMap.awaitMapLoad()
// Ensure all places are visible in the map
val bounds = LatLngBounds.builder()
places.forEach { bounds.include(it.latLng) }
googleMap.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds.build(), 20))
addClusteredMarkers(googleMap)
}
}
The
lifecycleScope.launchWhenCreated
coroutine scope will execute the block when the activity is at least in the created state. Also notice that the calls to retrieve the
GoogleMap
object, and to wait for the map to finish loading, have been replaced with
SupportMapFragment.awaitMap()
and
GoogleMap.awaitMapLoad()
, respectively. Refactoring code using these suspending functions enable you to write the equivalent callback-based code in a sequential manner.
- Go ahead and re-build the app with your refactored changes!
12. Congratulations
Congratulations! You covered a lot of content and hopefully you have a better understanding of the core features offered in the Maps SDK for Android.
Learn more
- Places SDK for Android
?explore the rich set of places data to discover businesses around you.
- android-maps-ktx
?an open source library allowing you to integrate with Maps SDK for Android and the Maps SDK for Android Utility Library in a Kotlin-friendly way.
- android-place-ktx
?an open source library allowing you to integrate with Places SDK for Android in a Kotlin-friendly way.
- android-samples
?sample code on GitHub demonstrating all the features covered in this codelab and more.
- More Kotlin codelabs
for building Android apps with Google Maps Platform