Abstract

This codelab teaches you how to integrate Maps SDK for Android into your app and use its core features.

What you'll build

You will build an application that displays a map of bicycle shops around San Francisco, CA.

What you'll learn

Prerequisites

You'll need to familiarize with the items below to complete this Codelab. If you are already familiar with working with Google Maps Platform, skip ahead to the Codelab!


Required Google Maps Platform Products

In this Codelab, you'll use the following Google Maps Platform products:


We'll cover how to enable this later, in the Getting Set Up section of this Codelab.

Other Requirements for this Codelab

To complete this codelab, you'll need the following accounts, services, and tools:

Getting Started with Google Maps Platform

If you have not used Google Maps Platform before, please follow our Getting Started Guide to learn how to complete the following steps:

Please note that a Google account is required. You will be asked to sign in to an existing Google account or create a new one as part of the sign-up process.

Google Maps Platform Setup

Environment: web

Duration: 15:00

If you do not already have a Google Cloud Platform account with a project created and billing enabled, please see the Getting Started with Google Maps Platform section of this Codelab.

1. Select a Google Cloud Platform Project

In the Google Cloud Platform console, click on the project drop-down menu and select the GCP project you wish to use for this Codelab.


2. Enable the required Google Maps Platform products

To use the Google Maps Platform APIs and SDKs required for this Codelab, you must enable them in the Google Cloud Platform console's Marketplace.

For a video walkthrough of how to enable Google Maps Platform APIs and SDKs, check out this video or our documentation.

3. Generate an API key

An API key must be sent with all requests to Google Maps Platform in order to authenticate and bill your requests. API keys are generated in the Google Cloud Platform console's Credentials page.

For a video walkthrough of how to generate an API key for Google Maps Platform APIs and SDKs, check out this video or our documentation.

To get you started as quickly as possible, here's some starter code to help you follow along with this codelab. You're welcome to jump to the solution, but if you want to follow along with all the steps to build it yourself, keep reading.

You can clone the repository if you have git installed.

git clone https://github.com/googlecodelabs/maps-platform-101-android.git

Alternatively, you can click the button below to download the source code.

Give me the code

Upon getting the code, go ahead and open the project found inside the starter directory on Android Studio.

In this step, you will add Google Maps to so that it loads when you launch the app.

Adding Your API Key

