Driving directions in native iOS and Android apps

One thing that initially surprised me when I started working with iOS’s MapKit and the com.google.android.maps libraries was that, in both cases, there is no API support for finding and displaying travel directions on their map views. It seems that this kind of capability commands big bucks for licensing, so they aren’t made freely available as part of the standard SDKs. (Apparently Android had a public API for this early on but Google removed it.)

If you are developing an app that includes a map component, and want to provide your users with travel directions without paying for some kind of license, there are a few avenues available. In this post, I’ll give a brief overview of the options and then describe how I used the Google Maps Javascript API to build directions into my CoffeeFast coffee shop locator app.

Unless you want to roll your own directions API (including some potentially gnarly code to handle the map overlays), there are three approaches available, summarized in the following table:

Approach Pros Cons
Launch system Map application
  • Easy (a line or two of code)
  • High potential for user confusion because they have left your app
  • Unable to launch system Maps app with directions already displayed.
3rd party extension to native map API
  • Integrates directly with your existing MapView
  • Unknown ease of use, quality and/or support
  • Potentially high latency
Google Maps Javascript API to display directions in a WebView
  • Relatively straightforward implementation
  • Well documented
  • Robust, reliable service
  • Same HTML & Javascript can (mostly) be re-used across platforms
  • Requires new “directions” view that is independent of the native map view
  • High bandwidth & latency to load new map tiles from network

Clearly, launching the built-in Maps application is without a doubt the easiest. However, while both iOS and Android support loading their Maps app with a particular point specified, neither allow the calling app to initialize it with directions automatically displayed. The user still has to make a few taps to bring up the directions, and they may get confused that they are no longer in your app. (Android’s “back” button – which is being retired on newer hardware – mitigates this somewhat. For iOS, though, the user has to go back to the launcher to get back into your app. And I’m guessing most users don’t know that double-tapping the home button will bring up the recently-used app list.)

So the lazy way works, but does not provide a great user experience. Clearly, the superior user experience comes from integrating directions as text and map overlays in the existing map view. There are a few open source libraries that seem to handle this. They might work great; I don’t know. Unfortunately, open source libraries are have wildly different levels of bugginess, documentation quality, and support. Maybe the map extensions out there are great, but I was worried that I’d invest a lot of time in them and hit a showstopping bug. Being relatively new to both Android and Cocoa, my concern was that I wouldn’t be able to easily determine if the problem was in my code, in the library code, or with the Maps API. Now that I have a bit more experience under my belt, I’d definitely give an existing library try. At the time, though, I wanted to remove as much development risk from my work as possible. Introducing a 3rd party library had the potential to be a significant time sink, so I decided against it.

I settled on launching a WebView that loads a locally-stored HTML file which invokes a remote call to the Directions Service of the Google Maps Javascript APIs to get directions to be displayed to the user. The Directions Service documentation gives some great examples of how to use the library, including this page that provides content panels for both a visual map and text-based directions. Screenshot below:

Screenshot of Google Maps API directions example

The problem is that this (and the other Google-provided examples) are all targeted for desktop browsers. Clearly, there is not enough real estate on a mobile phone screen to display both content panels simultaneously and have them both remain readable. To hack together a decent mobile experience, I had to dust off my (admittedly limited) Javascript skills and tweak the example code.

I decided a sliding content panel was the best choice: Initially, only the map (with highlighted route) would be displayed to the user. A button would cause the text directions panel to slide up over the map panel. Tapping the same button again would slide the text directions back down, again revealing the map.

directions screen with mapdirections screen with text

Fortunately, JQuery has nice sliding transitions built-in, and JQuery touch has a default button style that looks relatively consistent with standard mobile UI buttons. So, while I don’t have much Javascript or JQuery experience, this turned out to be pretty easy to implement. (And I got some help from this blog post on mkyong.com.) It isn’t going to win any design awards, but it gets the job done nicely.

Here’s the HTML content of the page:



This creates two named divs in the main content section of the page, one for the map and one for text directions. The button is anchored to the page footer div in the “fixed” data-position to keep it from moving around depending on the page size returned by Google. The data-url field is necessary to prevent the page from loading twice when the button is tapped. The default button text of “Loading” is just a somewhat hacky way to put some text content on the screen as soon as the local page loads. As we’ll see, it normally gets overwritten by a Javascript call before it renders. (All margins and padding values were unscientifically settled upon by trial and error. The explicit zeroes were necessary to override some parent style values.)

