Using Street View and Geocoding in your Android app

Publish date: 2022-07-17
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.

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:

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.

Code

Copy Text
<string name="google_maps_key" templateMergeStrategy="preserve" translatable="false">YOUR_KEY</string>

Code

Copy Text
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.

Code

Copy Text
<RelativeLayout xmlns:andro xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.jessicathornsby.google_maps.MapsActivity" > <fragment android: android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1" android:scrollbars="vertical" /> <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: android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/get_location" android:gravity="left" /> <TextView android: 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:

Code

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

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:

Code

Copy Text
<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:

Code

Copy Text
<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:

Code

Copy Text
<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:

Code

Copy Text
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.

Code

Copy Text
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:

Since we’re requesting the ACCESS_FINE_LOCATION permission at runtime, we need to test how our application handles rejection:

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:

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:

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:

To display anything other than a “normal” map, you’ll need to use the setMapType method:

Code

Copy Text
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:

Code

Copy Text
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:andro xmlns:app="http://schemas.android.com/apk/res-auto"> <item android: android:title="@string/normal" app:showAsAction="never"/> <item android: android:title="@string/hybrid" app:showAsAction="never"/> <item android: android:title="@string/satellite" app:showAsAction="never"/> <item android: android:title="@string/terrain" app:showAsAction="never"/> </menu>

Open your project’s strings.xml file and define all the menu labels:

Code

Copy Text
<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.

Code

Copy Text
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:

Code

Copy Text
<RelativeLayout xmlns:andro xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.jessicathornsby.google_maps.MapsActivity" > <fragment android: android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1" android:scrollbars="vertical" /> <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: android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Street View" android:onClick="launchStreetView" android:gravity="left" /> <TextView android: 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.

Code

Copy Text
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:

Code

Copy Text
<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:

Code

Copy Text
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!

Comments

ncG1vNJzZmivp6x7orrDq6ainJGqwam70aKrsmaTpLpwv9OrnJ6sXau2psOMmqWdZZeavKS7w6KloGVoZ4BzgJZo