Upcoming and OnDemand Webinars View full list

Embedding Custom Views with MapView V2

Andrew Lunsford

MapView! You can see a GoogleMap inside of it! Find your location! Pan! Zoom! Unzoom! There are too many amazing features to talk about. We should add one to our app like… now.

Google has graciously provided us with a MapFragment that can easily be added to any Activity, but what if you don’t want a full Fragment, or you want to add a MapView to a RecyclerView?

The Easy Way

First add the maps dependency:

compile 'com.google.android.gms:play-services-maps:6.5.87'

And add this fragment into your activity layout:

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

You’ll end up with this:

MapFragment added to Activity

You’re welcome. Post over, right?

No! You should yearn for more.

You now have a Fragment that wraps an instance of a MapView. That’s neat and all, but what can you do with it? Anything you want! As long as you enjoy using Activities, everything is fine and dandy.

Fragments 4 Evar!

But here at the Ranch, Fragments are all the rage, and we prefer to have the majority of the controller and view logic in the Fragment when possible. If you use support fragments like we do, you will want to use SupportMapFragment instead of MapFragment. It gives you the exact same result, but subclasses the support Fragment class instead.

At this point, you now have a full screen MapView, but not many options for customizing the view. Adding additional views around the <fragment> tag gets messy. What if you only want a MapView, not a whole Fragment? Why not just use it directly?

DIY MapViews

The docs will tell you that the MapView resides in the com.google.android.gms.maps package. Let’s create a simple layout for your host fragment.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:text="@string/header"
        style="@style/LargeTextViewCentered"/>

    <!-- Note: We use a reverse DNS naming style
          ie: fragment (layout type)
              + embedded_map_view (name)
              + (optional view name if only one)
              + mapview (view object type) -->
    <com.google.android.gms.maps.MapView
        android:id="@+id/fragment_embedded_map_view_mapview"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"/>

    <TextView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:text="@string/footer"
        style="@style/LargeTextViewCentered"/>

</LinearLayout>

Running the app, you get… Nothing?

Embedded MapView without Lifecycle

Double checking the XML, it looks like there should be a map between the header and footer TextViews. Consult the docs again and… Oh. It looks like the MapView needs to have the Activity / Fragment lifecycle methods sent to it.

The docs suggest sending onCreate, onResume, onPause, onDestroy, onSaveInstanceState and onLowMemory. Let’s try wiring those up.

protected MapView mMapView;

@Override
public void onResume() {
    super.onResume();
    if (mMapView != null) {
        mMapView.onResume();
    }
}

@Override
public void onPause() {
    if (mMapView != null) {
        mMapView.onPause();
    }
    super.onPause();
}

@Override
public void onDestroy() {
    if (mMapView != null) {
        try {
            mMapView.onDestroy();
        } catch (NullPointerException e) {
            Log.e(TAG, "Error while attempting MapView.onDestroy(), ignoring exception", e);
        }
    }
    super.onDestroy();
}

@Override
public void onLowMemory() {
    super.onLowMemory();
    if (mMapView != null) {
        mMapView.onLowMemory();
    }
}

@Override
public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    if (mMapView != null) {
        mMapView.onSaveInstanceState(outState);
    }
}

In onDestroy, we add a try/catch because sometimes the GoogleMap inside of the MapView is null, causing an exception. It’s unpredictable, so we can only guard against it. Since we are calling mMapView.onDestroy to allow it to clean up its possibly null GoogleMap, then our job is done and we can safely ignore the resulting exception.

All that’s left is to pass onCreate to mMapView. It doesn’t quite yet exist in the Fragment.onCreate, so let’s put it in the onCreateView.

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_embedded_map_view, parent, false);
    mMapView = (MapView) view.findViewById(R.id.fragment_embedded_map_view_mapview);
    mMapView.onCreate(savedInstanceState);
    return view;
}

Success! We now have a MapView in our Fragment.

MapView in our Fragment

Man, that thing looks great! The best part is that you can now rearrange and access it just like any other View. Now call mMapView.getMapAsync(OnMapReadyCallback callback) and have fun with the map!

Get Fancy

So what else can you do with a MapView?

On a recent project, the client wanted maps in a RecyclerView. The tricky part here is that the MapView list item view will be created, reused and possibly destroyed at the RecyclerView’s discretion. Its lifecycle will be out of sync from the hosting Fragment. Let’s see what we can do to get this working.

First, create the view and setup the RecyclerView.

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_recycler_view, parent, false);
    mRecyclerView = (RecyclerView) view.findViewById(R.id.fragment_recycler_view_recyclerview);
    mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
    RecyclerViewMapViewAdapter recyclerViewAdapter = new RecyclerViewMapViewAdapter();
    mRecyclerView.setAdapter(recyclerViewAdapter);
    return view;
}

Now the Adapter:

private class RecyclerViewMapViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    @Override
    public int getItemCount() {
        return 10;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        MapViewListItemView mapViewListItemView = new MapViewListItemView(getActivity());
        return new MapViewHolder(mapViewListItemView);
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {}
}

Final version

The MapViewHolder only holds onto our custom MapViewListItemView:

public class MapViewHolder extends RecyclerView.ViewHolder {

    private MapViewListItemView mMapViewListItemView;

    public MapViewHolder(MapViewListItemView mapViewListItemView) {
        super(mapViewListItemView);
        mMapViewListItemView = mapViewListItemView;
    }
}

Final version

And now the MapViewListItemView:

public class MapViewListItemView extends LinearLayout {

    protected MapView mMapView;

    public MapViewListItemView(Context context) {
        this(context, null);
    }

    public MapViewListItemView(Context context, AttributeSet attrs) {
        super(context, attrs);
        View view = LayoutInflater.from(getContext()).inflate(R.layout.list_item_map_view, this);
        mMapView = (MapView) view.findViewById(R.id.list_item_map_view_mapview);
        setOrientation(VERTICAL);
    }
}

Final version

The MapViewListItemView also contains a TextView as a simple divider, and to help you know where the MapView should be. Just to see what is working so far, let’s build and launch.

Adding a MapView to RecyclerView

There should be MapViews underneath each of those “Text”s. So just like before, lifecycle events need to be forwarded from the hosting fragment to each MapViewListItemView. Start by giving MapViewListItemView a few methods to pass each event on to its MapView.

public void mapViewOnCreate(Bundle savedInstanceState) {
    if (mMapView != null) {
        mMapView.onCreate(savedInstanceState);
    }
}
public void mapViewOnResume() {
    if (mMapView != null) {
        mMapView.onResume();
    }
}
...

You get the idea. Now, when should these be called? We create each MapViewListItemView in onCreateViewHolder where we stored it into a MapViewHolder. We need to call MapView.onCreate here, but it needs a savedInstanceState. It will probably be used to save map settings like zoom and pan, but let’s not worry about that just yet and attempt to pass null.

public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    MapViewListItemView mapViewListItemView = new MapViewListItemView(getActivity());
    mapViewListItemView.mapViewOnCreate(null);
    return new MapViewHolder(mapViewListItemView);
}

Great! Now the MapView needs to know when to resume.

onBind is when the view is setup after a recycle, but we need to call through the MapViewHolder.

public void mapViewListItemViewOnResume() {
    if (mMapViewListItemView != null) {
        mMapViewListItemView.mapViewOnResume();
    }
}

And now use it:

public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    MapViewHolder mapViewHolder = (MapViewHolder) holder;
    mapViewHolder.mapViewListItemViewOnResume();
}

Let’s see if there is a map or two.

RecyclerView MapView with Lifecycle

Excellent! What of the other lifecycle methods? Surely those are needed as well? They may be unnecessary, but the docs recommend it.

In order to do this, add the MapViewListItemView to a List in onCreateViewHolder. Then on each Fragment lifecycle event we attempt to call into all views. Bear in mind that although this is a very rudimentary approach, it complies with the docs.

@Override
public void onResume() {
    super.onResume();
    for (MapViewListItemView view : mMapViewListItemViews) {
        view.mapViewOnResume();
    }
}

@Override
public void onPause() {
    for (MapViewListItemView view : mMapViewListItemViews) {
        view.mapViewOnPause();
    }
    super.onPause();
}

@Override
public void onDestroy() {
    for (MapViewListItemView view : mMapViewListItemViews) {
        view.mapViewOnDestroy();
    }
    super.onDestroy();
}

@Override
public void onLowMemory() {
    super.onLowMemory();
    for (MapViewListItemView view : mMapViewListItemViews) {
        view.mapViewOnLowMemory();
    }
}

@Override
public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    for (MapViewListItemView view : mMapViewListItemViews) {
        view.mapViewOnSaveInstanceState(outState);
    }
}

Gotchas

You may notice that sometimes the GoogleMap has not started by the time you need it. This is a common problem, and it can be fixed by adding MapsInitializer.initialize to your Fragment.onCreate. This will ensure the Google Maps Android API has been started in time for your maps. It will also set up map-related classes and allow you to access them before the GoogleMap is finished initializing. You may be wondering why we passed null into our mapViewListItemView.mapViewOnCreate, because this gives the MapView nowhere to save any customizations.

Customizations will be lost during low memory or orientation changes. These include map markers, camera location, zoom levels, etc. To avoid this undesired behavior, you will need a rather complex system of Bundle management. Each Adapter item position will need its own Bundle, not just every view or holder. The MapView’s state will need to be saved each time the view is detached and recycled, and also during Fragment.onSaveInstanceState for the ones that are still visible.

If, for some reason, you choose to pass the host Fragments savedInstanceState to the MapView, be warned that there could be issues. If any non-MapView related data is saved by you, a library, or even a Google view class (i.e., RecyclerView) into the Bundle, it will need to be “sanitized.” It would seem that the MapView attempts to walk through all the information in the savedInstanceState and instantiate it, which can manifest as a ClassNotFoundException or BadParcelableException. If this happens, you will need to remove the data from the Bundle before passing it on to the MapView.onCreate.

Not Happy with Your Current App, or Digital Product?

Submit your event

Let's Discuss Your Project

Let's Discuss Your Project