The interesting stuff happens in the Javascript initialize() and ready() functions:

function initialize() {
  // parses params into dictionary with "start", "dest", and "mode" entries.
  var urlParams = parseUrlParams();

  var directionsService = new google.maps.DirectionsService();
  var directionsDisplay = new google.maps.DirectionsRenderer();

  // doesn't matter since we'll recenter with the directions
  var theCenter = new google.maps.LatLng(0, 0);
  
  var myOptions = {
    center : theCenter,
    mapTypeId : google.maps.MapTypeId.ROADMAP
  };

  var map = new google.maps.Map(document.getElementById('map_canvas'), myOptions);
  directionsDisplay.setMap(map);
  directionsDisplay.setPanel(document.getElementById('directions-panel'));

  var mode = google.maps.DirectionsTravelMode.DRIVING;
  if(urlParams['mode'] == "walking")
    mode = google.maps.DirectionsTravelMode.WALKING;

  var request = {
    origin : urlParams['start'],
    destination : urlParams['dest'],
    travelMode : mode
  };

  directionsService.route(request, function(response, status) {
                                     if(status == google.maps.DirectionsStatus.OK) {
                                       directionsDisplay.setDirections(response);

                                       // INSERT YOUR APP'S CALLBACK HERE

                                      }
                                    }});
}

The parts of the code dealing with the Google Maps API is almost a direct cut & paste from Google’s example code linked to above, along with some custom parsing of the URL string generated by the calling app. You’ll probably want to put a callback to your app inside the directionsService.route() request callback. The reason is that this HTML page is stored locally as part of your app distribution, but the actual directions content is being fetched asynchronously via the Javascript call. Because the local content of the page loads almost instantaneously, your your iOS app’s webViewDidFinishLoad: method or your Android app’s onPageFinished method will be invoked immediately. If you have a UIActivityIndicator or ProgressBar that gets hidden in one of these methods, it’ll disappear well before the map is rendered. Depending on the speed of their network connection, your user might think the app has frozen.

(If you are unfamiliar with how to callback to your app from the web view, callbacks from a UIWebView to Objective-C are described here, and callbacks from WebView to Java are described here. Caveat: There is a bug in the Android emulator that prevents the callback from working, but it will work on your physical device.)

Incidentally, the complete HTML file is identical across my Android and iOS builds, with the exception of the callback code to tell the app that the Google content has finished loading. It took me a little while to get this working the way I wanted in iOS, but then I had it up and running perfectly in Android right away, which was nice.

The nifty sliding animation is set up in the Javascript ready() function:

$(document).ready(function() {
  // hide the text directions initially
  $('#directions-panel').slideToggle(0);
  $("#slideToggle .ui-btn-text").text(showDirString);
  directionsShowing = false;

  $('#slideToggle').click(function() {
    var btnText;
    var btnIconClassToAdd;
    var btnIconClassToRemove;

    if(directionsShowing) {
      btnText = "Show Text Directions";
      btnIconClassToAdd = "ui-icon-arrow-u";
      btnIconClassToRemove = "ui-icon-arrow-d";
    } else {
      btnText = "Show Map";
      btnIconClassToAdd = "ui-icon-arrow-d";
      btnIconClassToRemove = "ui-icon-arrow-u";
    }

    // this may break with a different version of JQM.
    $("#slideToggle .ui-btn-text").text(btnText);
    $("#slideToggle .ui-icon").addClass(btnIconClassToAdd).removeClass(btnIconClassToRemove);

    $('#directions-panel').slideToggle(400);
    $('#map_canvas').slideToggle(400);
    directionsShowing = !directionsShowing;
    return false;
  });
});

This is also pretty straightforward: When the button is clicked, the JQ slideToggle method is invoked on the two panels, causing the hidden one to be shown and the visible one to be hidden. I did a lot of trial and error with slideToggle, slideUp, and slideDown and eventually settled on this one; you should experiment until you find what you like. I did use some undocumented button properties for the icons and text, but as long as you hard-code to a specific version of JQM, you won’t risk this breaking.

So that is a relatively simple way to add directions to your mobile app! If you want to see it in action, check out my CoffeeFast app. There are Lite versions available for free for both Android and iOS.


Comments

Driving directions in native iOS and Android apps — 1 Comment

  1. Pingback: Managing ad-free and ad-supported user interfaces, the easy way | thumblines