Previous  | Next  | Home

Map Example


 

Let us now construct a more extensive project that will show how to use the Google mapping libraries to add new functionalities to that possible with the Android API alone. In this project we shall

In the process we shall use the modern approach to laying out Android content and display our maps in instances of MapFragment, which extends Fragment. This leads to flexibility in layout across devices with different formfactors because Fragments represent a piece of an application's user interface that can be placed inside an Activity (see the preceding Fragments project). We shall also learn how to implement the Runtime Permissions introduced with Android 6 (API 23).

The use of Google Maps in Android is somewhat more involved than mere Android programming because one must obtain access to additional libraries that are not part of the core Android installation, and also register the app with Google to receive permission to download maps. These additional complications concern only the initial setup; once we have registered the app to receive map updates, declared the permissions that will be required, and linked to the appropriate libraries, programming mapping and location services is much like any other Android programming.


This project will be concerned primarily with programming Google Maps in conjuction with location services. Since it will target Android 6 and will require location permissions, it will also implement the new runtime permissions introduced with Android 6 (API 23). Recommended reading includes

All three of these areas (maps, location services, and permissions) have changed substantially since ~2014, so even if you are familiar with Android programming from an earlier day, these overviews will be useful introduction to what follows.


So after that rather long-winded introduction, let's get started!

 

Creating the Project

The Google Maps Android API is distributed as part of the Google Play Services SDK. First, check the SDK manager for your installation of Android Studio and be certain that the Google Play services package (under the SDK Tools tab) is installed. If it isn't, check its box on the tab and install it. Maps will not work if your app does not have access to Google Play Services. Further information about insuring that you have access to the correct version of Google Play Services may be found at Setting Up Google Play Services.

Following the general procedure in Creating a New Project, either choose Start a new Android Studio project from the Android Studio homepage, or from the Android Studio interface choose File > New > New Project. Fill out the fields in the resulting screens as follows,


Application Name: MapExample
Company Domain:< YourNamespace >
Package Name: <YourNamespace> .mapexample
Project Location: <ProjectPath> MapExample
Target Devices: Phone and Tablet; Min SDK API 15
Add an Activity: Google Maps Activity
Activity Name: MapsActivity
Layout Name: activity_maps
Title: Map

where you should substitute your namespace for <YourNamespace> (com.lightcone in my case) and <ProjectPath> is the path to the directory where you will store this Android Studio Project (/home/guidry/StudioProjects/ in my case). When the build is complete Android Studio should open the google_maps_api.xml and the MapsActivity.java files in the editor. If you have chosen to use version control for your projects, go ahead and commit this project to version control.

Before beginning actual coding, we must take care of some permissions and authorization issues .

 

Getting an API Key for Google Maps

The first step in developing a mapping application is to obtain an API key that allows access to the Google Maps servers. The instructions for doing this are given in the app/res/values/google_maps_api.xml file created with the project, and also may be found in the Maps Getting Started document. When you have retrieved an API key using these instructions, insert it in the google_maps_api.xml file as instructed by comment statements in that file. Once the API key is inserted, you are ready to test the skeleton mapping app that Android Studio has created.


To keep things simple we shall register using the debug certificate associated with our development machine to obtain a temporary Maps API key. This is adequate for demonstration and development. However, when you publish an app (e.g., deployment through the Google Play Store) you must digitally sign it and (if you employ Google Maps) before you publish your application you must register for a new Key based on your release certificate, and update the references in your app to this new key. More detail may be found at Sign Your App.

This maps API key is valid only under the debug certificate on a specific machine, for as long as that debug certificate is valid (typically 365 days).
  • If you transfer the application to another machine, you will have to obtain a map key for that machine in the same way as described above and change the entry in the google_maps_api.xml file accordingly.

  • Even on a given machine, the debug certificate expires after a year and you must delete the debug.keystore file to force acquisition of a new one (see the General heading under Android Studio Tips).
If you acquire a new debug certificate, your mapping applications will not be able to obtain data when deployed using the debug certificate until you obtain a new maps API key.

 

Testing the Default Mapping Application

It is strongly recommended that you use an actual device, with location services enabled, to develop mapping applications, but it can be done with an AVD if you don't have access to a suitable device.

Once your device is connected or your AVD configured, launch the application using Android Studio. After the build and deployment, you should see a display similar to the following image after pressing on the red marker in the map.



This indicates that the appropriate APIs are being accessed, and that the maps key just downloaded is indeed allowing access to Google mapping tiles.


If you get a blank screen with no map (in this or any mapping application), the most likely culprit is an authorization failure because the mapping API key has been deemed not valid. Check the logcat output for confirmation.

This app already does some useful things beyond verifying the development setup for maps.

We have a solid starting point, so let's develop some more ambitious applications of the Maps API!

 

Configuration of the Manifest File

For simple mapping tasks our app is already adequately configured, but the more ambitious mapping examples in this project will require some additional configuration of the manifest file. A summary of project configuration required when using the Maps API is given in the Project Configuration guide for the Google Maps Android API. For our tasks it is necessary to do the following.

 

Permissions Are Required (But You Don't Have to Add Them!)

Let us now consider some permissions that are either required or recommended so that the network can retrieve map data and display it in our app. We will see below that these are taken care of automatically, so there isn't anything to do yet; just read for your information.

  1. The required permissions for using maps correspond to

    <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    where

  2. The recommended permissions correspond to adding one of the following

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


    Generally only one of these is specified, depending on what level of precision the app requires for location. Simple applications of mapping may not require such permissions, but serious mapping applications will typically be used in conjunction with location services and thus will require one of these location permissions.

  3. In addition, the Google Maps Android API uses OpenGL ES version 2 graphics, so it is recommended to add as a child of <manifest> in the manifest file a uses-feature tag

    <uses-feature android:glEsVersion="0x00020000" android:required="true"/>
    which will prevent loading of the application on a device that does not implement the required level of OpenGL. (With this tag included, Google Play Store won't display the application as a download option for devices that don't have OpenGL ES version 2 installed.)

Because of the Android security model, insertion of these permissions in the manifest file means that the user will be required to give explicit approval for all of the actions implied by the permissions. Prior to Android 6.0, these permissions were given when the application was initially installed on a device. Once installed, the permissions remained in effect, even through any updates of the application, unless the list of permissions changed in an update. In that case, the user was required to authorize the new permission list before the update would install. Implicit in this procedure was that giving permissions was an all or none proposition---the user was not allowed to give some permissions and deny others.

This security model has evolved for Android 6.0 (API 23) and beyond. In the newer model there is more flexibility for the user of an app in granting some permissions and not others, granting of certain permissions ("dangerous" permissions) will shift largely to the first time the permission is needed rather than all at once at installation (runtime permissions), and the user will have the option of revoking a permission at some later time. These changes and the programming required to implement them will be explained further below.

Now for the GOOD NEWS! Although you should be aware of the above permissions requirements for your own edification, or in case you need to understand or modify an app written under older versions of the Maps API (where all these and more had to be inserted by the developer), these permissions have already automatically been taken care of by the Maps API, Android Studio, and Google Play Services!

  1. As already noted above, the WRITE_EXTERNAL_STORAGE permission is no longer required for the Google Play Services later than version 8.3 that we will target.

  2. The INTERNET and ACCESS_NETWORK_STATE permissions are required, but the setup of our app as a Google Maps Activity has already configured the build to merge those permissions, and the optional glEsVersion uses-feature, automatically from the Google Play Services manifest at runtime.

  3. The setup of our app as a Google Maps Activity has already inserted an ACCESS_FINE_LOCATION permission in the manifest file for the app.

Hence, there are no explicit permissions to add to the manifest file. However, there are some additional metadata and Activity declarations to be inserted in the manifest file that we will summarize in the next two subsections.

 

Embedding Google Play Services

Open the manifest file app/manifests/AndroidManifest.xml for the project and add (as a child of <application>) the meta-data tag


<meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" />

which will cause the version of Google Play Services with which the app was compiled to be embedded in the application.


This Google Play Services tag embeds metadata identifying the version of the Google Services client library to which it is bound. When the Google Play Services component is added to an application, the correct version of the Google Play Services client library is downloaded and the component automatically binds to that, instead of the version installed by the Android SDK Manager.

Much of Google Play Services operates under the hood for a mapping application, so we don't need to do much more than specify it in most applications; however, more detailed instructions concerning the Google Play SDK and its usage may be found in the Google Play Services documentation.

 

Declaration of (Fragment)Activities

There is one final set of entries that must be added to the manifest file. We will create a number of FragmentActivities in our application and Android requires that they be declared in the manifest file before they can be used. Although the FragmentActivities have not been created yet, let's go ahead and declare them. Add to the manifest file as children of the <application> tag the <activity> tags


<activity android:name=".MapsActivity" android:label="Default map"> </activity> <activity android:name=".IndoorExample" android:label="Indoor example"> </activity> <activity android:name=".MapMarkers" android:label="Map markers"> </activity> <activity android:name=".ShowMap" android:label="Show Map"> </activity> <activity android:name=".RouteMapper" android:label=""></activity> <activity android:name=".MapMe" android:label="Map Me"></activity> <activity android:name=".Help" android:label="Help" android:theme="@style/MyDialogTheme"> </activity> <activity android:name=".Settings" android:label="Settings" android:theme="@style/MyDialogTheme"> </activity>

(The activity names will be highlighted in red, indicating errors, since they haven't been defined yet. We will fix that later, so ignore it for now.)

This, at long last, completes the initial specification of the manifest file. For reference, the final form of AndroidManifest.xml, with the additions just implemented highlighted in red, should read


<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="<YourNamespace>.mapexample"> <!-- The ACCESS_COARSE/FINE_LOCATION permissions are not required to use Google Maps Android API v2, but you must specify either coarse or fine location permissions for the 'MyLocation' functionality. --> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" /> <!-- The API key for Google Maps-based APIs is defined as a string resource. (See the file "res/values/google_maps_api.xml"). Note that the API key is linked to the encryption key used to sign the APK. You need a different API key for each encryption key, including the release key that is used to sign the APK for publishing. You can define the keys for the debug and release targets in src/debug/ and src/release/. --> <meta-data android:name="com.google.android.geo.API_KEY" android:value="@string/google_maps_key" /> <meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" /> <activity android:name=".MapsActivity" android:label="@string/title_activity_maps" android:theme="@style/AppTheme"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".IndoorExample" android:label="Indoor example"> </activity> <activity android:name=".MapMarkers" android:label="Map markers"> </activity> <activity android:name=".ShowMap" android:label="Show Map"> </activity> <activity android:name=".RouteMapper" android:label=""></activity> <activity android:name=".MapMe" android:label="Map Me"></activity> <activity android:name=".Help" android:label="Help" android:theme="@style/MyDialogTheme"> </activity> <activity android:name=".Settings" android:label="Settings" android:theme="@style/MyDialogTheme"> </activity> </application> </manifest>

where your own package name should be substituted for <YourNamespace> and the identifier @string/google_maps_key will read and insert the 40-character Google Maps API key that was stored earlier in google_maps_api.xml.

 

Creating XML and Java Files, and Adding Resources

This is a large project with various pieces that illustrate a number of ways to use Google Maps in an Android application. Rather than construct it step by step, we shall take the approach of first creating all the XML files, Java files, and resources that will be required. Then we shall describe in some detail the functionality and the way that it is implemented with the underlying code. If you would rather not create all these files by hand, the entire project can be obtained directly from MapExample, with instructions for installing it in Android Studio in Packages for All Projects. In that case you can jump directly to the discussion below of the functionality and how the code implements it.

 

Changing the Initial Activity

Android apps typically consist of a set of activities that call each other. Which activity is called first (the initial entry point) is specified by the AndroidManifest.xml file. In the MapExample project created initially by Android Studio the entry point (specified by the intent-filter tag) is directly into a mapping application defined by the class MapsActivity. We wish to change that so that the initial entry point is a screen with choices that allow various mapping examples to be chosen. So let us first create the Activity that will define the entry screen, and modify the Manifest file so that this activity is called first when the app is launched.

Create a class app/java/<YourNamespace>.mapexample/MainActivity.java (where you must substitute for <YourNamespace> your own package identifier, and a correspondinglayout file app/res/layout/activity_main.xml. Edit the manifest file and change the entry activity from MapsActivity to MainActivity in the activity tag containing the intent-filter, and add an activity declaration for the original class MapsActivity (since it is no longer declared in the manifest once the preceding change is made). These changes to the manifest file are marked in red below:


<activity android:name=".MainActivity" android:label="@string/title_activity_maps" android:theme="@style/AppTheme"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".MapsActivity" android:label="Default map"> </activity> <activity android:name=".IndoorExample" android:label="Indoor example"> </activity>

Now the code in the Java class MainActivity will be executed first when the app is launched. Later we will add commands to MainActivity that implement the desired functionality of the entrance screen, but first let us define a set of XML resources that will be required.

 

XML Resources

Edit the file app/res/values/colors.xml to read


<?xml version="1.0" encoding="utf-8"?> <resources> <color name="barColor">@color/accent</color> <color name="barTextColor">#ffffff</color>color> <!--<color name="buttonColor">@color/primary_light</color>--> <!-- Tint with a palette color--> <color name="buttonColor">#dedede</color> <!-- Tint with very light gray--> <color name="dialogBackground">#eeeeee</color> <!-- Choose one of the following palettes and leave the rest commented out --> <!-- Palette 1 Generated at http://www.materialpalette.com --> <color name="primary">#009688</color> <color name="primary_dark">#00796B</color> <color name="primary_light">#B2DFDB</color> <color name="accent">#448AFF</color> <color name="primary_text">#212121</color> <color name="secondary_text">#727272</color> <color name="icons">#FFFFFF</color> <color name="divider">#B6B6B6</color> <!-- Palette 2 Generated at http://www.materialpalette.com--> <!-- <color name="primary">#FFC107</color> <color name="primary_dark">#FFA000</color> <color name="primary_light">#FFECB3</color> <color name="accent">#536DFE</color> <color name="primary_text">#212121</color> <color name="secondary_text">#727272</color> <color name="icons">#212121</color> <color name="divider">#B6B6B6</color>--> <!-- Palette 3 Generated at http://www.materialpalette.com --> <!-- <color name="primary">#4CAF50</color> <color name="primary_dark">#388E3C</color> <color name="primary_light">#C8E6C9</color> <color name="accent">#FFC107</color> <color name="primary_text">#212121</color> <color name="secondary_text">#727272</color> <color name="icons">#FFFFFF</color> <color name="divider">#B6B6B6</color>--> <!-- Palette 4 Generated at http://www.materialpalette.com --> <!-- <color name="primary">#CDDC39</color> <color name="primary_dark">#AFB42B</color> <color name="primary_light">#F0F4C3</color> <color name="accent">#009688</color> <color name="primary_text">#212121</color> <color name="secondary_text">#727272</color> <color name="icons">#212121</color> <color name="divider">#B6B6B6</color>--> <!-- Palette 5 Generated at http://www.materialpalette.com --> <!-- <color name="primary">#673AB7</color> <color name="primary_dark">#512DA8</color> <color name="primary_light">#D1C4E9</color> <color name="accent">#FFC107</color> <color name="primary_text">#212121</color> <color name="secondary_text">#727272</color> <color name="icons">#FFFFFF</color> <color name="divider">#B6B6B6</color>--> <!-- Palette 6 Generated at http://www.materialpalette.com --> <!-- <color name="primary">#8BC34A</color> <color name="primary_dark">#689F38</color> <color name="primary_light">#DCEDC8</color> <color name="accent">#CDDC39</color> <color name="primary_text">#212121</color> <color name="secondary_text">#727272</color> <color name="icons">#212121</color> <color name="divider">#B6B6B6</color>--> </resources>

Open the strings.xml file under res/values and edit it to define the strings


<resources> <string name="app_name">MapExample</string> <string name="title_activity_maps">MapExample</string> <string name="action_settings">Settings</string> <string name="settings">Settings</string> <string name="locLabel">Location:</string> <string name="goLabel">Go</string> <string name="latLabel">Lat</string> <string name="longLabel">Long</string> <string name="trackLabel">Track Present Location</string> <string name="satLabel">Sat</string> <string name="mapLabel">Map</string> <string name="place_name">Place name</string> <string name="lat_deg">Latitude (o)</string> <string name="lon_deg">Longitude (o)</string> <string name="markerLabel">Map with Markers</string> <string name="indoorLabel">Indoor Map Example</string> <string name="quit">Exit</string> <string name="help_title">Help</string> <string name="satellite_label">Sat</string> <string name="traffic_label">Traffic</string> <string name="track_label">Track</string> <string name="help_text"><p><i>Latitude</i> given in degrees, 0 to +90 above the equator; 0 to -90 below the equator.</p> <p><i>Longitude</i> is measured in degrees from the prime meridian; 0 to -180 West and 0 to +180 East.</p> </string> <string name="settings_text">We omit creating a preferences page here. See the project <i>PrefsFragments</i> for an introduction to constructing a fragments-based preferences page.</string> <string name="building_label">3D</string> <string name="indoor_label">Indoor</string> <string name="routeLabel">Route Overlay on Map</string> <string name="mapme_label">Map Current Location</string> <string name="default_label">Show Default Map</string> <string name="connected_toast">Location services connected</string> <string name="disconnected_toast">Location services disconnected</string> <string name="route_toggle_label">Route</string> <string name="eat_toggle_label">Eat</string> <string name="hc_toggle_label">Access</string> <string name="nomap_error">Error: target map not found</string> </resources>

Next, edit the file app/res/values/styles.xml to read


<resources> <!-- Base application theme. NoActionBar because we are using a Toolbar --> <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> <!-- Set AppCompat actionBarStyle --> <item name="actionBarStyle">@style/AppTheme</item> <item name="colorPrimary">@color/primary</item> <item name="colorPrimaryDark">@color/primary_dark</item> <item name="colorAccent">@color/accent</item> <item name="android:textColor">@color/primary_text</item> <!--<item name="android:windowBackground">@drawable/mapbkg</item>--> <!--Background image--> </style> <style name="MyDialogTheme" parent="Theme.AppCompat.Light.Dialog.Alert"> <!--buttons color--> <item name="colorAccent">@color/accent</item> <!--title and message color--> <item name="android:textColorPrimary">@color/primary_text</item> <!--dialog background--> <item name="android:textColorSecondary">@color/primary_text</item> <!--dialog background--> <item name="android:background">@color/dialogBackground</item> </style> <style name="MyPopupTheme" parent="ThemeOverlay.AppCompat.Light"> <!--buttons color--> <item name="colorAccent">@color/accent</item> <!--title and message color--> <item name="android:textColorPrimary">@color/primary</item> <!--dialog background--> <item name="android:textColorSecondary">@color/primary</item> <!--dialog background--> <item name="android:background">@color/primary_light</item> </style> </resources>

Finally, edit the file app/res/values/dimens.xml to read


<?xml version="1.0" encoding="utf-8"?> <resources> <!-- Default screen margins, per the Android Design guidelines. --> <dimen name="activity_horizontal_margin">16dp</dimen> <dimen name="activity_vertical_margin">16dp</dimen> <dimen name="button_vertical_margin">16dp</dimen> <dimen name="button_horizontal_margin">50dp</dimen> </resources>

Notice that by by defining string resources in strings.xml. dimension resources in dimens.xml, color resources in color.xml, and style resources in styles.xml, we are following Android best practices of separating these format resource definitions from the Java code implementation.

 

XML Layout Files

Let us now create and edit the files that will define the layouts of the screens in our application. Open the layout file app/res/layout/activity_main.xml and edit it to read


<?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/scrollView1" android:layout_width="match_parent" android:layout_height="wrap_content" > <LinearLayout android:id="@+id/LinearLayout1" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="0dp" android:paddingRight="0dp" android:paddingTop="0dp" tools:context=".MainActivity" > <android.support.v7.widget.Toolbar android:id="@+id/my_toolbar2" android:layout_height="wrap_content" android:layout_width="match_parent" android:elevation="10dp" android:minHeight="?attr/actionBarSize" android:background="@color/barColor" /> <LinearLayout android:layout_width="fill_parent" android:layout_height="fill_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:orientation="horizontal" > <EditText android:id="@+id/geocode_input" android:layout_width="0dip" android:layout_height="wrap_content" android:layout_weight="1.0" android:hint="@string/place_name" android:inputType="text" android:lines="1" /> <Button android:id="@+id/geocode_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/goLabel" /> </LinearLayout> <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:orientation="horizontal" android:padding="0sp" android:layout_marginLeft="10dp" android:layout_marginRight="10dp"> <EditText android:id="@+id/lat_input" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="5sp" android:layout_weight="0.35" android:hint="@string/lat_deg" android:inputType="numberDecimal|numberSigned" android:lines="1" android:layout_marginLeft="10dp" android:layout_marginRight="5dp" /> <EditText android:id="@+id/lon_input" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="5sp" android:layout_weight="0.35" android:hint="@string/lon_deg" android:inputType="numberDecimal|numberSigned" android:lines="1" android:layout_marginLeft="10dp" android:layout_marginRight="5dp" /> <Button android:id="@+id/latlong_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="5sp" android:layout_weight="0.25" android:text="@string/goLabel" android:layout_marginLeft="10dp" android:layout_marginRight="5dp" /> </LinearLayout> <LinearLayout android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" android:paddingLeft="@dimen/button_horizontal_margin" android:paddingRight="@dimen/button_horizontal_margin" android:paddingTop="10dp"> <Button android:id="@+id/honolulu_button" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:layout_marginTop="10sp" android:text="@string/markerLabel" /> <Button android:id="@+id/indoor_map_button" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:layout_marginTop="10sp" android:text="@string/indoorLabel" /> <Button android:id="@+id/route_map_button" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:layout_marginTop="10sp" android:text="@string/routeLabel" /> <Button android:id="@+id/mapme_button" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:layout_marginTop="10sp" android:text="@string/mapme_label" /> <Button android:id="@+id/default_button" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:layout_marginTop="10sp" android:text="@string/default_label" /> </LinearLayout> </LinearLayout> </ScrollView>

The documentation explaining the components of this layout may be found at ScrollView, LinearLayout, EditText, and Button, but briefly

Thus, reading vertically, activity_main.xml should lay out a row containing a textfield and a button arrayed across the entire screen, then a row with two textfields and a button arrayed across the entire screen, and then below that a column of five buttons centered on the screen. All of that is contained within a ScrollView, which has no effect if the widgets all fit on the screen but will allow scrolling to access all widgets if they do not.

Let's check our progress. Execute MapExample on a device or properly-configured emulator. You should see something like the following image



Notice that the XML attributes used in laying out this page have given certain capabilities and placed certain restrictions on text fields automatically. The first text field is set up to accept general text, but the other two text fields will accept only decimal numbers, possibly with a negative sign. Try it and see; you should find that you can type anything into the first field, but only digits, a single period, and a minus sign (only if it is the first entry) in the other two fields.

We have also indicated with the android:hint attributes further guidance for the user. For example, android:hint="@string/lat_deg" references the string lat_deg = "Latitude (o)" defined in strings.xml, which inserts initially in the corresponding field a hint that the user is to enter the latitude in degrees (this hint will disappear as soon as the user begins to type into the field). When the hints are displayed in the text fields, the actual content of the field is the empty string, which is the reason for the logic statements in the event handlers defined later that do not launch the intents if the fields have not been filled in.



If you execute on a device it is likely that autocompletion will be enabled on the first text field. For example, if I type the string "liverp" into the first field on a Nexus 6 phone I get




which illustrates text completion in the first EditText field. For example, if we were entering the word "Liverpool", it could be selected off the bar above the keyboard at this point, without typing further.

Automatic text completion in an EditText field can be implemented using the class AutoCompleteTextView, which extends EditText. It requires that the programmer supply the list of words against which the autocompletion will be checked. Most modern phones would have the EditText fields automatically autocomplete against a basic dictionary. However, Android provides the option of providing your own implementation of AutoCompleteTextView.

Now we create XML files to lay out a series of screen views that we shall need.

Now let us define a set of layouts that will be intended to display maps using the class MapFragment, which extends Fragment to include map management capabilities and represents the simplest way to insert a map in an Android application.

That completes the XML layout specifications.

 

XML Menu Files

Now let us create some additional XML files that will specify the layout of some menus that will be implemented on various screens.

That completes the XML files required to implement the menus.

 

Image Resources

We shall need some bitmap image resources. The four images required may be downloaded from the images resource page.

  1. Go to this link with a browser and download into the app/src/main/res/drawable subdirectory (for example, the full path would be /home/guidry/StudioProjects/MapExample/app/src/main/res/drawable on my system) of this project the files


  2. Then right-click on res/drawable in the project pane and select Synchronize 'drawable'. You should then see the new image resources under MapExample/res/drawable, as illustrated in the following figure.




  3. Alternatively, you can download the images to some directory on your computer and copy and paste them directly into drawable in the Android Studio project pane.

Image resources for Android are placed in the Drawable directories and must be named by strict rules: you can use only lower-case letters a-z, numbers 0-9, _ and . in these image file names. The preferred format is .png, but .gif and .jpg can be used. General design guidelines for image resources in Android may be found in the Icon Design Guidelines document.

This project was under version control with Git. If the image files are pasted directly into Android Studio it asks after each if you would like to add it to version control. On the other hand, if the files are copied manually into the directory and then synced with Android Studio they will not be under version control. In this case they may be added manually to version control by right-clicking on the file name and choosing Git > Add from the resulting context menu.

 

Java Class Files

Our final task is to define the code that will implement our app in a series of Java class files.

That completes the input of code and resources for the project.

 

What it Does and How it Does it

Let's now see how our application works and discuss how that functionality is implemented by the code we have just created. For purposes of discussion, we shall assume the application to be launched on a device having location services enabled, since testing location services using an emulator is possible but awkward. (See Simulating GPS Position for methods to simulate positioning at specific GPS coordinates if you must test on an emulator rather than actual device.) Launch the application on the device, which will give the entry screen already shown above:



 

Sending the Map to Specific Latitudes and Longitudes

If you insert latitude and longitude values in degrees in the second set of input fields and push the corresponding Go button, the map display screen should center on the location corresponding to the input latitude and longitude. For example, try

where you may have to change the map zoom for the best view.


As an aside, here is one way to get precise longitude and latitude coordinates for arbitrary locations: Using Google Maps on your computer, navigate to the desired area, zoom to the desired level of precision, and click on the desired point. This should display a box with the latitude and longitude of the point. For well-known places, you can also just search for their latitude and longitude with the browser: type "latitude longitude Big Ben" into a browser, for example. Or the more modern equivalent on your phone: "OK Google, what is the latitude and longitude of the Eiffel Tower?"

The Toolbar at the top of the display displays five options:

  1. Sat, which toggles the satellite view on and off.

  2. Traffic, which toggles the traffic view on and off.

  3. Indoor, which toggles indoor maps (where available) on and off.

  4. 3D, which toggles the 3D view of buildings on and off.

  5. Settings, which links to a settings and preferences page.

How many of these are displayed in the Toolbar at the top and how many are displayed in the overflow menu accessed by clicking the three vertical dots at the right of the Toolbar depends on our indicated preference in the programming and on the device. Our programming requests that the first four items be displayed in the top bar, if there is room, but for the last one to always be in the secondary overflow menu.

Then, for example, on a Galaxy Nexus phone with a 4.7" screen only the first two are displayed on the top bar (and the last three are relegated to the overflow menu) in portrait mode, and the first three are displayed on the top bar in landscape mode with two relegated to the overflow menu, but on a Nexus 7 tablet with a 7" screen the first four are displayed in the top bar in either portrait or landscape mode. The figure below shows the Toolbar and overflow menu for the Galaxy Nexus in landscape mode.



The figure below shows the satellite view toggled on (by clicking SAT in the Toolbar).



The figure below shows the traffic view toggled on (with green bands indicating little traffic congestion in either direction on the routes displayed).



The Indoor toggle is relevant only if one is zoomed in on a building that has an indoor map registered with Google Maps. We shall discuss examples below.

The 3D toggle displays buildings in 3D outline (if available). For this view we also tilt the camera view of the map to an angle, rather than from directly overhead where we have it set for the other map views. An example of a 3D view is shown in the figure below.



The screen displayed by the Settings option (in the overflow menu of the Toolbar) looks like the following:



As indicated in the display, this is just a placeholder for a Settings interface, which we shall discuss in more depth in the project Dialogs, Alerts, and Notifications.

 

Geocoding

Now let's make it even simpler for the user to send this mapping application to a location of interest. It would be much easier to just type in "Mount Fuji", or "1516 Android Way", or "The Kremlin", rather than trying to figure out the latitude and longitude coordinates for locations. The Android/Google API in fact has classes that allow us to implement that capability with just a few lines of code. The most important resource is the Android Geocoder class, which is a class for handling geocoding and reverse geocoding:

Try entering the locations "hoover dam" or "brooklyn bridge" or "little mermaid" or "ord" (the airport identifier for Chicago O'Hare Airport) in the top input textfield and clicking Go. For example, the first two options executed on my Galaxy Nexus phone give the following figures.



In the preceding examples, if you check the diagnostics sent to logcat you will see that in each of these cases a unique site was returned. But in general, a given name may correspond to more than one location. For example, if we enter the address 1101 Shevlin Drive (a street address where I once lived), the messages sent to the logcat output with Log.i() gave


06-15 13:16:19.592 I/Location-List( 8581): Raw String: 06-15 13:16:19.592 I/Location-List( 8581): Address[addressLines=[0:"1101 Shevlin Dr",1:"El Cerrito, CA 94530",2:"USA"],feature=1101,admin=California,sub-admin=Contra Costa,locality=El Cerrito,thoroughfare=Shevlin Dr,postalCode=94530,countryCode=US,countryName=United States,hasLatitude=true,latitude=37.91892,hasLongitude=true,longitude=-122.294027,phone=null,url=null,extras=null] 06-15 13:16:19.592 I/Location-List( 8581): Address[addressLines=[0:"1101 Shevlin Dr",1:"Wayzata, MN 55391",2:"USA"],feature=1101,admin=Minnesota,sub-admin=Hennepin,locality=Wayzata,thoroughfare=Shevlin Dr,postalCode=55391,countryCode=US,countryName=United States,hasLatitude=true,latitude=44.964079,hasLongitude=true,longitude=-93.5790595,phone=null,url=null,extras=null] 06-15 13:16:19.592 I/Location-List( 8581): Options: 06-15 13:16:19.592 I/Location-List( 8581): (1) 1101 Shevlin Dr, El Cerrito, CA 94530, United States 06-15 13:16:19.592 I/Location-List( 8581): (2) 1101 Shevlin Dr, Wayzata, MN 55391, United States

So two options have been returned, one in El Cerrito, California, and one in Wayzata, Minnesota. With the way we have set the code up, it will always send the map to the first option (which is my old address in this case; I lived in California, not Minnesota). We can see from this output that it would not be too difficult to use the information that has been extracted to present to the user a choice list (already contained in the array optionArray) when there is more than one option, so that the user can select the correct one. That is left as an exercise.


It would be good practice in an actual applications to call geocoding (and reverse geocoding discussed below) on a thread separate from your primary UI thread (see the Geocoder documentation). The getFromLocationName and getFromLocation methods of Geocoder throw both IllegalArgumentException and IOException. These could be caught and used to make a more user-friendly application (we are already catching the IOException, but not using it except for diagnostic output).

 

Street Views

If one goes to a location using either explicit coordinates or geocoding and long-presses on the map returned, a Google Street View is launched on that location. For example, typing in "golden gate bridge" gives a map view of the center of the Golden Gate Bridge. A long press on the bridge roadway returns a 360 degree Street View from the middle of the bridge, as illustrated in the following image.



Since one is now in Street View, its full capabilities to pan the image in any direction are available. To return to our app, hit the Back button (back arrow at bottom, typically) on the device.

 

Indoor Street Views

Many places now have indoor Street Views (there is a procedure for individuals to produce and upload Street Views). For example, type into the geocoding field (the top one on the main interface) "1434 California Street, Denver" and click Go. In the resulting image, long-press on the nearest (small) building to the map center. You should obtain the following indoor Street View of the Bubba Gump Shrimp Company:



Notice in the lower left the buttons that allow selection of Street Views for the first or second floor of the building.

 

The Code in MainActivity.java

The code implementing the functionality of the preceding two subsections is contained in MainActivity.java and ShowMap.java. As should be familiar from previous applications, in MainActivity we extend AppCompatActivity (a subclass of Activity) and implement the OnClickListener interface, which requires that we override the method OnClick (View v) to handle clicks on the view. In the onCreate method we

A Menu can be instantiated from an XML file using MenuInflater; see Menus and the discussion of Menus in Android User Interfaces for more details. In the method onCreateOptionsMenu we use the getMenuInflater method of Activity to return a MenuInflater, and then use its inflate() method to populate the Toolbar menu at the top (with layout specified by res/menu/main.xml).

The method onOptionsItemSelected is then used to process clicks on the items added to the Toolbar by using a switch statement to decide which button was clicked, and the method onClick is used in a similar way to decide which button in the main layout was clicked. The corresponding actions are generally implemented by Intents that launch new classes using the startActivity method of Activity.

The geocoding example is implemented by the method geocodeLocation, which instantiates the class Geocoder as the instance gcoder and uses that in conjunction with an Iterator to populate a List containing the possible matches (Address is an Android class that represents a location address as a set of strings). For our purposes in this example, we simply use the first entry in the list returned by the geocoder and send the map to that location. In a realistic application one would want to display the list and permit the user to choose among the possibilties.

 

Mapping Code in ShowMap.java

The code in ShowMap.java is representative of how maps are embedded in this and other classes in this project. The class extends FragmentActivity, which is a subclass of Activity. Our maps are embedded using a MapFragment, which extends Fragment.



A Fragment represent a piece of an application's user interface that can be placed inside an Activity. This leads to flexible layout deployment across devices with different formfactors. One interacts with a fragment through a FragmentManager, which can be obtained using Activity.getFragmentManager() or Fragment.getFragmentManager().

A fragment may be thought of as a modular piece of an activity, which
  • has its own lifecycle (but its lifecycles are dependent on those of its Activity),

  • receives its own input events, and

  • can be added or removed while the activity is running, but which

  • cannot be used apart from its Activity.
Thus a fragment is a kind of sub-activity that decomposes the functionality of an activity into modules that are reusable across different activities. For more details, see the Fragment Developer Document.

A MapFragment is a wrapper around a map view that handles the necessary life cycle needs. It represents the simplest way to embed a map in an application. As we have seen in res/layout/showmap.xml, a map fragment can be embedded simply in the layout as


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.v7.widget.Toolbar android:id="@+id/my_toolbar" android:layout_height="wrap_content" android:layout_width="match_parent" android:elevation="0dp" android:minHeight="?attr/actionBarSize" android:background="@color/barColor" /> <fragment xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/the_map" android:name="com.google.android.gms.maps.SupportMapFragment" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.lightcone.mapexample.ShowMap" tools:layout="@layout/showmap" /> </LinearLayout>

Then a reference to this map fragment can be obtained in the following way.


SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager() .findFragmentById(R.id.the_map); mapFragment.getMapAsync(this);

ShowMap extends AppCompatActivity, which extends FragmentActivity, which extends Activity. In the first statement above the method getSupportFragmentManager(), which ShowMap inherits from FragmentActivity, returns a FragmentManager, and the findFragmentById(R.id.the_map) method of the FragmentManager returns a Fragment corresponding to the fragment identified by the android:id="@+id/the_map" attribute in showmap.xml. This Fragment can then be cast to a SupportMapFragment (which extends Fragment). Thus, the variable mapFragment now holds a reference to our map fragment of type SupportMapFragment .

However, this map fragment is not manipulated directly. Instead we call in the second statement above the getMapAsync (OnMapReadyCallback callback) method of SupportMapFragment, which takes as an argument a callback method that will be triggered when the map is ready for interaction. But ShowMap implements the OnMapReadyCallback interface and hence must implement its method onMapReady (GoogleMap googleMap) [Hence the reference to the present (ShowMap) object, this.] This is a callback triggered by the system when the map is ready to use, and it passes in a reference googlemap to the GoogleMap.

Once the GoogleMap is available (the callback onMapReady has fired), we initialize it in the method initializeMap() by using various methods of the GoogleMap class. For example, in the chained expression


map.getUiSettings().setRotateGesturesEnabled(true);

the GoogleMap method getUiSettings() returns a UISettings instance containing the settings of the current map user interface and then the setRotateGesturesEnabled (boolean) method of UISettings enables rotation gestures on the map (the orientation of the map can be rotated by touching with two fingers and twisting).

The static method putMapData() is used by other classes to specify map data before they use ShowMap.class to display a map.

The method changeCamera() uses methods of the classes

to define new views for the camera view of the map, and then the camera is moved using the GoogleMap methods

where the CameraUpdate required in the arguments is generated by CameraUpdateFactory through its newCameraPosition() method.


Let us also note that location services have been implemented in ShowMap.java. At present they are used only to log a statement giving the current latitude and longitude of the user when ShowMap starts, but their implementation is included for potential additional future capability. (For example, the position of the observer could be used to determine the distance to a point accessed by geocoding, or to determine an optimal route from the observer to that point.) Because determining location is a "dangerous" permission, this also requires the implementation of runtime permissions in ShowMap. Implementing location services and runtime permissions will be discussed in detail below for the class MapMe, and a similar discussion applies to location services and runtime permissions in ShowMap.

 

Implementing StreetViews

Launching Google StreetViews in ShowMap.java is rather straightforward. We have implemented a long-press listener and used long-press events to extract the latitude and longitude and then passed that to an Intent defined in the method showStreetView(double latitude, double longitude).


Display and manipulation of map fragments in other classes of this project will use similar approaches as described above. A more extensive discussion of managing map views may be found in the developer guide Camera and View.

 

Maps and Markers

Not only can one do very powerful things with maps themselves, but it is possible to overlay additional location-context information on an arbitrary map. Let's introduce the concept by simply displaying a map and placing location-specific markers on it. Launch the app on a device having location services and click the button Map With Markers. This generates a display as follows:



which shows a map centered on Honolulu, Hawaii. Standard map zoom controls (lower right) and a control to center the camera on the current location of the user (upper right) are displayed. The Toolbar at the top displays options that we have already described above, so you can toggle satellite view and traffic view (displayed in the top Toolbar), and also 3D and indoor (hidden in the overflow menu that can be accessed by clicking the three vertical dots at the right of the Toolbar).

In addition, three markers in different colors are illustrated, inserted with programming that will be descibed below. If one clicks on a marker the map centers on that marker and an information window is displayed, as illustrated in the following image.



If one clicks on the displayed window, a webpage is launched with information about the location indicated by the marker.



The code that accomplishes this is all contained in the class file MapMarkers.java, which will now be described.

 

The Code in MapMarkers.java

Much of the map manipulation in MapMarkers.java is similar to that described above for ShowMap.java. The basic new ingredients involve the addition of Marker objects with properties specified through instances of MarkerOptions.


For an overview of using markers in Google Maps, see the Markers developer guide and the code samples referenced there.

The method addMapMarkers() illustrates the basic technique of adding a marker to the map with a characteristic code segment of the form


private final LatLng diamond_head = new LatLng(21.261941,-157.805901); map.addMarker(new MarkerOptions() .title("Diamond Head") .snippet("Extinct volcano; iconic landmark") .position(diamond_head) .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_VIOLET)) );

The GoogleMap method addMarker (MarkerOptions) adds a marker to the map with the properties specified by the argument of type MarkerOptions. In the argument of addMarker in the above expression the constructor is used to create a MarkerOptions object, which is then chained with MarkerOptions methods such as title to set the properties:

If the argument of icon(BitMapDescriptor) is null the default icon is used. In the above example we have instead used the static method defaultMarker(float hue) of BitmapDescriptorFactory to create a BitmapDescriptor of a custom icon by changing the color of the default icon to that specified by the constant HUE_VIOLET of the BitmapDescriptorFactory class.


Greater levels of marker customization than demonstrated here (including substitution of your own icons specified by resource images) are possible using other methods of BitmapDescriptorFactory. Some of these will be illustrated in the class RouteMapper.

 

Indoor Maps

A useful feature of Google Maps is the ability to view indoor maps of many facilities. One does not really have to do anything special to accomplish this, except to set the variable map.setBuildingsEnabled(Boolean) to true. Clicking the button Indoor Map Example provides an example. This is an ordinary map view, but under sufficiently high zoom (if INDOOR is toggled on in the Toolbar) an indoor map of the Honolulu International Airport appears in place of the normal view, with an option to display the first or second floor:



If you leave INDOOR on and toggle SAT on in the Toolbar (and zoom out one level for perspective), the resulting image changes to



which shows the indoor map in relation to the actual view of the airport from directly overhead.


An overview of indoor maps may be found in the Indoor maps document. In particular, you can find there
  1. a comprehensive list of buildings and structures around the world having an indoor map registered with Google,

  2. general guidelines for producing your own indoor map of a facility, and

  3. instructions to upload your floorplan.
You can investigate some of the indoor maps registered with Google by
  1. choosing a name from the comprehensive list,

  2. using the geocoding functionality of the present app that we described above to center the map on the location,

  3. toggling INDOOR on in the Toolbar or its overflow menu, and

  4. zooming in until the indoor map appears. (If you also toggle on SAT, you will see the indoor view inset in the surrounding satellite view.)
Notice also the StreetView functionality implemented below, which permits retrieving a view of the exterior of these facilities.

Between indoor maps and external 360-degree StreetViews, it is often possible to get a reasonable feeling for the appearance of a facility without ever going there.

 

Coding in IndoorExample.java

The coding implementing the indoor maps example is contained primarily in IndoorExample.java. Since indoor maps are handled largely in the same way as normal maps (except that we should set the boolean to true in the GoogleMap method setIndoorEnabled(boolean)), the coding in IndoorExample.java contains no significant features that are not already described in our discussion of the other classes in this project.

 

Custom Map Markers and Overlays

Clicking on the button Route Overlay On Map should bring up a map display centered on the University of Tennessee Knoxville campus (which should display a transient Toast indicating that location services have been initialized).



In this example we have replaced the text labels in the Toolbar from earlier examples with more compact image symbols: reading from left to right,

Because of the more compact display of the icons relative to text, all four options that we indicated a desire to be visible on the Toolbar (if possible) are displayed, even on a phone in portrait mode. Clicking both the knife/fork and wheelchair icons gives the following display:



Note: the food and handicapped accessible symbols are only representative examples of how to do this and are placed at arbitrary locations; they don't necessarily indicate services available at those specific points at this location.

Clicking the icons on the Toolbar again will toggle the symbols displays off. If instead one clicks on one of the icons an information window opens, as in the following example,



with a programmer-specified title and snippet of information. If one clicks on the map background the information window closes but a click on the information window instead will launch a webpage. In this example, the title, snippet of information, and linked website for the window are just place-holder examples, but clearly this provides a method to identify locations on the map with a short description, and a link to a website with more information.

If one clicks on the walking-path symbol in the Toolbar the device sends to a server the beginning and ending points of a desired path and receives in return the path from a server and displays the route on the map, as indicated below:



(Note that in this basic example we have not added the capability to remove the path overlay from the map once added. In a realistic application one would typically want to add this capability but we leave that as an exercise.)


The file returned for this example is simply a static XML file residing on the server that specifies an optimal walking path between two buildings on campus (the Department of Art and Architecture and the Department of Physics and Astronomy, to be specific). In a realistic application the server would compute the optimal path from the end-point information sent from the mobile device and send the XML specifying the computed path to the mobile device. Since we are concerned here only with the mobile-device side of the problem, it is irrelevant for us how the XML was generated on the server; we just need to know how to interepret and display the corresponding results.

The path displayed also is color-coded for slope, based on information returned from the server, with flatter portions in blue and the steepest portions in red. This information would be relevant, for example, if our application was displaying optimal paths to get from one location to another for persons who could walk only with difficulty, or who required use of a wheelchair.

There is additional information returned in the XML route information about potential hazards such as areas of construction. That information has been displayed as two red markers on the map. If a marker is clicked, information is displayed in a window. For example,



indicating that there is heavy traffic where the path crosses a street. The XML defining the sample route that is returned by the server corresponds to


<?xml version="1.0" encoding="utf-8" standalone="yes"?> <number numpoints="23" numwpts="2"></number> <wpt lat="35952967" lon="-83929158" description="Construction"></wpt> <wpt lat="35955038" lon="-83929126" description="Heavy traffic"></wpt> <trk> <trkseg> <trkpt lat="35952967" lon="-83929158" grade="1"></trkpt> <trkpt lat="35954021" lon="-83930341" grade="1"></trkpt> <trkpt lat="35954951" lon="-83929075" grade="1"></trkpt> <trkpt lat="35955038" lon="-83929126" grade="4"></trkpt> <trkpt lat="35955203" lon="-83928973" grade="1"></trkpt> <trkpt lat="35955212" lon="-83928855" grade="1"></trkpt> <trkpt lat="35955603" lon="-83928273" grade="2"></trkpt> <trkpt lat="35955807" lon="-83928369" grade="1"></trkpt> <trkpt lat="35955974" lon="-83927943" grade="1"></trkpt> <trkpt lat="35956063" lon="-83927720" grade="1"></trkpt> <trkpt lat="35956291" lon="-83927358" grade="1"></trkpt> <trkpt lat="35956471" lon="-83927229" grade="1"></trkpt> <trkpt lat="35956541" lon="-83927176" grade="2"></trkpt> <trkpt lat="35956397" lon="-83927044" grade="3"></trkpt> <trkpt lat="35956274" lon="-83926685" grade="1"></trkpt> <trkpt lat="35956213" lon="-83926642" grade="1"></trkpt> <trkpt lat="35956239" lon="-83926261" grade="1"></trkpt> <trkpt lat="35956202" lon="-83925722" grade="1"></trkpt> <trkpt lat="35956226" lon="-83925467" grade="1"></trkpt> <trkpt lat="35956343" lon="-83925502" grade="1"></trkpt> <trkpt lat="35956324" lon="-83925617" grade="1"></trkpt> <trkpt lat="35956445" lon="-83925379" grade="1"></trkpt> <trkpt lat="35956567" lon="-83925450" grade="1"></trkpt> </trkseg> </trk>

which is available in the file sampleRouteXML.xml, or by pasting the address

   http://eagle.phys.utk.edu/reubendb/UTRoute.php?lat1=35952967&lon1=-83929158&lat2=35956567&lon2=-83925450/span>

into a browser and then viewing the page source of the page returned by the browser (in either case the browser display will probably give an error unless it is set specifically to render XML, but look at the page source which should display the raw XML).

 

GoogleApiClient

The various pieces of this application use Google Play Services. The primary entry point for Google Play Services integration is GoogleApiClient, which is implemented in conjunction with

For an overview, see Accessing Google APIs.

The Google API Client provides a common entry point to all the Google Play services and manages the network connection between the user's device and each Google service. Schematically, the relationship is illustrated in the following figure.


A GoogleApiClient is created using GoogleApiClient.Builder as follows.


private GoogleApiClient locationClient; locationClient = new GoogleApiClient.Builder(this) .addApi(LocationServices.API) .addConnectionCallbacks(this) .addOnConnectionFailedListener(this) .build();

The first 'this' in the arguments is the present context; the next two indicate that the present class will handle the callbacks associated with connection and connection errors, respectively (see the onConnected, onDisconnected, and onConnectionError callbacks in Routemapper.java). You cannot use the Google API service until the onConnected callback fires, indicating a valid connection.

 

Coding in Routemapper.java

The class Routemapper extends AppCompatActivity and must implement a number of interfaces to achieve its functionality:


public class RouteMapper extends AppCompatActivity implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, GoogleMap.OnMarkerClickListener, OnMapReadyCallback, GoogleMap.OnInfoWindowClickListener, GoogleMap.OnMapLongClickListener, com.google.android.gms.maps.GoogleMap.OnMapClickListener {

These include

The food and handicap access markers are handled much as in MapMarkers.java, except that the relevant quantities are placed in arrays here so that they can be processed as groups. We implement in the method onMarkerClick(Marker marker) the default behavior, which causes an info window containing the information stored in the title and snippet arrays to be displayed. In addition, we process clicks on the open info window of a marker using the onInfoWindowClick(Marker marker) method. In that case, we identify the marker through its title and take appropriate action. In the present example, this just consists of opening some arbitrary web pages, but in an actual application these would be pages providing additional information about the location of the clicked symbol. For example, if the symbol is a food symbol representing the location of a restaurant, clicking on the marker would open the info window with the name of the restaurant and a snippet of information about it, and clicking on that window could open the full webpage of the restaurant or other appropriate links.

In the class Routemapper extensive use is made of custom icons, both for the buttons of the Toolbar (where all the displayed buttons are represented by icons without text), and for the food and handicap access overlay markers. The required icon bitmaps are stored in res/drawable. For the Toolbar they are accessed by appropriate references in the menu layout file res/menu/routemapper_menu.xml. For example,


<item android:id="@+id/route_toggle" android:icon="@drawable/route_icon" android:orderInCategory="10" android:showAsAction="always" android:title="@string/route_toggle_label"/>

where the icon property shown in red references the image file res/drawable/route_icon.png. For the markers, the images to use for the markers is specified by the icon method of MarkerOptions, as illustrated by the line marked in red in the following code


for(int i=0; i<numberAccessMarkers; i++){ accessMarker[i] = map.addMarker(new MarkerOptions() .position(accessLoc[i]) .icon(BitmapDescriptorFactory.fromResource(R.drawable.accessibility)) .draggable(false) .alpha(0.7f) .snippet(accessMarkerSnippet[i]) .title(accessMarkerTitle[i])); accessMarker[i].setVisible(accessInitiallyVisible); }

where the static fromResource() method of BitmapDescriptorFactory has been used to specify the bitmap resource res/drawable-hdpi/accessibility.png.

The retrieval of a route as XML from a server is handled on a background thread using AsyncTask. (A general discussion of processes and threaded programming in Android may be found in the Processes and Threads developer guide.) This is implemented in the inner class RouteLoader (which extends AsyncTask), and is well documented in the code comments for that class.


AsyncTask is an abstract class that must be subclassed to be used. An asynchronous task is defined to be one that runs on a background thread but publishes its results on the main UI thread. The class AsyncTask permits this to be done in rather blackbox fashion, since it performs background operations and publishes results on the UI thread without the user having to manipulate threads and/or handlers directly (that is being done for you under the hood).

The parsing of the XML stream in the inner class RouteLoader is handled primarily by XmlPullParser and XmlPullParserFactory, with the parsing API documentation and usage example given at the XML Pull Parsing site. These are used to parse the incoming XML and put the information defining the route into arrays routePoints[] and routeGrade[] and the information defining the warning waypoints in warnPointLatLng[] and warnPointSnippet[]. The route is then drawn segment by segment and the warning waypoints added by using loops over these arrays in the method overlayRoute(), with Polyline and PolylineOptions used to draw the route as a series of color-coded segments and Marker used to deploy the warning waypoints on the map.

 

Tracking Current Location

Click on the button Map Current Location. Once Location Services are started (indicated by a Toast) the map center should animate to your current location (indicated by a blue dot), as illustrated in the following figure:



You can test that location services are indeed active if you drag the map to the side. You should find that it quickly re-centers on the present location, since the location services are supplying position updates continuously.

The items in the Toolbar and its overflow dropdown (SAT, TRAFFIC, ...) are the same as described earlier, except for the TRACK button, which toggles location tracking on and off. As long as it is not toggled off, if you move with the device the blue dot should turn into an arrow indicating direction of motion and the map should animate to track you, with your position always in the center, the direction of the arrow indicating the direction of motion, and with the orientation of the map display changing continuously to orient the top of the map in the direction of motion (if you are moving at a speed greater than 1 meter/second).

Disable location tracking by clicking the TRACK toggle on the Toolbar so that you can drag the map to new positions without the location tracker immediately returning it to your present location. If you tap at any point on the map, you should find that a brief Toast appears showing the latitude and longitude for that point. If instead you long-press on a location, you should get a popup window indicating the address of the location (obtained by capturing the latitude and longitude coordinates and doing a reverse geocode on those coordinates).



The location of the marker indicates the position and the cirle around the base of the marker indicates the present uncertainty in determining the position (there is a little uncertainty in the above image because the phone was indoors when this shot of its screen was taken). As indicated in the window, if you tap the marker it and its window will be deleted, but if you tap the marker's information window the application will attempt to open a Google Street View on the location. If no Street View exists for this particular location you will just get a blank screen, but if a Street View exists it will be displayed, as in the following figure.



In this case our app has used an Intent to launch a separate application (typically Street View, if it is installed on the device, or a browser displaying a Street View). Thus, the controls now available are those of Street View. You should be able to return to our application by clicking the Android Back button.

 

Runtime Permissions

For Android 6 (API 23) and beyond, we must check for "dangerous" permissions such as ACCESS_FINE_LOCATION at runtime (in addition to declaring it in the manifest file, as has always been the case). The code in MapMe.java will require runtime permissions because it uses the "dangerous" permission ACCESS_FINE_LOCATION. We shall now discuss code that checks for this permission. If it has already been granted, it proceeds as normal. If it has not yet been granted by the user, the user is presented with an opportunity to grant it. In general, the rest of this class will not execute until the user has granted such permission.

Since the class MapMe will not execute without the runtime permission, we go ahead and seek it upfront. After the map has been returned in onMapReady(GoogleMap googlemap), the method checkForPermissions( ) is executed:


public void checkForPermissions( ){ if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { /* Permission has not been granted by user previously. Request it now. The system will present a dialog to the user requesting the permission, with options "accept", "deny", and a box to check "don't ask again". When the user chooses, the system will then fire the onRequestPermissionsResult() callback, passing in the user-defined integer defining the type of permission request (REQUEST_LOCATION in this case) and the "accept" or "deny" user response. We deal appropriately with the user response in our override of onRequestPermissionsResult() below.*/ ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, REQUEST_LOCATION); } else { // Do what you would do if this check had not been made. } }

In this code, if the initial check implemented by ActivityCompat.checkSelfPermission (which ActivityCompat inherits from ContextCompat) is passed, the system has determined that the user has given this runtime permission previously for this app. Hence, we may proceed as if the check had never been made.

It is a little more complicated if the test is not passed. Then we wish for the system to present a dialog to the user asking them to accept the permission. This is accomplished by invoking the ActivityCompat.requestPermissions method, which takes as arguments the permission to be requested (ACCESS_FINE_LOCATION in this case), and an integer chosen by the programmer to identify this request (REQUEST_LOCATION, which we have chosen to be equal to 2 in MapMe.java). The system launches a dialog asynchronously, with the choices shown below.



After the user responds, the system triggers the callback onRequestPermissionsResult, passing in the results of the user's response. We have overridden this callback to deal with the response, as shown below.


public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { switch(requestCode) { case REQUEST_LOCATION: // Permission response was for fine location // If the request was canceled by user, the results arrays are empty if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){ // Permission was granted. Do the location task that triggered permission request initializeLocation(); } else { // The permission was denied. Warn the user of the consequences and give // them one last time to enable the permission. showTaskDialog(1, "Warning!", "This part of the app will not function without this permission!", dialogIcon, this, "OK, Do Over", "Refuse Permission"); } break; // Additional cases here if more than one permission will be requested. } }

In this callback the system passes back to us in the variable requestCode the integer that we passed to it to identify the permission request. The switch statement is because in the general case more than one permission might be requested (for example, to determine fine location and to read the user's contacts), each identified by a different user-chosen integer. This logic isn't actually necessary here since only one runtime permission is being sought, but we have written the override in a form capable of dealing with the more general case.

Once it is determined that this is indeed the response to the fine location permission (requestCode = REQUEST_LOCATION), there are again two options.

  1. If the user accepted the permission, we may proceed as if the permission request had not intervened [by invoking the method initializeLocation() in this example]

  2. If the permission request was refused, we give the user one more chance by invoking our own dialog warning of the consequences and asking them to accept the permission.

We have defined the method showTaskDialog(index, title, context, message, acceptText, refuseText) to show general alert dialogs to the user, where index labels the request (we may wish to use this method to show more than one dialog in the same class), context is the present context (typically this), acceptText is the label for the accept button, and denyText is the label for the reject button. The version of this dialog corresponding to the second choice above is illustrated in the following figure.



The dialog generated by showTaskDialog invokes the method negativeTask() to respond if the user chooses "REFUSE PERMISSION" and the method positiveTask() if the user chooses "OK, DO OVER" from this dialog:


// Method to execute if user chooses negative button (REFUSE PERMISSION). This returns to main // activity since no permission has been given to execute required methods of this class. private void negativeTask(int id){ // Use id to distinguish if more than one usage of the showTaskDialog in same class switch(id) { case 1: String warn = "The purpose of this part of the app is to determine your "; warn += "location. Hence it cannot function unless you grant the requested "; warn += "permission. Returning to app homescreen. To enable this "; warn += "part of the app you may manually enable Location permissions in "; warn += " Settings > App > MapExample > Permissions."; // New single-button dialog showTaskDialog(2,"Task not enabled!", warn, dialogIcon, this, "", "OK"); break; case 2: // Return to main page since permission was denied Intent i = new Intent(this, MainActivity.class); startActivity(i); break; } } // Method to execute if user chooses positive button ("OK, Do Over"). This invokes // checkForPermissions(), which will initiate the location permissions dialog again. private void positiveTask(int id){ // Use id to distinguish if more than one usage of the alert dialog switch(id) { case 1: // User agreed to enable so go back to permissions check checkForPermissions(); break; case 2: break; } }

Once again, there are two possible courses of action, corresponding to the user's choice.

  1. If the user chooses OK, DO OVER, the method checkForPermissions() is invoked. This will initiate the location permissions dialog once again from the beginning, where the user will presumably accept the permission.

  2. If instead the user chooses REFUSE PERMISSION, another dialog is launched using showTaskDialog (with index set to 2, to distinguish from the previous dialog).

The second choice (continued refusal to accept the permissions request) presents the following dialog to the user:



which informs the user of why the permission is necessary, and how permissions can be enabled manually at a later time from the Settings > App > MapExample > Permissions menu. Clicking OK invokes the case 2: option from the method negativeTask( ) shown above. This returns the user to the app home screen, since MapMe has no useful functionality without the location permission but other options from the main menu that don't require the locations permission will be functional.



The permissions checks described above must be executed each time the class MapMe is invoked, because under Android 6 and later the user has the option of revoking a permission at some time after it is given by going to Settings > Apps > appname > Permissions on their device. Thus, in dealing with runtime permissions it is important that you test your app to be sure that it handles correctly the situation where a permission is given and then later revoked.

The preceding runtime permissions functionality is operational only on devices running Android 6 and beyond. If an app with such runtime permissions code is executed on a device running earlier versions of Android, the old permissions model---where all permissions are given at install---is invoked (that is why we had to declare the location permission in the manifest file and in runtime code: backward compatibility). For example, here is a screenshot of the present app running under Android 5.0 on a Samsung Note 4 phone. In this case, no runtime permission requests were presented to the user.




Apps written under earlier versions of Android and already deployed to devices should continue to function correctly if those devices are upgraded to Android 6 and beyond. The exception is that in Android 6 and beyond the user can revoke a permission originally given at install of the app. This will break the functionality associated with that permission unless a new version of the app dealing correctly with runtime permissions is installed.

In the sequence described above several different alert dialogs may be presented to the user (depending on choices made). The first, requesting that permission be granted, is controlled by the system and the user has no choice concerning its content. The others are generated by the user-defined method showTaskDialog and so can be configured as desired.

Runtime permissions are a fact of life for Android developers going forward. The preceding example can be used as a template for implementing them, so adding the required code is not hard. The bigger issue is to be certain that your app remains functional under the permutations associated with the multiple possible paths through the runtime permissions process described above.

Although runtime permissions give greater flexibility and choice to users, and likely improve the Android security model, they can become a burden to users if not handled correctly by the developer. The two primary situations to worry about are

  1. The user may become reluctant to enable the functionality of your app because they do not understand why a particular permission is being requested.

  2. If your app requires too many runtime permissions, particularly if they are bunched together, the user may become annoyed and abandon it.

The first issue can be dealt with by providing information to the user for why the permission is needed. Part of the design of your app should now include how to get that information to users in a non-intrusive fashion. (Remember that as of API 23 you have no control over the initial permission request dialog from the system, so you cannot provide the information there. The preceding example inserted that information in the final dialog window, after the user had refused twice to grant the permission. For further discussion of these issues, see Best Practices for Runtime Permissions

The second can be ameliorated by (1) requesting only those permissions that your app actually needs, and (2) structuring your app so that permissions requests are presented in the least intrusive manner (for example, not requesting them until they are actually needed, and avoiding bunching of requests if possible).

 

Code in MapMe.java

MapMe.java implements most of the same interfaces employed above in RouteMapper, and in addition implements the interface LocationListener, which receives notifications that the location has changed from FusedLocationProviderApi. Implementation of LocationListener requires that the callback onLocationChanged() be implemented.

We first create a new GoogleApiClient for location services and a LocationRequest object that will specify the parameters of our location request:


private GoogleApiClient mGoogleApiClient; private LocationRequest mLocationRequest; . . . mGoogleApiClient = new GoogleApiClient.Builder(this) .addApi(LocationServices.API) .addConnectionCallbacks(this) .addOnConnectionFailedListener(this) .build(); // Create the LocationRequest object mLocationRequest = LocationRequest.create(); // Set request for high accuracy mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); // Set update interval mLocationRequest.setInterval(UPDATE_INTERVAL); // Set fastest update interval that we can accept mLocationRequest.setFastestInterval(FASTEST_INTERVAL);

where the first this in the arguments for the constructor is the present context and the next two this arguments indicate that this class will handle callbacks associated with connection and connection errors, respectively (see the onConnected, onDisconnected, and onConnectionError callbacks).

The location client cannot be used until the onConnected callback fires, indicating a valid connection. At that point one can access location services such as present position and location updates.


public void onConnected(Bundle dataBundle) { Toast.makeText(this, getString(R.string.connected_toast), Toast.LENGTH_SHORT).show(); initializeLocation(); // Center map on current location map_center = new LatLng(myLat, myLon); if (map != null) { initializeMap(); } else { Toast.makeText(this, getString(R.string.nomap_error), Toast.LENGTH_LONG).show(); } } // Called after onConnected returns, indicating that GoogleApiClient is ready to // accept location requests public void initializeLocation(){ // We already checked for runtime permissions when onMapReady returned, but formally we // must check for it again here. if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { return; } // Request location updates LocationServices.FusedLocationApi.requestLocationUpdates( mGoogleApiClient, mLocationRequest, this); // Get current location and move the camera there myLocation = LocationServices.FusedLocationApi.getLastLocation(mGoogleApiClient); myLat = myLocation.getLatitude(); myLon = myLocation.getLongitude(); map.setMyLocationEnabled(true); // Retrieve stored zoom from SharedPreferences if (prefs.contains("KEY_ZOOM") && map != null) { currentZoom = prefs.getFloat("KEY_ZOOM", map.getMaxZoomLevel()); } map.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(myLat,myLon), currentZoom)); storeZoom(currentZoom); }

Once we have registered for location updates, when a location change is detected the callback onLocationChanged(Location newLocation) is invoked by the system and we take actions there to update the map view using the new Location passed into onLocationChanged. Updates of the camera view on the map to follow the current location are implemented primarily through the GoogleMap method animateCamera() and the classes

A camera animation in response to locationClient updates using these methods and classes can be implemented by the generic code


CameraPosition cameraPosition = new CameraPosition.Builder() .target(center) // Sets the center of the map .zoom(zoom) // Sets the zoom .bearing(bearing) // Sets the bearing of the camera .tilt(tilt) // Sets the tilt of the camera relative to nadir .build(); // Creates a CameraPosition from the builder // Move (if variable animate is false) or animate (if animate is true) to new // camera properties. Move is sudden; animate is smooth. if(animate){ map.animateCamera(CameraUpdateFactory.newCameraPosition(cameraPosition)); } else { map.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition)); }

The reverse geocoder implemented in the method reverseGeocodeLocation(latitude, longitude) mirrors the code in ShowMap.java for the method geocodeLocation(placename), except that in the present case we pass latitude and longitude coordinates instead of a placename, and we use the Geocoder method getFromLocation() instead of the Geocoder method getFromLocationName() to make our query.

Location services, particularly if they are determining fine location using GPS, can consume substantial amounts of power. Thus, it is imperative that Android lifecycle methods be used to manage those resources, so that we are not requesting location services when they are not needed. One logical way to manage that is to connect location services when a map becomes visible and disconnect those services when the map is not visible. This is handled in MapMe.java by the following code.

// Called by system when Activity becomes visible, so connect location client. @Override protected void onStart() { super.onStart(); if(mGoogleApiClient != null) mGoogleApiClient.connect(); } // Called by system when Activity no longer visible, so disconnect client, @Override protected void onStop() { // Save the current zoom level for next session if (map != null) { currentZoom = map.getCameraPosition().zoom; storeZoom(currentZoom); } // If the client is connected, disconnect if (mGoogleApiClient.isConnected()) { mGoogleApiClient.disconnect(); } // Turn off the screen-always-on request getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); super.onStop(); }

Notice that in addition to managing location services through connecting and disconnecting the LocationClient, in onStop() we also turn off the request made when we started the map activity to keep the screen on at all times.

 

Styles and Themes

Finally, let us discuss briefly the styles and themes used to configure the look of the app (see Themes, Styles, and Preferences for a general discussion of styles and themes in Android).

For the menus we have in general replaced ActionBar with ts more modern equivalent, ToolBar. (For further discussion of transitioning from ActionBar to Toolbar, see AppCompat and Toolbar).

The basic look and feel is determined by the default app and custom styles and themes of res/values/styles.xml,


<resources> <!-- Base application theme. NoActionBar because we are using a Toolbar --> <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> <!-- Set AppCompat’s actionBarStyle --> <item name="actionBarStyle">@style/AppTheme</item> <item name="colorPrimary">@color/primary</item> <item name="colorPrimaryDark">@color/primary_dark</item> <item name="colorAccent">@color/accent</item> <item name="android:textColor">@color/primary_text</item> <!--<item name="android:windowBackground">@drawable/mapbkg</item>--> <!--Background image--> </style> <style name="MyDialogTheme" parent="Theme.AppCompat.Light.Dialog.Alert"> <!--buttons color--> <item name="colorAccent">@color/accent</item> <!--title and message color--> <item name="android:textColorPrimary">@color/primary_text</item> <!--dialog background--> <item name="android:textColorSecondary">@color/primary_text</item> <!--dialog background--> <item name="android:background">@color/dialogBackground</item> </style> <style name="MyPopupTheme" parent="ThemeOverlay.AppCompat.Light"> <!--buttons color--> <item name="colorAccent">@color/accent</item> <!--title and message color--> <item name="android:textColorPrimary">@color/primary</item> <!--dialog background--> <item name="android:textColorSecondary">@color/primary</item> <!--dialog background--> <item name="android:background">@color/primary_light</item> </style> </resources>

and the colors of res/values/colors.xml.


<?xml version="1.0" encoding="utf-8"?> <resources> <color name="barColor">@color/accent</color> <color name="barTextColor">#ffffff</color>color> <!--<color name="buttonColor">@color/primary_light</color>--> <!-- Tint with a palette color--> <color name="buttonColor">#dedede</color> <!-- Tint with very light gray--> <color name="dialogBackground">#eeeeee</color> <!-- Choose one of the following palettes and leave the rest commented out --> <!-- Palette 1 Generated at http://www.materialpalette.com --> <color name="primary">#009688</color> <color name="primary_dark">#00796B</color> <color name="primary_light">#B2DFDB</color> <color name="accent">#448AFF</color> <color name="primary_text">#212121</color> <color name="secondary_text">#727272</color> <color name="icons">#FFFFFF</color> <color name="divider">#B6B6B6</color> <!-- Palette 2 Generated at http://www.materialpalette.com--> <!-- <color name="primary">#FFC107</color> <color name="primary_dark">#FFA000</color> <color name="primary_light">#FFECB3</color> <color name="accent">#536DFE</color> <color name="primary_text">#212121</color> <color name="secondary_text">#727272</color> <color name="icons">#212121</color> <color name="divider">#B6B6B6</color>--> <!-- Palette 3 Generated at http://www.materialpalette.com --> <!-- <color name="primary">#4CAF50</color> <color name="primary_dark">#388E3C</color> <color name="primary_light">#C8E6C9</color> <color name="accent">#FFC107</color> <color name="primary_text">#212121</color> <color name="secondary_text">#727272</color> <color name="icons">#FFFFFF</color> <color name="divider">#B6B6B6</color>--> <!-- Palette 4 Generated at http://www.materialpalette.com --> <!-- <color name="primary">#CDDC39</color> <color name="primary_dark">#AFB42B</color> <color name="primary_light">#F0F4C3</color> <color name="accent">#009688</color> <color name="primary_text">#212121</color> <color name="secondary_text">#727272</color> <color name="icons">#212121</color> <color name="divider">#B6B6B6</color>--> <!-- Palette 5 Generated at http://www.materialpalette.com --> <!-- <color name="primary">#673AB7</color> <color name="primary_dark">#512DA8</color> <color name="primary_light">#D1C4E9</color> <color name="accent">#FFC107</color> <color name="primary_text">#212121</color> <color name="secondary_text">#727272</color> <color name="icons">#FFFFFF</color> <color name="divider">#B6B6B6</color>--> <!-- Palette 6 Generated at http://www.materialpalette.com --> <!-- <color name="primary">#8BC34A</color> <color name="primary_dark">#689F38</color> <color name="primary_light">#DCEDC8</color> <color name="accent">#CDDC39</color> <color name="primary_text">#212121</color> <color name="secondary_text">#727272</color> <color name="icons">#212121</color> <color name="divider">#B6B6B6</color>--> </resources>

Notice that in the color menu six different Material Design color palettes are defined, with all but one commented out. The effect of the different color palettes on the style can be investigated by commenting out the one in use and uncommenting another one (select and Ctrl-Shift-/ to block-comment or uncomment).


Various resources on the Web can be used to produce Material Design color palettes. The ones used here were created with the aid of this material palette generator.

Examples of using the six different color palettes for the current app are illustrated in the following figures.



Where reading across to the right and down these correspond to palettes 1-6, respectively, in colors.xml.


The complete project for the application described above is archived on GitHub at the link MapExample. Instructions for installing it in Android Studio may be found in Packages for All Projects.

Last modified: June 29, 2016


Exercises

1. When a Street View is opened in MapMe.java, the app displays a blank page if no Street View exists for the requested location. Can you figure out a way to check first whether a Street View exists for a given location and warn the user that no street view exists rather than display a blank page?

2. The Geocoder example was implemented on the main UI thread. While adequate for demonstration purposes, putting a potentially blocking operation (caused by the network access in this case) on the main UI thread is not a good idea for a realistic app, since it could lead to sluggish response of the UI. Write a new version that subclasses AsyncTask to run this task asynchronously. Of course, you can also do this using using standard Java threading classes, if you choose. Hint: See the class RouteMapper.java in this project for an example of using AsyncTask.

3. In the Geocoder example we caught only one exception (IOException) and did not do anything substantial with it. Modify the code to use exception handling to make the user experience more seamless and foolproof.

4. In the Geocoder example multiple matches were stored in the array optionArray, but our implementation displayed a map of only the last match in the list. Modify the code so that when there is more than one potential match the list of possibilities is presented to the user and when one of them is selected the map displays that location.

5. Modify the Geocoder example so that when a user goes to a location she has the option of storing that location in a favorites list that persists across sessions with the app. Add capability to view the favorites list and to go directly to the location when an entry is selected. Give the user the capability to delete entries in the favorites list, and to share the favorites list by email, text message, or social media.

6. Modify the Geocoder example so that the user has the option of uploading to cloud storage (Google Drive or Box, for example) the image displayed.

7. Modify the Geocoder example so that the user has the option of sending the image displayed to a printer.

8. In the map tracking implemented in MapMe.java, especially in horizontal mode on a smaller screen, it would be nice to have the Toolbar removed to give more screen space. However, the Toolbar contains menu items that the user may wish to access. Modify MapMe.java to address this issue by having the toolbar collapse when tracking is started, but reappear in response to a gesture like dragging down from the top.

9. In the route overlay implemented in RouteMapper.java there is no capability to remove the overlay once added. Modify RouteMapper to add this capability.

10. Use the location services implemented in ShowMap.java but not presently used to determine the distance to a geocoded target from the user.


Previous  | Next  | Home