June 24, 2012

Android Compatibility Package and GoogleMaps issues: FragmentMapActivity and MapFragment

Chapter 1: "The Good, The Bad and The Ugly"

A short introduction:

The target - the cemetery treasure:

we want develop an android app for all platforms, from 1.6 to 4.x, having same ui and backend code. i.e., we want write once, and run everywhere.
(Did you know those words, isn't ?)

The requirements - we have to make an alliance with "The Ugly"

So, we have (only?) to use fragment support from compatibility package, in order to take advantage of fragments paradigma in our application, on device which version < 3.x - honeycomb.
Easy, isn't ? Not so much.

The problem - aka "The Bad"

Android compatibility package doesn't offer support for maps in fragments, so there is not mapfragment for us.
Then, we could just implement our own version... just a "MapFragment extends Fragment"; and honoring fragments paradigma, we let fragment contains all ui/event code.
And an obvious thing is to be able to instance MapView in our MapFragment.

But immediately the first problem: we know a *Fragment must be attached to *FragmentActivity, and we know a MapView must living in a MapActivity - and we don't have a MapActivity with fragment support. There is nothing about this in compatibility package.
The unique existing FragmentActivity inherits from Activity, so the problem.

The road to solution, "The Good"

Pete Doyle proposes first solution, modifiyng the compatibility package source, and we can have a FragmentActivity inheriting from MapActivity.
You can see all about it on his github https://github.com/petedoyle/android-support-v4-googlemaps.
However, the problem is that only one MapActivity is supported per process (here details)
Taro Kobayashi talks about this in his blog http://uwvm.blogspot.jp
So, two alternatives:
  1. we have only and only one FragmentActivity inheriting from MapActivity in our app, and we have to project ui as a "detach current fragment" then "attach next fragment" ("replace" method has some issues in fragment support)
  2. have a distinct FragmentMapActivity inheriting from MapActivity, and obviously providing fragment support while honors compatibility package.
    reading in Kobayashi blog, it's evident he prefers this second way.. et voila his fork from petedoyle's project:
    https://github.com/9re/android-support-v4-googlemaps

And yes, I also searched a solution for all this issue, and even providing the second way too.
Searched, tried to implement my ones, and so on. As long as i just found Kobayashi ones ;D So, it just remains to use ? Oh no, nothing is as easy as it sounds.

"Hey Blondie! You know what you are? Just a dirty son of a ahAHahAHaaaaah!"

As already shown in petedoyle's project, there is some issue with MapFragment, especially instancing MapView:
https://github.com/petedoyle/android-support-v4-googlemaps/blob/master/samples/FragmentLayoutMaps/src/com/example/android/apis/app/FragmentLayout.java plz focus on "onCreateView", row 224 (and row 80, for fields declarations)


As you could see, Pete instances a View inflating from R.layout.map, that is file containing <com.google.android.maps.MapView> (or another MapView), and then this view is returned as viewcontainer in onCreateView.
In onCreateView body he use mapview and does his stuffs, and you can see as View mapView and View mapViewContainer are instanced in parent FragmentMapActivity. But why ?

Fragment borns to contains all ui and event code, and handles fields life within its lifecycle, so we would instance our fields in our MapFragment, not in FragmentMapActivity.

Then, my endeavour (anyhow, it works) for MapFragment:
  • it handles viewContainer directly, instancing it in onAttach;
  • it is an abstract class, with "providesMapViewContainerId" abstract method;
    in concrete subclass, it provides file layout resource id
  • a concrete (final?) "addMapViewToViewParent" method, to be invoked within concrete subclass onCreateView, passing View parameter as its first parameter, and our MapView as second ones.


And MapView where it comes from?
Simply with findViewById (or injecting as singleton, if you use roboguice). So:
public MyMapFragment extends MapFragment {
   private MapView mapView;
   /*
    * other methods
    */
   @Override
   public void onViewCreated(View view, Bundle savedInstanceState) {
      super.onViewCreated(view, savedInstanceState);
      if (mapView == null) // cause only mapview is admitted
         mapView = getActivity().findViewById(R.id.yourmapview);
      addMapViewToViewParent(view,mapView);

      // your stuffs with mapview
   }
}

and MapFragment.java on github or code below:
/*
 * Copyright (C) 2012 Massimiliano Leone - maximilianus@gmail.com
 * Licensed under the Apache License, Version 2.0 (the "License");
 */
package android.support.v4.app;

import android.app.Activity;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;

import com.google.android.maps.MapView;

public abstract class MapFragment extends Fragment {

  private View mapViewContainer;

  /**
    * inflating from layout provided by (your concrete) providesMapViewContainerId method;
    * here we instance the viewcontainer returned by onViewCreated,
    * something as:
    *
    * <pre>return LayoutInflater.from(activity).inflate(R.layout.yourmaplayout, null);* </pre>
    */
  @Override
  public void onAttach(Activity activity) {
    super.onAttach(activity);       

    if (mapViewContainer == null) {
      mapViewContainer =  LayoutInflater.from(activity).inflate( providesMapViewContainerId(), null);
    }
  }

  /**
    * in your concrete class you have to provide the layout containing mapview:
    * something as:
    *
    * "return R.layout.map;"
    * where R.layout.map is map.xml file in layout directory, declaring a RelativeLayout,
    * which contains Mapview, as below:
    *
    * <pre>
    *  <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    *   android:id="@+id/maplayout"
    *   android:orientation="vertical"
    *   android:layout_width="fill_parent"
    *   android:layout_height="fill_parent">

    *  <com.google.android.maps.MapView
    *    android:id="@+id/mapview"
    *    android:layout_width="fill_parent"
    *    android:layout_height="fill_parent"
    *    android:clickable="true"
    *    android:apiKey="YOUR_APIKEY"/>
    *  </RelativeLayout>
    * </pre>    
    * @param activity
    * @return View
    */
  protected abstract int providesMapViewContainerId();

  protected void addMapViewToViewParent(View view, MapView mapView) {

    ViewGroup viewGroup = (ViewGroup) view.getParent();
    if (viewGroup == null) viewGroup = (ViewGroup) view;
    int childs = viewGroup.getChildCount();
    if (childs == 0) {
 viewGroup.addView(mapView);
    } else {
 for (int i = 0; i < viewGroup.getChildCount(); i++) {
   View child = viewGroup.getChildAt(i);
   if (child instanceof FrameLayout) {
       continue;
   } else if (child instanceof MapView) {
       viewGroup.removeView(child);
       viewGroup.addView(mapView, 1);
   } else {
       viewGroup.removeView(child);
   }
      }
      //viewGroup.addView(mapView, 0);
    }
    viewGroup.invalidate();
  }   

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
  return mapViewContainer;
  }

  @Override
  public void onDestroyView() {
    super.onDestroyView();
    ViewGroup parentViewGroup = (ViewGroup) mapViewContainer.getParent();
    if( parentViewGroup != null ) {
      parentViewGroup.removeView( mapViewContainer );
      //parentViewGroup.removeAllViews();
    }
  }   

}

No comments :