Google Maps for Android: Basics to Advanced features

Google Maps is one of the most awesome applications that Google makes available for us. Their integration in Android apps is a common desire of a lot of developers, but fortunately it’s not so difficult.


Blog

In this tutorial, you will learn how to do it and the example will show you important concepts from basic configuration to advanced use cases.

We are going to build a “distance calculator” using Google Maps. The approach is totally visual. We choose two points on the Map: the first one moving a Marker, the second one tapping the display.

The app draws a red line between these points, calculates their distance and retrieves corresponding street addresses via geocoding. At the end, an alert dialog notifies the user of the results.

The following figure shows the app in action:

maps_img_03

The case we are treating is interesting as it involves several features beyond the foundations of Google Maps in Android: markers, drawing on the map, distance calculation by Location objects, handling events on Map and geocoding.

The code to write is not so much because Java classes in Google Play Services SDK make our job easier.

Let’s start.

Google Play Services and Maps: preparing the project

To uniform interaction between apps and its own services (including Maps, Drive, Google Plus and so on), Google provided Google Play Services, a software package integrated in Android OS and constantly updated.

An app that uses Google Maps API requires the integration of Google Play Services SDK in the project.

The first step is installing Google Play Services library in our app. The Android SDK Manager can help us. This tutorial is based on Android Studio, the official development tool.

In the Android SDK Manager GUI (we can open it selecting the “SDK Manager” item from Tools > Android sub-menu), we have to download two elements: “Google Play Services” and “Google Repository”.

At the end of download, you can integrate it in the project by using Gradle directives:

dependencies {
    …
    …
    compile 'com.google.android.gms:play-services:7.3.0'
}

 

I indicated 7.3.0 as version number according to my installation. Obviously, it has to be personalized.

However,  this is the complete Gradle build file:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 21
    buildToolsVersion "22.0.1"

    defaultConfig {
        applicationId "com.androidbegin.googlemapstutorial"
        minSdkVersion 14
        targetSdkVersion 21
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:22.1.1'
    compile 'com.google.android.gms:play-services:7.3.0'
}

 

Creating a Google API Project

After installing the library, you have to create a Google project on the Google Developers Console (https://console.developers.google.com). To access it you only need a Google account: your Gmail credentials will be sufficient.

This allows you to create an API key that is required to permit your app to download information’s from Google Maps service.

First of all, you need a Google API Project. After successful login, select an existing project or create a new one clicking the “Create project” button.

The dialog window that appears will ask you a Project Name and a Project ID. You can choose both them.

maps_img_04

In the sidebar on the left, select “API & Auth” > APIs. Search “Google Maps Android API v2” and enable them.

Now you can create your access key.

Select “API & Auth” > Credentials and click the button “Create a new key”. We need a Public access key, but it’s important to remember that it has to be an “Android key”.

You will be asked for a code that is the result of the concatenation of two parts separated by a semicolon: a SHA-1 code and your app’s package name.

The SHA-1 code is your app fingerprint. You can obtain it using key-tool, but the command is different in every operating system.

For Windows:

keytool -list -v -keystore “%USERPROFILE%.androiddebug.keystore” -alias androiddebugkey -storepass android -keypass android

For Linux or Mac:

keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android

These commands generate a debug key. You have to search a SHA1 key in the amount of output.

In my project, SHA1 fingerprint and app’s package name look like this:

17:5F:A3:CA:67:94:E1:AA:E4:02:11:82:9B:3D:8A:53:3A:B2:8D:12;com.androidbegin.googlemapstutorial

After creation you can see an API key like this:

AIzaSy62NbSRLQScJa_5sAWBYM_fGA0G4eAYCeo

The key will be copied and pasted in your project manifest file.

The configuration: the AndroidManifest.xml file

First of all, we have to configure the application.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.androidbegin.googlemapstutorial" >

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES" />

    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <meta-data
            android:name="com.google.android.gms.version"
            android:value="@integer/google_play_services_version" />
        <meta-data
            android:name="com.google.android.maps.v2.API_KEY"
            android:value="@string/google_maps_key" />

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

</manifest>

The AndroidManifest.xml contains fundamental elements so that GoogleMap class we use can get information’s from remote servers.

The required permissions are:

  • android.permission.INTERNET;
  • android.permission.ACCESS_NETWORK_STATE;
  • android.permission.WRITE_EXTERNAL_STORAGE.

They allow the app to access the network and save data for cache on external storage.

Other two permissions are not indispensable, but they can be useful if the app accesses user location:

ACCESS_COARSE_LOCATION and ACCESS_FINE_LOCATION.

Then, we add two meta-data nodes as a child of the <application> element. The first one sets the API KEY we created in our Google Project. The second one declares the version of Google Play Services.

Values within them are referred to files in two /res/values sub-folders:

google_maps_api.xml:

<resources>
    <string name="google_maps_key">AIzaSy62NbSRLQScJa_5sAWBYM_fGA0G4eAYCeo</string>
</resources>

and version.xml:

<resources>
    <integer name="google_play_services_version">7327000</integer>
</resources>

 

The Activity

The only element we need in the layout is a fragment containing the Map that will be provided from the SupportMapFragment class:

<fragment
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/map"
    android:name="com.google.android.gms.maps.SupportMapFragment" />

The code in the Activity class perform three main tasks:

  • configuration of the UI and check of the availability of a Map;
  • configuration of the Map included the Marker and the listener that handles its drag-and-drop events;
  • Map click listener: it activates the functionality of the distance calculation.
public class MapsActivity extends FragmentActivity {

    // the Google Map object
    private GoogleMap mMap;

    // LatLng objects store a pair of terrestrial coordinates (latitude and longitude)
    // STARTING_MARKER_POSITION first values are the coordinates of the Colosseo in Rome (Italy)
    private static LatLng STARTING_MARKER_POSITION =new LatLng(41.892950, 12.494456);

    /* distanceFrom indicates the starting point to calculate the distance from.
       It's initialized with STARTING_MARKER_POSITION
    */

    private LatLng distanceFrom= STARTING_MARKER_POSITION;

    // line will be drawn at the click event
    private Polyline line=null;

    // A Geocoder can transform a pair of latitude/longitude into a street address and viceversa.
    // We'll use it in the listener
    private static Geocoder geocoder=null;


    private GoogleMap.OnMapClickListener clickListener=new GoogleMap.OnMapClickListener() {
        @Override
        public void onMapClick(final LatLng pos) {

            // this method is called when the user taps the map

            // if a line already appears, it's removed
            if (line!=null)
                line.remove();

            // a new line is created
            line = mMap.addPolyline(new PolylineOptions()
                    .add(distanceFrom, pos)
                    .width(5) // width of the line
                    .color(Color.RED)); // line color

            // call the converter object for geocoding invocation and distance calculation
            new AddressConverter().execute(distanceFrom, pos);

        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // we set the layout for the Activity
        setContentView(R.layout.activity_maps);

        // the geocoder is instantiated for the first time
        geocoder=new Geocoder(this);

        // if there isn't a map, it will be created
        setUpMapIfNeeded();
    }

    @Override
    protected void onResume() {
        super.onResume();

        // the availability of the GoogleMap will be checked before the Activity starts interacting with the user
        setUpMapIfNeeded();
    }

    private void setUpMapIfNeeded() {

        // the map is created only it has not been initialized
        if (mMap == null) {

            // the map is located in the layout
            mMap = ((SupportMapFragment) getSupportFragmentManager().findFragmentById(R.id.map)).getMap();

            // if a map exists, we proceed with initialization
            if (mMap != null) {
                setUpMap();
            }
        }
    }

    // Now it's time to configure the map. We can add markers, shapes, event handlers and so on
    private void setUpMap() {

        // the camera will be positioned according to the new coordinates
        mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(STARTING_MARKER_POSITION, 16));

        // we choose the type of the map: Satellite in this case
        mMap.setMapType(GoogleMap.MAP_TYPE_SATELLITE);

        // markerOptions describes the marker we want to place
        MarkerOptions markerOptions=new MarkerOptions()
                                    .position(STARTING_MARKER_POSITION)
                                    .draggable(true);
        // the marker has to be draggable as we'll move it

        // the marker is rendered on the map
        mMap.addMarker(markerOptions);

        // we define the object to invoke when the marker is dragged
        mMap.setOnMarkerDragListener(new GoogleMap.OnMarkerDragListener()
        {
            @Override
            public void onMarkerDragStart(Marker arg0)
            {
                // this method is called when the drag starts
                // the operation we need is the cancellation of a preexisting line
                if (line!=null)
                    line.remove();
            }
            @Override
            public void onMarkerDragEnd(final Marker pos)
            {
               // we get the final position of the marker
               distanceFrom=pos.getPosition();

            }

            @Override
            public void onMarkerDrag(Marker arg0)
            {
                // operations performed during the movement. Nothing to do
            }
        });

        // the callback to invoke is set
        mMap.setOnMapClickListener(clickListener);
    }

    // we want to know which address corresponds to this location
    // we use AsyncTask to perform slower operations on a separate thread
    private class AddressConverter extends AsyncTask<LatLng,Void,String>
   {
       // The ProgressDialog window we'll show during the calculation
       private ProgressDialog progress=null;

       // this method is called before the background job starts. It works on the main thread
       @Override
       protected void onPreExecute() {

           // ProgressDialog is shown
           progress= ProgressDialog.show(MapsActivity.this,"Distance calculator","We are calcultating the distance...", true,false);
       }

       // this method works on a separate thread
       // it performs geocoding operations to retrieve the address of the points and calculates the distance in meters between them
       @Override
       protected String doInBackground(LatLng... params) {

           float[] distance=new float[1];
           try {
               // the Location class contains what we need to calculate distances

               Location.distanceBetween(params[0].latitude,params[0].longitude,params[1].latitude,params[1].longitude,distance);

               // geocoding operations
               List<Address> fromResult=geocoder.getFromLocation(params[0].latitude,params[0].longitude,1);
               List<Address> toResult=geocoder.getFromLocation(params[1].latitude,params[1].longitude,1);

               // the message informs the user about the distance from the marker to the point selected with the click
               // if we have got both the addresses, we use them to compose the message, otherwise we show only the distance
               if (fromResult.size()>0 && toResult.size()>0)
               {
                   return "The distance between " + getAddressDescription(fromResult.get(0)) + " and " + getAddressDescription(toResult.get(0)) + " is " + Math.round(distance[0]) + " meters";
               }
               else
                   return "The distance is " + Math.round(distance[0]) + " meters";

           }
           catch (IOException e) {
               return "The distance is " + Math.round(distance[0]) + " meters";
           }
       }

       @Override
       protected void onPostExecute(String message)
       {
           if (progress!=null)
               progress.dismiss();

           // The builder of the window is instantiated
           AlertDialog.Builder builder=new AlertDialog.Builder(MapsActivity.this);
           builder.setTitle("Distance");
           builder.setMessage(message);

           // the Alert dialog appears
           builder.show();
       }



   }

    // this method only formats the message with addresses
    private String getAddressDescription(Address a)
    {
        String city=a.getLocality();
        String address=a.getAddressLine(0);

        return "'"+address+"' ("+city+")";

    }

}

The onCreate and onResume methods set the layout and initialize a Geocoder object we’ll use to convert geographical coordinates into street addresses.

We store a reference to a GoogleMap object as a private member of the Activity. This reference is retrieved in the setUpMapIfNeeded method call from onResume.

The most important portion of configuration is performed in setUpMap. After calling it, we know a GoogleMap object is available.We move the camera that indicates what part of the world our Map has to show.

The position we start from is described in a LatLng object, referenced in the code as STARTING_MARKER_POSITION.

The programmer can choose the type of the map. We prefer a satellite map that shows details of the city. STARTING_MARKER_POSITION is set to Colosseum coordinates, one of the most important monument of the ancient Rome.

To create a Marker, we have to instantiate a MarkerOptions object. At least, it has to get the marker position as a LatLng object. It’s important that the marker is draggable for our purposes

The OnMarkerDragListener requires the override of three methods:

  • onMarkerDragStart: it’s called when the drag movement starts. We need only to delete a line if a calculation has just been executed;
  • onMarkerDrag: called repeatedly during the movement. We don’t need it;
  • onMarkerDragEnd: it notifies the end of the drag movement. We store final Marker position into distanceFrom variable.

The nested class: geocoding operations

There’s a nested class within MapsActivity, AddressConverter. It extends AsyncTask as we use it to perform geoconding operations. The Geocoder needs to communicate with external servers and this can cause some delay.

AsyncTask definition contains an abstract method, doInBackground, that executes code on a separate thead to not affect UI performances.

On the other hand, onPreExecute and onPostExecute methods work on main thread so they can update the user interface.

The onPreExecute method shows a ProgressDialog that will disappear at the completion of background job.

In doInBackground, we calculate the distance between the points and perform geocoding operations using the Geocoder object. Then, we prepare the string for the message that will be showed in the AlertDialog.

The onPostExecute method dismisses the ProgressDialog as the background job has terminated and shows the AlertDialog that contains computation results for the user.

Source Code

[purchase_link id=”8048″ text=”Purchase to Download Source Code” style=”button” color=”green”]