The API key that you created in an earlier step needs to be provided to the application so that the Maps SDK can associate your key with your app. To provide this, first create a new file called secrets.properties in the root directory of your project (i.e. 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.

secrets.properties

GOOGLE_MAPS_API_KEY="YOUR KEY HERE"

Notice that secrets.properties is listed in the .gitignore file in the Git repository. This is because your API key is considered sensitive information should not be checked into source control if possible.

Next, modify your app's build.gradle file located under the app/ directory and add the following lines under android > defaultConfig:

build.gradle

def secretsProperties = new Properties()
if (rootProject.file("secrets.properties").exists()) {
   rootProject.file("secrets.properties")?.withInputStream {
       secretsProperties.load(it)
   }
}
resValue "string", "google_maps_key", (secretsProperties["GOOGLE_MAPS_API_KEY"] ?: "")

These lines will read the values defined within the secrets.properties file that you created and create a new string resource with the ID google_maps_key at build time. This gradle technique allows you to access the API key that you defined while also keeping the secrets.properties file secret.

Adding Google Maps Dependency

Now that your API key can be accessed inside the app, the next step is to add the Google Play Services - Maps 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:

build.gradle

dependencies {
   // Dependency to include Maps SDK for Android
   implementation 'com.google.android.gms:play-services-maps:17.0.0'
}

Next, you will need to 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.

AndroidManifest.xml

<meta-data
   android:name="com.google.android.geo.API_KEY"
   android:value="@string/google_maps_key" />

Next, create a new layout file called activity_main.xml inside the app/src/main/res/layout/ directory and define it as:

activity_main.xml

<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 will use in later steps.

Lastly, update the MainActivity class, located in app/src/main/java/com/google/codelabs/buildyourfirstmap by adding the following to override the onCreate method so you can set its contents with the new layout you just created.

MainActivity

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.

In this task, you'll add markers to the map that represent points of interest that you want to highlight on the map. First, you'll retrieve a list of places that have been provided in the starter project for you, followed by adding those places to the map. In this example, these are bicycle shops.

Get a Reference to GoogleMap

First, you'll need to obtain a reference to the GoogleMap object so that you can use its methods. To do that, add the following code inside your MainActivity.onCreate() method right below the call to setContentView():

MainActivity.onCreate()

val mapFragment = supportFragmentManager.findFragmentById(   
    R.id.map_fragment
) as? SupportMapFragment
mapFragment?.getMapAsync { googleMap ->
    addMarkers(googleMap)
}

The implementation above will first find 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 will be passed. Inside this lambda, the addMarkers() method call is invoked which will be 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 on Android Studio.

PlacesReader

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:

MainActivity.places

private val places: List<Place> by lazy {
   PlacesReader(this).read()
}

This code will invoke 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:

Place

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:

MainActivity.addMarkers()

/**
* 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!

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 will display its title and snippet (if set). You'll 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 on the left hand-side of Android Studio and select New > Layout Resource File.

In the presented window, type "marker_info_contents" in the "File name" field, "LinearLayout" in the "Root element" field and click on OK.

This layout file will later be inflated to represent the contents within the info window. Copy the contents in the code snippet below which adds 3 TextViews within a vertical LinearLayout view group, and overwrite the default code in the file.

marker_info_contents.xml

<?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'll 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 on the left-hand-side in Android Studio followed by selecting New > Kotlin File/Class.

In the presented window, type "MarkerInfoWindowAdapter" and keep the "File" type highlighted.

Once you have the file created, copy the contents in the code snippet in your new file.

MarkerInfoWindowAdapter

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 will return null (you haven't set the tag property on the Marker yet, but will 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'll 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 below the addMarkers() method call inside the getMapAsync() lambda.

MainActivity.onCreate()

// 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:

MainActivity.addMarkers()

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
}

Adding 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'll 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 under app/src/res/drawable which you will be using.

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 will use to accomplish this.

First, you'll 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 will do just that:

BitmapHelper

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/master/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 will use the predefined color colorPrimary in your app and use 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:

MainActivity.addMarkers()

val marker = googleMap.addMarker(
    MarkerOptions()
        .title(place.name)
        .position(place.latLng)
        .icon(bicycleIcon)
)

Run the app to see the updated markers!

https://www.youtube.com/watch?list=PLOU2XLYxmsIKgVkdTcsULfTIIsKz4n-I8&time_continue=79&v=FFm9NmSfjw0&feature=emb_title

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 around one another, 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 will be clustered together such as this:

To implement this, you'll be needing the help of the Maps SDK for Android Utility Library.

Maps SDK for Android Utility

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 & GeoJson support, polyline encoding/decoding, and a handful of helper functions around spherical geometry.

Update your build.gradle

Since the Utility library is packaged separately from the Maps SDK itself, you'll need to add an additional dependency to your build.gradle file. Go ahead and update the dependencies section of your app/build.gradle file.

build.gradle

implementation 'com.google.maps.android:android-maps-utils:1.1.0'

Upon adding this line, you will have to perform a project sync to fetch the new dependencies.

Implement Clustering

To implement clustering on your app, there are 3 steps:

  1. Implement the ClusterItem interface
  2. Subclass the DefaultClusterRenderer class
  3. 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:

Place

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 the three methods:

  1. getPosition() which represents the place's LatLng,
  2. getTitle() which represents the place's name
  3. 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, since markers need to be customized, you will need to extend this class and add the customizations in there.

Go ahead and create the Kotlin file PlaceRenderer.kt inside the package com.google.codelabs.buildyourfirstmap.place and define it as follows:

PlaceRenderer

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 (i.e. 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 (i.e. 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 two functions:

Create a ClusterManager and Add Items

Lastly, to get clustering working you'll need to modify MainActivity to instantiate a ClusterManager and provide the necessary dependencies to it. ClusterManager handles adding the markers (i.e. the ClusterItem objects) internally so instead of adding markers directly on the map, this responsibility will be delegated to ClusterManager.

To start, modify the contents of the lambda inside of the getMapAsync() call in MainActivity.onCreate(). Go ahead and comment out the call to addMarkers() and instead invoke a method called addClusteredMarkers() which you will define next.

MainActivity.onCreate()

mapFragment?.getMapAsync { googleMap ->
    //addMarkers(googleMap)
    addClusteredMarkers(googleMap)

    // Set custom info window adapter
    googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
}

Next, in MainActivity define addClusteredMarkers():

MainActivity.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
       )

   // 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 will instantiate a ClusterManager, pass the custom renderer PlacesRenderer into it, followed by adding all the places and invoking the cluster() method. Lastly, since 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!

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 this 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'll learn how to draw a 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, since 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().

MainActivity.addClusteredMarkers()

// Show polygon
clusterManager.setOnClusterItemClickListener { item ->
   addCircle(googleMap, item)
   return@setOnClusterItemClickListener false
}

This method adds a listener and invokes the method addCircle() which you will define next. Lastly, false is returned from this method to indicate that this method has not consumed this event.

Next, you'll need to define the property circle and the method addCircle() inside of MainActivity.

MainActivity.addCircle()

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.

As your last task, you'll 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 to. 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.

MainActivity.onCreate()

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 since the map properties, such as dimensions, need to be computed first before making a camera update call.

Inside the lambda, a new LatLngBounds object is constructed which defines a rectangular region on the map. This is incrementally built by including all of the place LatLng values into 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 into it via CameraUpdateFactory.newLatLngBounds(bounds.build(), 20).

If you run the app, 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'll modify the code to make the markers translucent whenever the camera is moved. In the addClusteredMarkers() method, go ahead and add the following lines towards the bottom of the method:

MainActivity.addClusteredMarkers()

// 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 will add an OnCameraMoveStartedListener so that whenever the camera starts moving, all of the markers' (both clusters and markers) alpha value is 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:

MainActivity.addClusteredMarkers()

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 re-clustering
   // can be performed when the camera stops moving
   clusterManager.onCameraIdle()
}

Go ahead and run the app to see the results!

Congratulations on making it this far! You've covered a lot of content and hopefully by now you have a better understanding of the core features offered in the Maps SDK for Android. To recap, you now should be able to:

What's next?

While you've covered a lot of ground (pun intended), there's still a lot you can learn on how to use the Maps SDK for Android. Here are some resources you can check out next: