What's wrong with Google Maps development on Android - Part Three

April 13, 2011, 7:37 am

Update 5th December 2012 - Android Maps v2

Another sentence from the MapActivity documentation that causes developer pain:

"Only one MapActivity is supported per process. Multiple MapActivities running simultaneously are likely to interfere in unexpected and undesired ways."

This means one MapActivity subclass per Android process and therefore, only one MapView onscreen at any one time. This might not seem like a big deal, but there are times when you would like to have two maps on view at a time. A good example is my WW2 Daily app which lets you swipe locations (and their associated maps) on screen in a scrollview:

To implement this on Android I am going to have to layout image views around my map view and, as the user swipes, replace the adjacent images with a screenshot of the map view, then swap the image view and the map view in the hierarchy. So this is do-able, but compared to keeping multiple map views, a massive pain.

Based on the some of the questions on StackOverflow, this is not a unique gripe with the API (check out the this truly horrible workaround for the issue. Sticking your second map activity in a separate process? ouch!).

This rant continues:

Permalink - Comments - Tags: Development,Android,Google

What's wrong with Google Maps development on Android - Part Two

April 13, 2011, 7:05 am

Update 5th December 2012 - Android Maps v2

A smaller issue with the Android maps library, but worth mentioning

When I started doing web development with the Google Maps API six years ago, it required an API key. You associated it with your domain and prevented naughty people from doing bad things with the API.

Times have changed. With version three, the key went away. The API was infinitely more awesome than the old one, and this was one tiny part of that. The old key was a little irritating if you had to move code between domains, so not having to worry about it made things that much easier for developers.

"The android:apiKey attribute holds the Maps API Key for your application, which proves your application and signer certificate has been registered with the Maps service. This is required in order to receive the map data, even while you are developing.

So you grab your Maps API key by taking an md5 hash of your signing certificate and submitting it to Google to get your key. There are a couple of things that make this annoying for developers:

  • When you are debugging your application you use the SDK debug certificate, so you will need a different Google Maps key for your dev build.
  • If you are building multiple applications from the same source code, then you are going to have to make sure you have an API key for each one and conditionally compile them in.

Admittedly these are pretty minor irritations, but when you consider that neither the iPhone MapKit or the web API require a key (and I realise there are technical differences here), it seems like an unnecessary one.

This rant continues:

Permalink - Comments - Tags: Development,Android,Google

What's wrong with Google Maps development on Android - Part One

April 12, 2011, 12:23 pm

Update 5th December 2012 - Android Maps v2

This rant is coming from an unabashed Google fanboy. I have always loved Google as a company and the maps API in particular. I write this in the hope that it will help Android become a better place for developers like me.

Also, all the hard work for this post was done by Nick Maher. He blazed the Android development trail for me and most of the insights in this post are his. Some of the hoops that he has jumped through makes Tripview for Android even more impressive.

This fairly inocuous sentence in the Hello MapView tutorial hides a nasty underlying problem with the Google Maps library:

This is a special sub-class of Activity, provided by the Maps library, which provides important map capabilities.

Basically it means if you want to display a google map inside an activity in Android, you need to derive from the MapActivity class. So this isn't so bad right? Well maybe not.

What if you have an activity, GenericToolboxActivity, that does a bunch of different things, one of which involves displaying a map? Forcing GenericToolboxActivity to derive from MapActivity seems kinda dumb.

You can get around that problem by sticking your MapActivity in a LocalActivityManager. The LocalActivityManager is basically a sandbox for your activity. You stick your MapActivity subclass, let's call her JealousSpouseMapActivity, in your LocalActivityManager and gently prod her until she gives up that MapView object that you so desire:

// set up the LAM m_lam = new LocalActivityManager(this, true); // stick the map activity in the LAM Intent intent = new Intent(this, JealousSpouseMapActivity.class); m_lam.dispatchCreate(null); m_lam.dispatchResume(); m_lam.startActivity("map", intent); m_mapActivity = (JealousSpouseMapActivity)m_lam.getActivity("map"); // the view please? return m_mapActivity.getWindow().getDecorView(); // Once I am finished with the view, clean it up m_lam.dispatchPause(true); m_lam.dispatchDestroy(true);

You are essentially lulling your JealousSpouseMapActivity into a false sense of security:

Dear JealousSpouseMapActivity, I would never re-parent any of your stuff. That would be wrong. Put your feet up and relax, i'll just need a pointer to your MapView before I pop off and get you breakfast in bed.

Once you have the MapView you can stick it in your view heirachy for your generic activity and JealousSpouseMapActivity is none the wiser. So why can't this stuff just be done in a MapView class instead? According to the MapActivity documentation its responsibilities include:

  • Activity lifecycle managment - So presumably most activities need to manage their lifecycle, so I wonder how much map specific stuff is done here.
  • Setup and teardown of services behind a MapView - Hrm. If we can have a WebView object that does requests in the background, fairly complex rendering and all sorts of other magical things, I wonder why we can't have a MapView that does the same. What is so "special" about a map that makes it impossible to implement as a view subclass.

In fact thinking about WebView brings up another argument against implementing the map this way. What if our helpful and friendly WebView had some kind of mental break and turned into AngryExGirlfriendWebActivity? What if we had to derive activies that show web content from AngryExGirlfriendWebActivity? If we want to show web and map content at the same time, basically we are screwed, we can't derive from them both. I guess we could try and stick both AngryExGirlfriendWebActivity and JealousSpouseMapActivity into a LocalActivityManager and then gently ask them to give up their respective view goodness, but I can't imagine that is going to end well.

Ok, Perhaps I have taken this analogy a little too far, but you get the idea.

This rant continues:

Permalink - Comments - Tags: Development,Android,Google

Filterable ArrayAdapter Followup

November 9, 2010, 6:01 pm

A couple of points to follow up my previous ListView post:

Top left hand corner bug with the workaround

There is actually a hack that will force the ListView to re-request the section names, but unfortunately it causes the section name overlay view to re-render at the top left corner of the screen:

@Override protected void publishResults(CharSequence prefix, FilterResults results) { // ... Turning fast scrolling off before updating our // index and then turning it on again will cause // getSections to be called, but it messes up the fast scrolling // section name overlay lv.setFastScrollEnabled(false); updateListIndex (); lv.setFastScrollEnabled(true); // ... }

How AlphabetIndexer does it

When I was writing this post I discovered the AlphabetIndexer class, a helper class that implements the sectionIndex interface:

If the items in the adapter are sorted by simple alphabet-based sorting, then this class provides a way to do fast indexing of large lists using binary search. It caches the indices that have been determined through the binary search and also invalidates the cache if changes occur in the cursor.

Having a look at the source for this guy, we can see that it uses a similar fixed set of section names that are intialized based on the alphabet passed into the constructor. So if you are filtering a large list with a diverse range of section names, you will have the same problems that exist with my SectionIndexer hack.

Permalink - Comments - Tags: Development,Android,Google

A Filterable ArrayAdapter with Fast Scrolling in Android

November 6, 2010, 12:30 pm

I spent today at the Google Sydney offices for an Android Developer Lab. It is my experience with events like this that you pick up handful of useful bits of information from the presentations, but the real value is the people you talk to during the day and the stuff that you build during the coding labs. My time in the lab was spent chasing down an issue with ListViews, Filterable ArrayAdapters and setFastScrollEnabled.

The issue raises its head when you implement a filterable and section indexed ArrayAdapter for your ListView and then call setFastScrollEnabled to enable the thumb button fast scrolly thing on the right hand side of the list:

Enables fast scrolling by letting the user quickly scroll through lists by dragging the fast scroll thumb. The adapter attached to the list may want to implement SectionIndexer if it wishes to display alphabet preview and jump between sections of the list.

Filterable ArrayAdapter

The first part, building a filterable android.widget.ArrayAdapter, is pretty straightforward. You implement the android.widget.Filter abstract class and store it as a member of your ArrayAdapter subclass. ArrayAdapter has a getFilter method which returns your custom Filter implementation to the ListView.

The Filter interface has a couple of methods that let you create a subset of your ListView data based on the search term that user types on the keyboard. Called in a worker thread performFiltering does the actual work adjusting the ListView content based on the CharSequence parameter. The publishResults method publishes the results of the filtering operation to the UI thread. The publishResults method will look something like this:

@Override protected void publishResults(CharSequence prefix, FilterResults results) { //noinspection unchecked mItems = (ArrayList<GlossaryEntry>) results.values; updateListIndex (); // Let the adapter h about the updated list if (results.count > 0) { notifyDataSetChanged(); } else { notifyDataSetInvalidated(); } }

If we have some data after the filtering operation, then tell the ListView that its data set has changed via the notifyDataSetChanged method, otherwise just tell it to invalidate its contents (so it can render a blank view).

This method is significant for what I am going to talk about next, because it is the mechanism with which I can tell the ListView that it's stuff has changed and it needs to re-draw. I'll come to that in a minute.

SectionIndexer implementation on ArrayAdapter

SectionIndexer is the interface on the ArrayAdapter that allows the ListView to find out how the items should be broken up by section and what text to display as the fast scroll UI is dragged on the right hand side of the view:

Interface that should be implemented on Adapters to enable fast scrolling in an AbsListView between sections of the list. A section is a group of list items to jump to that have something in common.

So this interface has three methods:

  • getPositionForSection(int section) - Provides the starting index in the list for a given section.
  • getSectionForPosition(int position) - This is a reverse mapping to fetch the section index for a given position in the list.
  • Object[] getSections() - This provides the list view with an array of section objects.

So when I initilise my ListView, you need to build an index to map from items to sections, sections to items and the list of section names. In my first implimentation of this stuff I did this organically based on the actual items in the list. I assumed that when I called notifyDataSetChanged the ListView would call getSections on the SectionIndexer so it can get the section names that correspond to the new content. Alas, this is not the case.

public ListIndex (int size, Delegate d) { // Build an index map m_sectionNames = new ArrayList<Object>(); m_sectionForPositionArray = new ArrayList<Integer>(); m_positionForSectionArray = new ArrayList<Integer>(); String prevSection = ""; m_Size = size; for(int i = 0; i < size; i++) { //String sectionName = names[i].substring(0, 1); String sectionName = d.sectionNameForIndex(i); if(!prevSection.equals(sectionName)) { m_sectionNames.add(sectionName); m_positionForSectionArray.add(i); prevSection = sectionName; } m_sectionForPositionArray.add(m_sectionNames.size() - 1); } }

So this implementation will build an m_sectionNames array that will be different for each subset of the ListView content. Because getSections isn't called when you publish a filtering operation to the listView, you will get index out of range errors. The solution I came up with during the lab was to build a section names array based on a fixed alphabet, rather than the variable contents of the ListView:

public ListIndex (int size, Delegate d) { // Build an index map m_sectionNames = new ArrayList<Object>(26); m_sectionForPositionArray = new ArrayList<Integer>(); m_positionForSectionArray = new ArrayList<Integer>(26) ; for (int j = 0; j < 26; j++) { // populate all the position for sections with -1 m_positionForSectionArray.add (-1); // add the section names for the alphabet m_sectionNames.add (String.format ("%c", j + 65)); } // now run through the index int currentIndex = 0; for(int i = 0; i < size; i++) { String sectionName = d.sectionNameForIndex(i); char c = sectionName.charAt(0); int sectionIndex = currentIndex; if ((int)c >= 65 && (int)c < (66 + 26)) { // if this term has an alpha first character, store the section Index sectionIndex = (int)c - 65; currentIndex = sectionIndex; } // set the position for section array with the first value we find if (m_positionForSectionArray.get(sectionIndex) == -1) m_positionForSectionArray.set(sectionIndex, i); // add the section for position item with the last valid sectionIndex we got m_sectionForPositionArray.add(sectionIndex); } // now we have to run through our position for section and make // sure we don't have any gaps left currentIndex = -1; for (int i = 0; i < m_positionForSectionArray.size (); i++) { if (currentIndex == -1 && m_positionForSectionArray.get(i) != -1) { // have our first index ... go back through the array // and set the values currentIndex = m_positionForSectionArray.get(i); for (int j = 0; j < i; j++) m_positionForSectionArray.set(j, currentIndex); continue; } if (m_positionForSectionArray.get(i) != -1) { currentIndex = m_positionForSectionArray.get(i); continue; } if (currentIndex != -1 && m_positionForSectionArray.get(i) == -1) { // fill any gap values with the last index m_positionForSectionArray.set(i, currentIndex); continue; } } }

So this implementation is less than ideal for a couple of reasons. It will miss-categorize items that don't start with an alpha character and it will include sections with no entries (the index is forced to the next available item). It would be much nicer if the ListView just re-grabbed the section names when you call notifyDataSetChanged.

Permalink - Comments - Tags: Development,Android,Google