Tracking Highly Accurate Location in Android — (Vol.2)Display location on Google Map

內容

In the last post, I’ve explained how to get locations both in the background with proper criteria. Before explaining much deeper location topics, I am going to take a time to implement map feature on the sample app.It is because many location-related apps have a map. And even if your app doesn’t have a map, it often becomes necessary to have a map for debugging and tuning your location tracking feature.

Here is the link to the Google Maps Android SDK official document.

I’m going to follow the same steps as the document above to setup the map first.

Obtaining an API Key

Go to the Google Map Android SDK official site, and press Get Key button, then follow the instruction.

Select Create a project, then press Continue.

New project called “My Project” is created, and you are going to see the page as below.

(You can change the project name. I recommend you to rename the name to your app’s name.)

Input API Key name here. I name it as “AndroidLocationStarterKit”.

Select Android app in the Key restriction section. By doing it, you API key is only used by Android apps. (If you are developing an iOS version of the same app, check iOS apps as well.)

Press +Add package name and fingerprint, and input your app’s package name and SHA-1 fingerprint.

You can double check your app’s package name package name in the Manifest fail as below.

Fingerprint

Input a command as blew in Terminal of your Mac.

keytool -list -v -keystore mystore.keystore

If you haven’t released your app and don’t have a release keystore, you can pass your debug keystore as the parameter to the keytool command above.

Find a file named debug.keystore under ~/.android/ in your Mac, that’s the debug keystore file.

Unless you haven’t moved Android SDK installation in your Mac, the entire command you should input should like this.

$ keytool -list -v -keystore ~/.android/debug.keystore

Then you are asked to input a password for the keystore.
You must not have set any password for the debug.keystore, so just press Enter.

You are going to see a SHA-1 fingerprint displayed on the Terminal.

The finger print should like one below. It is a array of twenty 2-digit hex values.

5F:77:F6:40:9B:E2:3D:C4:F9:65:92:4D:55:F0:1C:05:CB:36:FC:FF

Register this, and follow a few more instructions, then you get an API key finally.

(Upon releasing your app to Google Play, you have to repeat this process but with release keystore. Get a new SHA-1 fingerprint from your release keystore, and get a release version of the API key.)

Embed the key to Manifest

Add a <meta-data> element as below to your Manifest file.

Replace “YOUR_API_KEY” with the actual API key you have created in the step above.

Make sure this tag is located under <application> tag as below.

Now API key is properly set for you to be able to use Google Map feature in your app.

Layout a MapView

To display Google Map on your app, embed MapView to your app’s layout.

In the layout xml above, MapView is embedded FrameLayout, and occupies entire screen.

Start coding

First few important steps to be done in MainActivity class which are;

  1. Get a reference to MapView object in onCreate of the MainActivity class.
  2. In each lifecycle method of the Activity class, call the corresponding lifecycle method of the MapView object.
  3. Obtain GoogleMap object from the MapView object

Get a reference to MapView object in onCreate()

Declare a member variable of MapView class

Inside OnCreate(), get the reference to the MapView object.

In each lifecycle method of the Activity class, call the corresponding lifecycle method of the MapView object

The official says

“must forward the following activity lifecycle methods to the corresponding methods in the MapView”

This means, in Activity’s onCreate()、onDestroy()、onStart()、onStop、onResume()、onPause()、onLowMemory()、and onSaveInstanceState(), call MapView’s onCreate()、onDestroy()、onStart()、onStop、onResume()、onPause()、onLowMemory()、onSaveInstanceState()

Do one by one like the code below.

Obtain GoogleMap object from the MapView object

Finally get an object of GoogleMap class from the MapView object by using getMapAsync().

Calling this method will give us a callback inside the OnMapReadyCallback() listener class. The callback method is onMapReady.

A GoogleMap object is passed to this method, so store it to a member variable (which is called “map” in my sample code)

In the code above, around the line #6 — #9, I did

  • Hide the zoom controller from the map.
  • Hide the user’s position indicator
  • Turn off the compass feature
  • Show “My location” button.

The reason I hided the user’s position indicator is I am going to draw my own custom indicator later in this post.

In the line #11, I set OnCameraMoveStartedListener to get a callback when the map’s position or zoom level is changed. What I do inside this callback is later explained in the section called “Auto zoom”.

After this we are not going to touch the MapView object.

Drawing the current position, drawing the path of GPS waypoints, and moving or zooming the map are going to be done by using the GoogleMap object.

Draw the current location

We turned off the GoogleMap’s default marker to indicate the user’s position. In this section, I’m going to explain how to draw custom marker of the user’s position.

Get a location

To draw the marker, we need to get the user’s current location. I’m going to modify the LocationService class which we have created in the previous post.

Add these two member variables, and instantiate them in onCreate as the code below.

I’m going to change onLocationChanged method.

Below is the new implementation.

I added a couple of lines of code to accumulate each location to the locationList when isLogging flag is true. This isLogging flag becomes true only when the app is tracking location.

In addition to accumulating each new location to the locationList, onLocationChanged method broadcasts each new location to other Activities using LocalBroadcastManager. The lines of code from #9 to #11 above does this broadcasting.

MainActivity is going to receive this location.

Receive new location

MainActivity creates a BroadcastReceiver to receive each new location sent from the LocationService.

By overriding a method named “onReceive”, MainActivity can get new location.

MainActivity passes each new location to the four methods below.

  • drawLocationAccuracyCircle(newLocation) :draws a circle indicating the accuracy of the location information.
  • drawUserPositionMarker(newLocation) :draws the user’s current position
  • addPolyline() :draw the path of the user’s location history until now
  • zoomMapTo(): zooms the map to the proper position and zoom level.

