Using Street View and Geocoding in your Android app
Today, Google Maps is easily one of the world’s most popular mapping services, allowing millions of users to plan their journeys, discover new places to visit, and get a taste of what it’s really like to walk around places they may have never even visited.
We’ve already looked at how you can use the Google Maps API to retrieve and display the user’s location, but this powerful API isn’t limited to sticking a pin in a map!
In this article, we’ll be looking at some of the additional features that are included in the Google Maps API. By the end of this article, you’ll know how to:
- Give your users the freedom to switch between all the different Google Maps styles: Normal, Satellite, Terrain and Hybrid.
- Convert the device’s longitude and latitude coordinates into a more user-friendly street address, and display this information as part of your UI.
- Display 360-degree, interactive panoramas of locations across the globe, by adding Street View support to your app.
Creating a basic Google Maps app
Before we can implement any of these features, we need to create a project that displays a basic Google Maps fragment.
To get this setup out of the way as quickly as possible, I’ll be using Android Studio’s ‘Google Maps Activity’ template and generating a debug API key, which is required if your project is going to display any Google Maps content. Just be aware that debug API keys aren’t particularly secure, so before publishing an application you must always generate a new API key based on your project’s release certificate.
- Create a new project using the ‘Google Maps Activity’ template.
- Open your project’s res/values/google_maps_api.xml file. This file contains a URL with all the information the Google API Console needs to generate an API key. Find this URL, and copy/paste it into your web browser.
- Make sure ‘Create a project’ is selected in the Console’s dropdown menu, and then click ‘Continue.’
- Click ‘Create API key.’
- The API Console will prompt you to restrict the API key. A restricted API will only work on a platform that supports that type of application, which tends to make your key more secure. Unless you have a specific reason not to, you should select ‘Restrict key.’
- Under ‘Key restriction,’ make sure ‘Android apps’ is selected, and then click ‘Save.’
- Copy your API key, and then switch back to Android Studio.
- Open your project’s google_maps_api.xml file and paste your API key into the YOUR_KEY section:
<string name="google_maps_key" templateMergeStrategy="preserve" translatable="false">YOUR_KEY</string>
- Open your module-level build.gradle file and add the Google Maps dependencies:
dependencies { compile 'com.google.android.gms:play-services-maps:11.6.2' compile 'com.google.android.gms:play-services-location:11.6.2'
If your project refuses to compile, then make sure your development environment is up to date, by opening the Android SDK Manager and installing any available updates – in particular make sure you have the latest versions of Google Play Services and Google Repository.
This is the bare minimum required to display Google Maps content, so at this point you may want to take this project for a spin by installing it on your physical smartphone or tablet, or an AVD (Android Virtual Device). If you’re testing this project on an AVD, then you’ll need to use a system image that includes the Google APIs.
Currently this project displays a map with a marker permanently set to Sydney, Australia. This isn’t exactly going to wow your users, so let’s look at a few different ways of making this project more interesting.
Displaying the user’s address with reverse geocoding
When you include Google Maps content in your application, you typically display the user’s current location via a marker, but there’s plenty of scenarios where it’s more useful to display location as a street address. For example, if you’re booking a taxi the old-fashioned way (i.e by calling the taxi company) or arranging to meet a friend, then knowing the street you’re currently on is going to be pretty useful!
While your users could work this out for themselves by zooming in on their location marker and looking at the surrounding labels, you can provide a much better experience by presenting this information to them. This process of converting a set of longitude and latitude values into a street address, is known as reverse geocoding.
In this section, we’re going to add a button to our application that, when tapped, retrieves the device’s longitude and latitude, reverse geocodes these coordinates into an approximate street address, and then presents this information to the user.
Update your layout
Let’s start with the easy stuff, and update our user interface. When you create a project using the Google Maps Activity template, the activity_maps.xml file contains a SupportMapFragment that fills the entire screen.
I’m going to expand on this layout to include a ‘Get My Location’ button that, when tapped, updates a TextView with the reverse geocoded data.
<RelativeLayout xmlns:android="http://ift.tt/nIICcg" xmlns:tools="http://ift.tt/LrGmb4" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.jessicathornsby.google_maps.MapsActivity" > <fragment android:id="@+id/map" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1" android:scrollbars="vertical" class="com.google.android.gms.maps.SupportMapFragment"/> <LinearLayout android:layout_width="match_parent" android:layout_height="60dp" android:orientation="horizontal" android:padding="10dp" android:layout_alignParentBottom="true" android:background="#ffffff" > <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/get_location" android:gravity="left" /> <TextView android:id="@+id/textview" android:layout_width="0dp" android:layout_height="match_parent" android:gravity="right" android:layout_weight="1"/> </LinearLayout> </RelativeLayout>
Create your strings
Next, define the string resources that we’ll be using throughout this project:
//Create the button label// <string name="get_location">Get my location</string> <string name="address_text">"Address: %1$s"</string>
The second string resource is a placeholder that contains the following:
- %1. A placeholder for a value. This value will either be a formatted address or a message that an error has occurred.
- $s. The format of the placeholder value, i.e a String.
You convert latitude and longitude values into a physical address using the getFromLocation() method, which returns a list of Address objects.
The level of detail returned by getFromLocation() will vary depending on the location. Sometimes reverse geocoding may return a full address, right down to the house number; sometimes it’ll return the name of the nearest building – and occasionally it may return no information at all.
While the latter is unlikely, your application shouldn’t crash if it does encounter this scenario. Here, I’m creating a String just in case this app can’t match the coordinates to any known address:
<string name="no_address">Cannot retrieve address at this time</string>
On devices running Android 6.0 (API level 23) and higher, applications need to request permissions at runtime, and the user can then accept or deny each request, on a permission-by-permission basis.
If the user denies a permission request, then you need to communicate the impact this will have on your application. In this project, I’m going to display the following text as part of a toast:
<string name="location_permission_denied">Location permission denied. Current location unavailable.</string>
When working on your own Android projects, you may also want to disable or remove parts of your application that rely on the denied permission, for example removing items from menus, or “greying out” certain UI controls.
Add the Internet permission
Reverse geocoding requires an Internet connection, so open your project’s Manifest and add the Internet permission:
<uses-permission android:name="android.permission.INTERNET" />
Create an AyncTask
Since reverse geocoding uses the network, it has the potential to block Android’s main thread. To avoid Application Not Responding (ANR) errors and application crashes, you must perform the reverse geocoding operation off the main thread. There’s various ways of creating background threads, but I’m going to use an AsyncTask.
Create a new Java class (I’m naming mine ReverseGeo) and implement the AsyncTask:
import android.location.Address; import java.util.ArrayList; import android.os.AsyncTask; import android.content.Context; import android.location.Location; import android.location.Geocoder; import java.util.List; import java.util.Locale; import java.io.IOException; import android.text.TextUtils; /** * Created by jessicathornsby on 06/12/2017. */ class ReverseGeo extends AsyncTask<Location, Void, String> { private Context mContext; //Add a parameter for the onTaskComplete interface that we’ll be creating shortly// private OnTaskComplete mListener; ReverseGeo(Context applicationContext, OnTaskComplete listener) { mListener = listener; mContext = applicationContext; } //Publish the results of our AsyncTask; in this instance that’s the returned address// @Override //Override the onPostExecute() method// protected void onPostExecute(String address) { //Once the AsyncTask has finished, //call onTaskComplete and update your UI with the returned address// mListener.onTaskComplete(address); super.onPostExecute(address); } //Implement AsyncTask’s doInBackground() method, //where we’ll convert the Location object into an address// @Override protected String doInBackground(Location... params) { //Create a Geocoder object, which is a class that can perform geocoding operations// Geocoder mGeocoder = new Geocoder(mContext, //Localize the address// Locale.getDefault()); //Obtain a Location object// Location location = params[0]; //Create an empty List of Address objects, which will eventually contain the returned address// List<Address> addresses = null; //Create a String to hold the formatted address// String printAddress = ""; //Obtain the list of addresses for the current location, using getFromLocation// try { addresses = mGeocoder.getFromLocation( location.getLatitude(), location.getLongitude(), //Specify the maximum number of addresses that the TextView should display// 1); //Catch any exceptions, for example if the network is unavailable// } catch (IOException ioException) { printAddress = mContext.getString(R.string.no_address); } //If the geocoder can't match the coordinates to an address, then return an empty list// if (addresses.size() == 0) { if (printAddress.isEmpty()) { //If the address list is empty, then display the no_address string// printAddress = mContext.getString(R.string.no_address); } } else { //If the list isn’t empty, then create an ArrayList of strings// Address address = addresses.get(0); ArrayList<String> addressList = new ArrayList<>(); //Fetch the address lines, using getMaxAddressLineIndex, //and then and combine them into a String// for (int i = 0; i <= address.getMaxAddressLineIndex(); i++) { addressList.add(address.getAddressLine(i)); } printAddress = TextUtils.join( ",", addressList); } //Return the printAddress object// return printAddress; } //Create the OnTaskComplete interface, which takes a String as an argument// interface OnTaskComplete { void onTaskComplete(String result); } }
Implement ReverseGeo in MapsActivity
Next, we need to implement ReverseGeo in our project’s automatically-generated MapsActivity class, and then override the onTaskComplete() method. I’m also implementing the onClickListener so our application can respond to the user tapping the ‘Get My Location’ button.
import com.google.android.gms.location.FusedLocationProviderClient; import com.google.android.gms.location.LocationCallback; import com.google.android.gms.location.LocationResult; import com.google.android.gms.location.LocationRequest; import com.google.android.gms.location.LocationServices; import android.support.v4.app.ActivityCompat; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.widget.Button; import android.Manifest; import android.content.pm.PackageManager; import android.widget.TextView; import android.widget.Toast; import android.view.View; public class MapsActivity extends AppCompatActivity implements ReverseGeo.OnTaskComplete { private static final int MY_PERMISSIONS_REQUEST_LOCATION = 1; private Button button; private TextView textview; private boolean addressRequest; //Create a member variable of the FusedLocationProviderClient type// private FusedLocationProviderClient mFusedLocationClient; private LocationCallback mLocationCallback; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_maps); button = findViewById(R.id.button); textview = findViewById(R.id.textview); //Initialize mFusedLocationClient// mFusedLocationClient = LocationServices.getFusedLocationProviderClient( this); //Create the onClickListener// button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //Call getAddress, in response to onClick events// if (!addressRequest) { getAddress(); } } }); //Create a LocationCallback object// mLocationCallback = new LocationCallback() { @Override //Override the onLocationResult() method, //which is where this app receives its location updates// public void onLocationResult(LocationResult locationResult) { if (addressRequest) { //Execute ReverseGeo in response to addressRequest// new ReverseGeo(MapsActivity.this, MapsActivity.this) //Obtain the device's last known location from the FusedLocationProviderClient// .execute(locationResult.getLastLocation()); } } }; } //Implement getAddress// private void getAddress() { if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.ACCESS_FINE_LOCATION}, MY_PERMISSIONS_REQUEST_LOCATION); } else { addressRequest = true; //Request location updates// mFusedLocationClient.requestLocationUpdates (getLocationRequest(), mLocationCallback, null); //If the geocoder retrieves an address, then display this address in the TextView// textview.setText(getString(R.string.address_text)); } } //Specify the requirements for your application's location requests// private LocationRequest getLocationRequest() { LocationRequest locationRequest = new LocationRequest(); //Specify how often the app should receive location updates, in milliseconds// locationRequest.setInterval(10000); return locationRequest; } @Override public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { switch (requestCode) { case MY_PERMISSIONS_REQUEST_LOCATION: if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { //If the permission request has been granted, then call getAddress// getAddress(); } else { Toast.makeText(this, R.string.location_permission_denied, Toast.LENGTH_SHORT).show(); } break; } } @Override public void onTaskComplete(String result) { if (addressRequest) { //Update the TextView with the reverse geocoded address// textview.setText(getString(R.string.address_text, result)); } } }
Testing your reverse geocoding application
Let’s put this application to the test:
- Install the updated project on your Android device.
- Make sure you’re connected to the Internet.
- Tap the ‘Get My Location’ button.
- Grant the ACCESS_FINE_LOCATION request; the TextView should update to display an estimated street address.
Since we’re requesting the ACCESS_FINE_LOCATION permission at runtime, we need to test how our application handles rejection:
- Launch your device’s ‘Settings’ application.
- Tap ‘Apps.’
- Select the maps application from the list.
- Select ‘Permissions.’
- Push the ‘Location’ slider into the ‘Off’ position.
- Launch your maps application.
- Tap the ‘Get My Location’ button.
- When prompted, deny the ACCESS_FINE_LOCATION request; the application should respond by displaying a toast.
You should also test how your application functions when it has access to your location, but can’t match the coordinates to any known address. If you’re using a physical Android device, then you can test this scenario using a third party app:
- Download an application that can spoof your location, such as the free ‘Fake GPS’ app.
- Use this application to trick your device into believing you’re somewhere that doesn’t have a street address – the middle of the ocean is usually a safe bet!
- Switch back to your maps application, and tap ‘Get My Location.’ The TextView should display the no_address string.
If you’re testing this project on an AVD, then you can change the device’s coordinates using the strip of buttons that appear alongside the emulator:
- Click the three-dotted menu icon (where the cursor is positioned in the following screenshot).
- Select ‘Location’ from the left-hand menu.
- Enter a new set of longitude/longitude values, and click ‘Send.’
- Press the application’s ‘Get My Location’ button; the TextView should update to display the no_address string.
Adding different map types
Any Google Maps content you include in your app will use the “normal” map style by default – but “normal” isn’t the only option!
The Google Maps API supports a few different map styles:
- MAP_TYPE_SATELLITE. A Google Earth satellite photograph, without road or feature labels.
- MAP_TYPE_HYBRID. A satellite photograph with road and feature labels.
- MAP_TYPE_TERRAIN. A topographic map featuring contour lines, labels and perspective shading, with some labels.
To display anything other than a “normal” map, you’ll need to use the setMapType method:
mMap = googleMap; mMap.setMapType(GoogleMap.MAP_TYPE_TERRAIN);
Alternatively, why not give your users the freedom to switch between map styles?
In this section, we’re going to add a dropdown menu that allows your users to move between the normal, hybrid, terrain and satellite map styles, with ease.
Start by creating a menu resource:
- Control-click your project’s ‘res’ directory and select ‘New > Android Resource File.’
- Give this resource a name; I’m using ‘maps_menu.’
- Open the ‘Resource type’ dropdown and select ‘Menu.’
- Click ‘OK.’
- Copy/paste the following code into this file:
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://ift.tt/nIICcg" xmlns:app="http://ift.tt/GEGVYd"> <item android:id="@+id/normal" android:title="@string/normal" app:showAsAction="never"/> <item android:id="@+id/hybrid" android:title="@string/hybrid" app:showAsAction="never"/> <item android:id="@+id/satellite" android:title="@string/satellite" app:showAsAction="never"/> <item android:id="@+id/terrain" android:title="@string/terrain" app:showAsAction="never"/> </menu>
Open your project’s strings.xml file and define all the menu labels:
<string name="normal">Normal map</string> <string name="terrain">Terrain map</string> <string name="hybrid">Hybrid map</string> <string name="satellite">Satellite map</string>
Next, you’ll need to implement the menu in your MapsActivity. To make this process clearer, I’ve removed all geocoding-specific code from this Activity.
import android.content.pm.PackageManager; import android.os.Bundle; import android.support.v4.content.ContextCompat; import android.support.v7.app.AppCompatActivity; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.maps.GoogleMap; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import com.google.android.gms.maps.OnMapReadyCallback; import com.google.android.gms.maps.SupportMapFragment; public class MapsActivity extends AppCompatActivity implements OnMapReadyCallback, GoogleApiClient.ConnectionCallbacks { private GoogleMap mMap; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_maps); //Obtain the SupportMapFragment// SupportMapFragment mapFragment = SupportMapFragment.newInstance(); getSupportFragmentManager().beginTransaction() .add(R.id.map, mapFragment).commit(); mapFragment.getMapAsync(this); } //Override the onCreateOptionsMenu() method// @Override public boolean onCreateOptionsMenu(Menu menu) { //Inflate the maps_menu resource// MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.maps_menu, menu); return true; } //Override the onOptionsItemSelected() method// @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.normal: //Use setMapType to change the map style based on the user’s selection// mMap.setMapType(GoogleMap.MAP_TYPE_NORMAL); return true; case R.id.hybrid: mMap.setMapType(GoogleMap.MAP_TYPE_HYBRID); return true; case R.id.terrain: mMap.setMapType(GoogleMap.MAP_TYPE_TERRAIN); return true; case R.id.satellite: mMap.setMapType(GoogleMap.MAP_TYPE_SATELLITE); return true; default: return super.onOptionsItemSelected(item); } } @Override public void onMapReady(GoogleMap googleMap) { mMap = googleMap; if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED) { mMap.setMyLocationEnabled(true); } } public void onConnected(Bundle bundle) { //To do// } @Override public void onConnectionSuspended(int i) { } }
Install the updated application on your physical Android device or AVD, open the menu, and test all the different map styles.
Adding Street View to your project
Even examining the same location across multiple map styles can’t quite compare to the experience of exploring that location from a first-person perspective – which is where Street View comes in.
In this final section, I’ll show you how to provide a tangible sense of what a location is really like, by integrating Street View into our application.
Let’s start by updating our layout:
<RelativeLayout xmlns:android="http://ift.tt/nIICcg" xmlns:tools="http://ift.tt/LrGmb4" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.jessicathornsby.google_maps.MapsActivity" > <fragment android:id="@+id/map" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1" android:scrollbars="vertical" class="com.google.android.gms.maps.SupportMapFragment"/> <LinearLayout android:layout_width="match_parent" android:layout_height="60dp" android:orientation="horizontal" android:padding="10dp" android:layout_alignParentBottom="true" android:background="#ffffff" > <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Street View" android:onClick="launchStreetView" android:gravity="left" /> <TextView android:id="@+id/textview" android:layout_width="0dp" android:layout_height="match_parent" android:gravity="right" android:layout_weight="1"/> </LinearLayout> </RelativeLayout>
Next, I’m going to create a StreetViewActivity, where I’ll implement the Street View service. When you include a Street View panorama in your application, all the standard Street View actions are included by default, which is why the following code doesn’t contain any manual implementations of panning and zooming gestures, or navigating to adjacent panoramas, as you already get all this functionality for free!
Since I’m displaying the Street View panorama inside an Android View, I’m using StreetViewPanoramaView, which is a subclass of the View class. To display a panorama inside a Fragment, you’d use StreetViewPanoramaFragment instead.
import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.view.ViewGroup.LayoutParams; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.StreetViewPanoramaOptions; import com.google.android.gms.maps.StreetViewPanoramaView; public class StreetViewActivity extends AppCompatActivity { //Define the LatLng value we’ll be using for the paranorma’s initial camera position// private static final LatLng LONDON = new LatLng(51.503324, -0.119543); private StreetViewPanoramaView mStreetViewPanoramaView; private static final String STREETVIEW_BUNDLE_KEY = "StreetViewBundleKey"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //Configure the panorama by passing in a StreetViewPanoramaOptions object// StreetViewPanoramaOptions options = new StreetViewPanoramaOptions(); if (savedInstanceState == null) { //Set the panorama’s location// options.position(LONDON); } mStreetViewPanoramaView = new StreetViewPanoramaView(this, options); addContentView(mStreetViewPanoramaView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); Bundle mStreetViewBundle = null; if (savedInstanceState != null) { mStreetViewBundle = savedInstanceState.getBundle(STREETVIEW_BUNDLE_KEY); } mStreetViewPanoramaView.onCreate(mStreetViewBundle); } }
Don’t forget to add the StreetViewActivity to your Manifest:
<activity android:name=".MapsActivity" android:label="@string/title_activity_maps"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".StreetViewActivity"></activity> </application>
Finally, we need to implement launchStreetView in our MapsActivity, so that android:onClick=”launchStreetView” triggers the StreetViewActivity class:
import android.content.pm.PackageManager; import android.os.Bundle; import android.support.v4.content.ContextCompat; import android.support.v7.app.AppCompatActivity; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.maps.GoogleMap; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import com.google.android.gms.maps.OnMapReadyCallback; import com.google.android.gms.maps.SupportMapFragment; import android.content.Intent; import android.view.View; public class MapsActivity extends AppCompatActivity implements OnMapReadyCallback, GoogleApiClient.ConnectionCallbacks { private GoogleMap mMap; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_maps); SupportMapFragment mapFragment = SupportMapFragment.newInstance(); getSupportFragmentManager().beginTransaction() .add(R.id.map, mapFragment).commit(); mapFragment.getMapAsync(this); } @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.maps_menu, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.normal: mMap.setMapType(GoogleMap.MAP_TYPE_NORMAL); return true; case R.id.hybrid: mMap.setMapType(GoogleMap.MAP_TYPE_HYBRID); return true; case R.id.terrain: mMap.setMapType(GoogleMap.MAP_TYPE_TERRAIN); return true; case R.id.satellite: mMap.setMapType(GoogleMap.MAP_TYPE_SATELLITE); return true; default: return super.onOptionsItemSelected(item); } } @Override public void onMapReady(GoogleMap googleMap) { mMap = googleMap; if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED) { mMap.setMyLocationEnabled(true); } } public void onConnected(Bundle bundle) { //To do// } @Override public void onConnectionSuspended(int i) { } public void launchStreetView(View view) { Intent intent = new Intent(MapsActivity.this, StreetViewActivity.class); startActivity(intent); } }
Install this project on your Android device, and give the ‘Street View’ button a tap. Your application should respond by launching a new Activity displaying a 360 degree panorama of the London Eye.
Wrapping Up
In this article, we explored a few ways of enhancing your app’s Google Maps content, by adding support for Street View, multiple map styles, and reverse geocoding – but these are still just a few of the features that the Google Maps API has to offer.
What Google Maps features have you used in your own projects? Let us know in the comments below!
No comments: