When Google introduced a map service many years ago, it is hard to say whether or not they ever anticipated having a version available for integration into mobile applications. When the first web-based version of what would eventually be called Google Maps was introduced in 2005, the iPhone had yet to ignite the smartphone revolution, and Google would not acquire the company that was developing the Android operating system for another six months. Whatever aspirations Google had for the future of Google Maps, it is remarkable to consider that all of the power of Google Maps can now be accessed directly via Android applications using the Google Maps Android API.
This chapter is intended to provide an overview of the Google Maps system and Google Maps Android API. The chapter will provide an overview of the different elements that make up the API, detail the steps necessary to configure a development environment to work with Google Maps, and then work through some code examples demonstrating some of the basics of Google Maps Android integration.
The Elements of the Google Maps Android API
The Google Maps Android API consists of a core set of classes that combine to provide mapping capabilities in Android applications. The key elements of a map are as follows:
- GoogleMap – The main class of the Google Maps Android API. This class is responsible for downloading and displaying map tiles and for displaying and responding to map controls. The GoogleMap object is not created directly by the application but is created when MapView or MapFragment instances are created. A reference to the GoogleMap object can be obtained within application code via a call to the getMap() method of a MapView, MapFragment, or SupportMapFragment instance.
- MapView – A subclass of the View class, this class provides the view canvas onto which the map is drawn by the GoogleMap object, allowing a map to be placed in the user interface layout of an activity.
- SupportMapFragment – A subclass of the Fragment class, this class allows a map to be placed within a Fragment in an Android layout.
- Marker – The purpose of the Marker class is to allow locations to be marked on a map. Markers are added to a map by obtaining a reference to the GoogleMap object associated with a map and then making a call to the addMarker() method of that object instance. The position of a marker is defined via Longitude and Latitude. Markers can be configured in various ways, including specifying a title, text, and an icon. Markers may also be “draggable” allowing the user to move the marker to different positions on a map.
- Shapes – Drawing lines and shapes on a map is achieved using the Polyline, Polygon, and Circle classes.
- UiSettings – The UiSettings class customizes which controls appear on a map. Using UiSettings, for example, the application can control whether or not the zoom, current location, and compass controls appear on a map. This class can also configure which touchscreen gestures are recognized by the map.
- My Location Layer – When enabled, the My Location Layer displays a button on the map which, when selected by the user, centers the map on the user’s current geographical location. If the user is stationary, a blue marker represents this location on the map. If the user is in motion, the location is represented by a chevron indicating the user’s direction of travel.
The best way to gain familiarity with the Google Maps Android API is to work through an example. The remainder of this chapter will create a Google Maps-based application while highlighting the key areas of the API.
Creating the Google Maps Project
Select the New Project option from the welcome screen and choose the No Activity template within the resulting new project dialog before clicking on the Next button.
Enter MapDemo into the Name field and specify com.ebookfrenzy.mapdemo as the package name. Before clicking on the Finish button, change the Minimum API level setting to API 26: Android 8.0 (Oreo) and the Language menu to Kotlin.
Next, right-click on the app -> java -> com.ebookfrenzy.mapdemo entry in the Project tool window and select the New -> Google -> Google Maps Views Activity menu option. Finally, enable the Launcher Activity checkbox in the New Android Activity dialog before clicking the Finish button:
Creating a Google Cloud Billing Account
Before using the Google Map APIs, you must create a Google Cloud billing account (if you already have one, you can skip to the next section). To do this, open a browser and use the following link to navigate to the Google Cloud Console:
https://console.cloud.google.com/
Next, click on the menu button in the top left-hand corner of the console page and select the Billing entry as illustrated in Figure 79-2 below:
On the Billing page, select the option to add a new billing account and then follow the steps to start a free trial. You must provide a credit card to open the account, but Google won’t charge you when the free trial ends without your consent.
Creating a New Google Cloud Project
The next step is to create a Google Cloud project to be associated with the MapDemo app. To do this, return to the Google Cloud Console dashboard by using the following URL:
https://console.cloud.google.com/home/dashboard
Within the dashboard, click the Select a project button located in the top toolbar:
When the project selection dialog appears, click on the New Project button (highlighted in Figure 79-4):
When the new project screen appears, provide a name for the project. The console will display a default id for the project beneath the project name field. If you don’t like the default id, click the Edit button to change it:
Click the Create button, and after a brief pause, you will be returned to the dashboard where your new project will be listed.
Enabling the Google Maps SDK
Now that we have created a new Google Cloud project, the next step is to allow the project to use the Google Maps SDK. To enable Google Maps support, select your project in the Google Cloud Console, click the menu button in the top left-hand corner, and select the Google Maps Platform entry. Then, from the resulting menu, select the APIs option as shown in Figure 79-6:
On the APIs screen, click on the Maps SDK for Android option and, on the resulting screen, click the Enable button:
Repeat the above steps to enable the Geocoding API credential, which will be needed later in the chapter to allow our app to display the user’s current location.
Once you have enabled the credentials for your project, click the back arrow to return to the product details page in preparation for the next step.
Generating a Google Maps API Key
Before an application can use the Google Maps Android SDK, it must be configured with an API key to associate it with a Maps-enabled Google Cloud project. To generate an API key, select the Credentials menu option (marked A in Figure 79-8) followed by Create Credentials button (B):
After the credential is created, a dialog displaying the API key will appear:
Adding the API Key to the Android Studio Project
Now that we have generated an API key allowing our app to use the Google Maps SDK, we must add it to our project. Return to Android Studio, edit the manifests -> AndroidManifest.xml file, and locate the API key entry, which will read as follows:
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="YOUR_API_KEY" />
Code language: Kotlin (kotlin)
Delete the text “YOUR_API_KEY” and replace it with the API key created in the Google Play Console.
Next, edit the Gradle Scripts -> local.properties file and add a new line that reads as follows (where the API key for your project replaces YOUR_API_KEY):
MAPS_API_KEY=YOUR_API_KEY
Code language: HTML, XML (xml)
Testing the Application
Perform a test run of the application to verify that the API key is correctly configured. The application will run and display a map on the screen if the configuration is correct.
If a map is not displayed, check the following areas:
- If the application is running on an emulator, make sure that the emulator is running a version of Android that includes the Google APIs. The current operating system can be changed for an AVD configuration by selecting the Tools -> Android -> AVD Manager menu option, clicking on the pencil icon in the Actions column of the AVD, followed by the Change… button next to the current Android version. Select a target within the system image dialog that includes the Google APIs.
- Check the Logcat output for any areas relating to Google Maps API authentication problems. This usually means the API key was entered incorrectly. Ensure that the API key in the AndroidManifest.xml and local.properties files matches the key generated in the Google Cloud console. • Verify within the Google API Console that Maps SDK for Android has been enabled in the Credentials panel.
Understanding Geocoding and Reverse Geocoding
It is impossible to talk about maps and geographical locations without first covering the subject of Geocoding. Geocoding converts a textual-based geographical location (such as a street address) into geographical coordinates expressed as longitude and latitude.
Geocoding can be achieved using the Android Geocoder class. For example, an instance of the Geocoder class can be passed a string representing a location, such as a city name, street address, or airport code. The Geocoder will attempt to find a match for the location and return a list of Address objects that potentially match the location string, ranked in order with the closest match at position 0 in the list. A variety of information can then be extracted from the Address objects, including the longitude and latitude of the potential matches.
The following code, for example, requests the location of the National Air and Space Museum in Washington, D.C.:
import android.location.Geocoder
import android.location.Address
import java.io.IOException
.
.
val latitude: Double
val longitude: Double
var geocodeMatches: List<Address>? = null
try {
geocodeMatches = Geocoder(this).getFromLocationName(
"600 Independence Ave SW, Washington, DC 20560", 1)
} catch (e: IOException) {
e.printStackTrace()
}
if (geocodeMatches != null) {
latitude = geocodeMatches[0].latitude
longitude = geocodeMatches[0].longitude
}
Code language: Kotlin (kotlin)
Note that the value of 1 is passed through as the second argument to the getFromLocationName() method. This tells the Geocoder to return only one result in the array. Given the specific nature of the address provided, there should only be one potential match. For more vague location names, however, requesting more potential matches and allowing the user to choose the correct one may be necessary.
The above code is an example of forward-geocoding in that coordinates are calculated based on a text location description. Reverse-geocoding, as the name suggests, involves the translation of geographical coordinates into a human-readable address string. Consider, for example, the following code:
import android.location.Geocoder
import android.location.Address
import java.io.IOException
.
.
var geocodeMatches: List<Address>? = null
val Address1: String?
val Address2: String?
val State: String?
val Zipcode: String?
val Country: String?
try {
geocodeMatches = Geocoder(this).getFromLocation(38.8874245, -77.0200729, 1)
} catch (e: IOException) {
e.printStackTrace()
}
if (geocodeMatches != null) {
Address1 = geocodeMatches[0].getAddressLine(0)
Address2 = geocodeMatches[0].getAddressLine(1)
State = geocodeMatches[0].adminArea
Zipcode = geocodeMatches[0].postalCode
Country = geocodeMatches[0].countryName
}
Code language: Kotlin (kotlin)
The Geocoder object is initialized with latitude and longitude values via the getFromLocation() method. Once again, only a single matching result is requested. The text-based address information is then extracted from the resulting Address object.
The geocoding is not performed on the Android device but rather on a server to which the device connects when a translation is required, and the results are returned when the translation is complete. Geocoding can only occur when the device has an active internet connection.
Adding a Map to an Application
The simplest way to add a map to an application is to specify it in the user interface layout XML file for an activity. The following example layout file shows the SupportMapFragment instance added to the activity_maps. xml file created by Android Studio:
<fragment 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"
android:id="@+id/map"
tools:context=".MapsActivity"
android:name="com.google.android.gms.maps.SupportMapFragment"/>
Code language: HTML, XML (xml)
Requesting Current Location Permission
As outlined in the chapter entitled An Android Permission Requests Tutorial, certain permissions are considered dangerous and require special handling for Android 6.0 or later. One set of permissions allows applications to identify the user’s current location. Edit the AndroidManifest.xml file located under app -> manifests in the Project tool window and add the following permission lines:
<uses-permission
android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission
android:name="android.permission.ACCESS_COARSE_LOCATION" />
Code language: HTML, XML (xml)
These settings will ensure that the app can provide permission to obtain location information when installed on older versions of Android. To support Android 6.0 or later, however, we need to add some code to the MapsActivity.kt file to request this permission at runtime.
Begin by adding some import directives and a constant to act as the permission request code:
package com.ebookfrenzy.mapdemo
.
.
import androidx.core.content.ContextCompat
import androidx.core.app.ActivityCompat
import android.Manifest
import android.widget.Toast
import android.content.pm.PackageManager
.
.
class MapsActivity : FragmentActivity(), OnMapReadyCallback {
private val LOCATION_REQUEST_CODE = 101
private lateinit var mMap: GoogleMap? = null
.
.
}
Code language: Kotlin (kotlin)
Next, a method must be added to the class to request a specified permission from the user. Remaining within the MapsActivity.kt class file, implement this method as follows:
private fun requestPermission(permissionType: String,
requestCode: Int) {
ActivityCompat.requestPermissions(this,
arrayOf(permissionType), requestCode
)
}
Code language: Kotlin (kotlin)
When the user has responded to the permission request, the onRequestPermissionsResult() method will be called on the activity. Remaining in the MapsActivity.kt file, implement this method now so that it reads as follows:
override fun onRequestPermissionsResult(requestCode: Int,
permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
LOCATION_REQUEST_CODE -> {
if (grantResults.isEmpty() || grantResults[0] !=
PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this,
"Unable to show location - permission required",
Toast.LENGTH_LONG).show()
} else {
val mapFragment = supportFragmentManager
.findFragmentById(R.id.map) as SupportMapFragment
mapFragment.getMapAsync(this)
}
}
}
}
Code language: Kotlin (kotlin)
If the user has not granted permission, the app displays a message indicating that the current location cannot be displayed. If, on the other hand, permission was granted, the map is refreshed to provide an opportunity for the location marker to be displayed.
Displaying the User’s Current Location
Once the appropriate permission has been granted, the user’s current location may be displayed on the map by obtaining a reference to the GoogleMap object associated with the displayed map and calling the setMyLocationEnabled() method of that instance, passing through a true value.
When the map is ready to display, the onMapReady() method of the activity is called. This method will also be called when the map is refreshed within the onRequestPermissionsResult() method above. By default, Android Studio has implemented this method and added some code to orient the map over Australia with a marker positioned over the city of Sidney. Locate and edit the onMapReady() method in the MapsActivity.kt file to remove this template code and add code to check that the location permission has been granted before enabling the display of the user’s current location. If permission has not been granted, a request is made to the user via a call to the previously added requestPermission() method:
override fun onMapReady(googleMap: GoogleMap) {
mMap = googleMap
// Add a marker in Sydney and move the camera
// val sydney = LatLng(-34.0, 151.0)
// mMap.addMarker(MarkerOptions().position(sydney).title("Marker in Sydney"))
// mMap.moveCamera(CameraUpdateFactory.newLatLng(sydney))
val permission = ContextCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_FINE_LOCATION)
if (permission == PackageManager.PERMISSION_GRANTED) {
mMap.isMyLocationEnabled = true
} else {
requestPermission(
Manifest.permission.ACCESS_FINE_LOCATION,
LOCATION_REQUEST_CODE)
}
}
Code language: Kotlin (kotlin)
When the app is now run, the dialog shown in Figure 79-10 will appear requesting location permission. If permission is granted, a blue dot will appear on the map indicating the device’s location.
Changing the Map Type
The type of map displayed can be modified dynamically by making a call to the setMapType() method of the corresponding GoogleMap object, passing through one of the following values:
· GoogleMap.MAP_TYPE_NONE – An empty grid with no mapping tiles displayed.
· GoogleMap.MAP_TYPE_NORMAL – The standard view consisting of the classic road map.
· GoogleMap.MAP_TYPE_SATELLITE – Displays the satellite imagery of the map region.
· GoogleMap.MAP_TYPE_HYBRID – Displays satellite imagery with the road map superimposed.
· GoogleMap.MAP_TYPE_TERRAIN – Displays topographical information such as contour lines and colors. The following code change to the onMapReady() method, for example, switches a map to Satellite mode:
.
.
} else {
requestPermission(
Manifest.permission.ACCESS_FINE_LOCATION,
LOCATION_REQUEST_CODE)
}
mMap.mapType = GoogleMap.MAP_TYPE_SATELLITE
}
.
.
Code language: Kotlin (kotlin)
Alternatively, the map type may be specified in the XML layout file where the map is embedded using the map:mapType property together with a value of none, normal, hybrid, satellite, or terrain. For example:
<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:map="http://schemas.android.com/apk/res-auto"
android:id="@+id/map"
android:layout_width="match_parent"
android:layout_height="match_parent"
map:mapType="hybrid"
android:name="com.google.android.gms.maps.SupportMapFragment"/>
Code language: HTML, XML (xml)
Displaying Map Controls to the User
The Google Maps Android API provides several controls that may be optionally displayed to the user consisting of zoom-in and out buttons, a “my location” button, and a compass.
Whether or not the zoom and compass controls are displayed may be controlled programmatically or within the map element in XML layout resources. To programmatically configure the controls, a reference to the UiSettings object associated with the GoogleMap object must be obtained:
val mapSettings = mMap.uiSettings
Code language: Kotlin (kotlin)
The zoom controls are turned on and off via the isZoomControlsEnabled property of the UiSettings object. For example:
mapSettings.isZoomControlsEnabled = true
Code language: Kotlin (kotlin)
Alternatively, the map:uiZoomControls property may be set within the map element of the XML resource file:
map:uiZoomControls="false"
Code language: HTML, XML (xml)
The compass may be displayed via a call to the setCompassEnabled() method of the UiSettings instance or through XML resources using the map:uiCompass property. Note that the compass icon only appears when the map camera is tilted or rotated away from the default orientation.
As outlined earlier in this chapter, the “My Location” button will only appear when My Location mode is enabled. The button may be prevented from appearing even in this mode via a call to the setMyLocationButtonEnabled() method of the UiSettings instance.
Handling Map Gesture Interaction
The Google Maps Android API can respond to various user interactions. These interactions can be used to change the map area displayed, the zoom level, and even the angle of view (such that a 3D representation of the map area is displayed for certain cities).
Map Zooming Gestures
Support for gestures relating to zooming in and out of a map may be turned on or off using the isZoomGesturesEnabled property of the UiSettings object associated with the GoogleMap instance. For example, the following code turns off zoom gestures for our example map:
val mapSettings = mMap.uiSettings
mapSettings.isZoomGesturesEnabled = true
Code language: Kotlin (kotlin)
The same result can be achieved within an XML resource file by setting the map:uiZoomGestures property to true or false.
When enabled, zooming will occur when the user makes pinching gestures on the screen. Similarly, a double tap will zoom in, while a two-finger tap will zoom out. On the other hand, one-finger zooming gestures are performed by tapping twice but not releasing the second tap and then sliding the finger up and down on the screen to zoom in and out, respectively.
Map Scrolling/Panning Gestures
A scrolling or panning gesture allows the user to move around the map by dragging the map around the screen with a single-finger motion. Scrolling gestures may be enabled within code via a call to the isScrollGesturesEnabled property of the UiSettings instance:
val mapSettings = mMap.uiSettings
mapSettings.isScrollGesturesEnabled = true
Code language: Kotlin (kotlin)
Alternatively, scrolling on a map instance may be enabled in an XML resource layout file using the map:uiScrollGestures property.
Map Tilt Gestures
Tilt gestures allow the user to tilt the map’s projection angle by placing two fingers on the screen and moving them up and down to adjust the tilt angle. Tilt gestures may be turned on or off via the isTiltGesturesEnabled property of the UiSettings instance, for example:
val mapSettings = mMap.uiSettings
mapSettings.isTiltGesturesEnabled = true
Code language: Kotlin (kotlin)
Tilt gestures may also be turned on and off using the map:uiTiltGestures property in an XML layout resource file.
Map Rotation Gestures
By placing two fingers on the screen and rotating them in a circular motion, the user may rotate the orientation of a map when map rotation gestures are enabled. This gesture support is turned on and off in code via the isRotateGesturesEnabled property of the UiSettings instance, for example:
val mapSettings = mMap.uiSettings
mapSettings.isRotateGesturesEnabled = true
Code language: Kotlin (kotlin)
Rotation gestures may also be turned on and off using the map:uiRotateGestures property in an XML layout resource file.
Creating Map Markers
Markers notify the user of locations on a map and take the form of either a standard or custom icon. Markers may also include a title and optional text (called a snippet) and may be configured to be dragged to different locations on the map by the user. When the user taps a marker, an info window will appear, displaying additional information about the marker’s location.
Markers are represented by instances of the Marker class and are added to a map via a call to the addMarker() method of the corresponding GoogleMap object. A MarkerOptions class instance containing the various options required for the marker, such as the title and snippet text, is passed through as an argument to this method. The location of a marker is defined by specifying latitude and longitude values, also included as part of the MarkerOptions instance. For example, the following code adds a marker, including a title, snippet, and a position to a specific location on the map:
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.MarkerOptions
.
.
val position = LatLng(38.8874245, -77.0200729)
mMap.addMarker(MarkerOptions()
.position(position)
.title("Museum")
.snippet("National Air and Space Museum"))
Code language: Kotlin (kotlin)
When executed, the above code will mark the location specified, which, when tapped, will display an info window containing the title and snippet, as shown in Figure 79-11:
Controlling the Map Camera
Because Android device screens are flat and the world is a sphere, the Google Maps Android API uses the Mercator projection to represent Earth on a flat surface. The map’s default view is presented to the user as though through a camera suspended above the map and pointing directly down at the map. The Google Maps Android API allows the target, zoom, bearing, and tilt of this camera to be changed in real time from within the application:
- Target – The location of the center of the map within the device display specified using longitude and latitude.
- Zoom – The zoom level of the camera specified in levels. Increasing the zoom level by 1.0 doubles the width of the amount of the map displayed.
- Tilt – The camera’s viewing angle specified as a position on an arc spanning directly over the center of the viewable map area measured in degrees from the top of the arc (this being the nadir of the arc where the camera points directly down to the map).
- Bearing – The orientation of the map in degrees measured in a clockwise direction from North.
Camera changes are made by creating an instance of the CameraUpdate class with the appropriate settings. CameraUpdate instances are created by making method calls to the CameraUpdateFactory class. Once a CameraUpdate instance has been created, it is applied to the map via a call to the moveCamera() method of the GoogleMap instance. To obtain a smooth animated effect as the camera changes, the animateCamera() method may be called instead of moveCamera().
A summary of CameraUpdateFactory methods is as follows:
- CameraUpdateFactory.zoomIn() – Provides a CameraUpdate instance zoomed in by one level.
- CameraUpdateFactory.zoomOut() – Provides a CameraUpdate instance zoomed out by one level.
- CameraUpdateFactory.zoomTo(float) – Generates a CameraUpdate instance that changes the zoom level to the specified value.
- CameraUpdateFactory.zoomBy(float) – Provides a CameraUpdate instance with a zoom level increased or decreased by the specified amount.
- CameraUpdateFactory.zoomBy(float, Point) – Creates a CameraUpdate instance that increases or decreases the zoom level by the specified value.
- CameraUpdateFactory.newLatLng(LatLng) – Creates a CameraUpdate instance that changes the camera’s target latitude and longitude.
- CameraUpdateFactory.newLatLngZoom(LatLng, float) – Generates a CameraUpdate instance that changes the camera’s latitude, longitude, and zoom.
- CameraUpdateFactory.newCameraPosition(CameraPosition) – Returns a CameraUpdate instance that moves the camera to the specified position. A CameraPosition instance can be obtained using CameraPosition. Builder().
The following code, for example, zooms in the camera by one level using animation:
mMap.animateCamera(CameraUpdateFactory.zoomIn())
Code language: Kotlin (kotlin)
The following code, on the other hand, moves the camera to a new location and adjusts the zoom level to 10 without animation:
val position = LatLng(38.8874245, -77.0200729)
mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(position, 10f))
Code language: Kotlin (kotlin)
Finally, the next code example uses CameraPosition.Builder() to create a CameraPosition object with changes to the target, zoom, bearing, and tilt. This change is then applied to the camera using animation:
import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.CameraUpdateFactory
.
.
val cameraPosition = CameraPosition.Builder()
.target(position)
.zoom(50f)
.bearing(70f)
.tilt(25f)
.build()
mMap.animateCamera(CameraUpdateFactory.newCameraPosition(
cameraPosition))
Code language: Kotlin (kotlin)
Summary
This chapter has provided an overview of the key classes and methods that make up the Google Maps Android API and outlined how to prepare both the development environment and an application project to use the API.