Let’s look each method in detail.

drawLocationAccuracyCircle(newLocation)

This method draws a translucent black circle around the user’s position. The radius of the circle indicates the inaccuracy of the current location information. The larger the circle is the more inaccurate the current location information is. It is exactly the same indicator as the translucent blue circle which you see on the normal Google Map app.

Below is the code.

I used addCircle to add a circle to the map. In the line #5, I created a CircleOptions object with the center location of the circle, the color of the circle and the radius of the circle.

I called getAccuracy of the location object which returns how inaccurate the location is in meters.

I set this value as the radius of the circle.

This is how you can indicate how certain the user’s current position is exactly as Google Map app.

drawUserPositionMarker(newLocation)

This method draws a opaque red circle on the position of the user.

The code looks like this.

I created BitmapDescriptor object from a red dot image resource, then called addMarker method of the map object.

The reason I don’t use addCircle is addCircle shows a circle with the specified radius in meters which is geographical size. If the user zooms the map, the circle is going to be enlarged.

On the other hand, addMarker sows a maker with the fixed pixel size on the screen. If the user zooms the map, the size on the screen is going to be the same.

Keeping the same size of the marker is very helpful for the user to find his position while zooming out the map.

This is why I used two different methods to add circles on the map.

addPolyline()

This method draws the path which connects the user’s tracked locations from his/her starting position to his current position.

This code first obtains the locationList from the LocationService object.

If there are only two locations in the list, the code creates a PolylineOptions object with these two locations and calls addPolyline method of map object. This is going to draw a line between the first and second location of the user. Calling addPolyline returns a Polyline object which is stored to the member variable runningPathPolyline.

If there are more than two locations, this code picks up the latest location and adds it to the the runningPathPolyline in the code from #23 to #16 above.

Since this method is called everything new location is obtained, so that pick and add the latest location only works.

zoomMapTo(newLocation)

This method zooms the map to the user’s current location by calling animateCamera method and passing the current location.

animateCamera puts animation while moving and zooming to the user’s position.

New UX challenge

If you run the sample app with this zoomMapTo() method, you are going to find there is one UX problem.

zoomMapTo() zooms the map whenever new location is obtained. If the user is scrolling the map and browsing a certain location, the use is going to loose that scene by the forced scroll of the map by zoomMapTo().

To improve this, we are going to implement 4 behaviors.

  • Zoom to the user’s location when the first position of the user is obtained.
  • Remove animation from the first zoom above. This is because the first zoom is done when the user opens the app (and the map), locate the map to the user’s location without animation gives the user a feeling that the app works quick.
  • After the first zoom, if the user touches the map, block zoom by zoomMapTo() method. We assume that the user is still browsing a map for a while after his/her touch action.
  • After the first zoom, enable auto scroll the map to current position only when the user is not touching the map. (We don’t auto zoom to respect the zoom level which the user has last browsed. )

The code below is the zoomMapTo function which implements the four features above.

At the line number 4 above, we check the flag didInitalZoom which indicates the map is at very initial state and not zoomed yet. In this case we changes the map’s location to the current position and changed the zoom level to 17.5 without animation.

From the second time (under the line number 16), we check `zoomable` flag. This flag is kept false for 10 seconds after the user’s interaction on the map. If this flag is true meaning the user hasn’t touched the map for a while, move the map to the user’s current position with animation. We don’t change the zoom level from what user has lastly set. We keep zoomable flag false while the scroll animation.

Lastly, we add a code to keep the zoomable flag false for 10 seconds after the user’s interaction on the map.

Back to onCreate, and go inside getMapAsync(),

Here, we attach setOnCameraMoveStartedListener to the GoogleMap and within this listener implement setOnCameraMoveStartedListener. This callback is called whenever the map’s focus is changed. This callback is called when the map’s focus is changed by the user or by the program by calling moveCamera or animateCamera functions.

The reason parameter passed in the callback helps us understand the reason why the callback is called.
If the value is GoogleMap.OnCameraMoveStartedListener.REASON_GESTURE, the map’s focus has been changed by the user’s gesture. We change zoomable flag to false only when the map’s focus is changed by the user, so turn the flag to false within this case.
We set timer here and after the 10 seconds, turn on the flag to true. By doing this the auto focus function can change the focus of the map to the current position while the user is moving. This provides the user with smooth transition to auto navigation move 10 seconds after the user stops touching the app. This is very good UX especially in running type of apps.

Now we’ve finished zoomMapTo function. We call this function from the bottom of locationUpdateReceiver’s onReceive callback.

Sample Code

Clone https://github.com/mizutori/AndroidLocationStarterKit
and roll back commits to
**38d83401805970ee2b631b5dac18c2823b98b03d
**You can check the state of the code until here.
In the next, I’m going to explain how to make filters which picks up only trustable high accuracy location information.

_I am Tokyo-based Android and iOS app developer.
If you have any questions about location tracking on Android or iOS, or you are interested in asking us to develope your app. Feel free to contact me anytime.
_@mizutory[email protected]

總結
The article discusses the process of implementing a map feature in an Android app using the Google Maps Android SDK. It explains obtaining an API key, embedding the key in the Manifest file, setting up a MapView in the layout, and coding in the MainActivity class to interact with the MapView object. The article covers obtaining the user's location, drawing custom markers, adding polylines to show location history, and zooming the map to the user's current location. It also addresses UX challenges related to zooming the map. The steps include getting the user's location, drawing accuracy circles, markers, and polylines, as well as zooming the map to the user's position. The article provides detailed code snippets and explanations for each step in the implementation process.