50

I'm trying to add a MapFragment to my current Fragment. The use of nested fragments is restricted to FragmentTransactions, you can't use the xml tag in your layout. Also, I want it to be added to the main Fragment when the user presses a button. So, I'm creating the MapFragment programmatically with getInstance() when the user presses that button and adding it to the proper place. It is shown correctly, so far so good.

The problem is that after attaching the MapFragment I need to get a reference to GoogleMap to place a Marker, but the getMap() method returns null (as the fragment's onCreateView() hasn't been called yet).

I looked at the demo example code and I found the solution they use is initializing the MapFragment in onCreate() and getting the reference to GoogleMap in onResume(), after onCreateView() has been called.

I need to get the reference to GoogleMap right after the MapFragment initialization, because I want the users to be able to show or hide the map with a button. I know a possible solution would be to create the Map at the start as said above and just set it's visibility gone, but I want the map to be off by default so it doesn't take the user's bandwidth if they don't explicitly asked for it.

I tried with the MapsInitializer, but doesn't work either. I'm kind of stuck. Any ideas? Here is my testing code so far:

public class ParadaInfoFragment extends BaseDBFragment {
// BaseDBFragment is just a SherlockFragment with custom utility methods.

private static final String MAP_FRAGMENT_TAG = "map";
private GoogleMap mMap;
private SupportMapFragment mMapFragment;
private TextView mToggleMapa;
private boolean isMapVisible = false;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View v = inflater.inflate(R.layout.fragment_parada_info, container, false);
    mToggleMapa = (TextView) v.findViewById(R.id.parada_info_map_button);
    return v;
}

@Override
public void onStart() {
    super.onStart();
    mToggleMapa.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            if (!isMapVisible) {
                openMap();
            } else {
                closeMap();
            }
            isMapVisible = !isMapVisible;
        }
    });
}

private void openMap() {
    // Creates initial configuration for the map
    GoogleMapOptions options = new GoogleMapOptions().camera(CameraPosition.fromLatLngZoom(new LatLng(37.4005502611301, -5.98233461380005), 16))
            .compassEnabled(false).mapType(GoogleMap.MAP_TYPE_NORMAL).rotateGesturesEnabled(false).scrollGesturesEnabled(false).tiltGesturesEnabled(false)
            .zoomControlsEnabled(false).zoomGesturesEnabled(false);

    // Modified from the sample code:
    // It isn't possible to set a fragment's id programmatically so we set a
    // tag instead and search for it using that.
    mMapFragment = (SupportMapFragment) getChildFragmentManager().findFragmentByTag(MAP_FRAGMENT_TAG);

    // We only create a fragment if it doesn't already exist.
    if (mMapFragment == null) {
        // To programmatically add the map, we first create a
        // SupportMapFragment.
        mMapFragment = SupportMapFragment.newInstance(options);
        // Then we add it using a FragmentTransaction.
        FragmentTransaction fragmentTransaction = getChildFragmentManager().beginTransaction();
        fragmentTransaction.add(R.id.parada_info_map_container, mMapFragment, MAP_FRAGMENT_TAG);
        fragmentTransaction.commit();
    }
    // We can't be guaranteed that the map is available because Google Play
    // services might not be available.
    setUpMapIfNeeded(); //XXX Here, getMap() returns null so  the Marker can't be added
    // The map is shown with the previous options.
}

private void closeMap() {
    FragmentTransaction fragmentTransaction = getChildFragmentManager().beginTransaction();
    fragmentTransaction.remove(mMapFragment);
    fragmentTransaction.commit();
}

private void setUpMapIfNeeded() {
    // Do a null check to confirm that we have not already instantiated the
    // map.
    if (mMap == null) {
        // Try to obtain the map from the SupportMapFragment.
        mMap = mMapFragment.getMap();
        // Check if we were successful in obtaining the map.
        if (mMap != null) {
            mMap.addMarker(new MarkerOptions().position(new LatLng(37.4005502611301, -5.98233461380005)).title("Marker"));
        }
    }
}
}

Thanks

Sloy
  • 2,975
  • 3
  • 20
  • 21
  • Have you looked at subclassing SupportMapFragment and doing your initialisation in there? – Estel Dec 05 '12 at 23:22
  • @Estel I just saw your comment. I wrote an answer with that aproach, thanks :) – Sloy Dec 05 '12 at 23:41
  • You can use OnMapReadyCallback: mMapFragment.getMapAsync(this) and @Override public void onMapReady(GoogleMap googleMap) {} – Tim May 07 '15 at 19:47

4 Answers4

53

The good AnderWebs gave me an answer in Google+ but he is too laz.... emm busy to write it here again, so here is the short version: Extend the MapFragment class and override the onCreateView() method. After this method is done we can get a non-null reference to que GoogleMap object.

This is my particular solution:

public class MiniMapFragment extends SupportMapFragment {
    private LatLng mPosFija;

    public MiniMapFragment() {
        super();
    }

    public static MiniMapFragment newInstance(LatLng posicion){
        MiniMapFragment frag = new MiniMapFragment();
        frag.mPosFija = posicion;
        return frag;
    }

    @Override
    public View onCreateView(LayoutInflater arg0, ViewGroup arg1, Bundle arg2) {
        View v = super.onCreateView(arg0, arg1, arg2);
        initMap();
        return v;
    }

    private void initMap(){
        UiSettings settings = getMap().getUiSettings();
        settings.setAllGesturesEnabled(false);
        settings.setMyLocationButtonEnabled(false);

        getMap().moveCamera(CameraUpdateFactory.newLatLngZoom(mPosFija,16));
        getMap().addMarker(new MarkerOptions().position(mPosFija).icon(BitmapDescriptorFactory.fromResource(R.drawable.marker)));
    }
}

Now in the previous Fragment class I do

mMapFragment = MiniMapFragment.newInstance(new LatLng(37.4005502611301, -5.98233461380005));

Maybe it's not perfect yet, because the screen blinks when showing the map. But not sure if the problem is because of this or something else.

Gerstmann
  • 5,368
  • 6
  • 37
  • 57
Sloy
  • 2,975
  • 3
  • 20
  • 21
  • i am trying to the same but not able to to the solution http://stackoverflow.com/questions/13764266/android-supportmapfragment-exception-error – Harsha M V Dec 07 '12 at 14:06
  • I'm just wondering, why are you using a static function to return a new instance of MiniMapFragment ? Isn't it possible to take the LatLng position as an argument for the constructor ? – yasith Feb 04 '13 at 17:58
  • Yes, it's perfectly possible. It's just a matter of habit. – Sloy Feb 05 '13 at 22:37
  • 3
    @yasith it's a rule don't have fragment without empty constructor, this could bring nasty effect when trying to restore it. – Necronet Apr 28 '13 at 13:37
  • Indeed If you decide to use a constructor to create your fragments, make sure you always provide one without parameters. By using static methods you avoid this potential problem. – Sloy Apr 29 '13 at 14:20
  • Great job. (Buen trabajo amigo ;)) – Dani Oct 14 '13 at 11:04
  • 1
    Calling initMap() from onCreateView() will give you NullPointerException, because getMap() returns null. To make it work, move initMap() to onViewCreated. – G. Kh. Feb 11 '14 at 10:38
  • @Sloy, getMap() returns a valid map if called from onCreateView() or onViewCreated() but if I call it later then it returns null. Any ideas? – AndroidDev Apr 11 '14 at 10:19
  • The code should be moved to `onActivityCreated`, otherwise you'll get a NPE. – mbmc Jun 10 '14 at 18:28
  • @sloy can you edit your answer by providing manifest permissions and xml file. – Bhavesh Jethani Oct 04 '14 at 06:51
  • 2
    Thank you very much! Remark. [getMap() is deprecated](http://developer.android.com/reference/com/google/android/gms/maps/SupportMapFragment.html#getMap%28%29). Google suggest to use [getMapAsync()](http://developer.android.com/reference/com/google/android/gms/maps/SupportMapFragment.html#getMapAsync%28com.google.android.gms.maps.OnMapReadyCallback%29). Here are changes to your code. Transform `initMap` to `onMapReady`. Call `getMapAsync(this)` instead of `initMap`. Add that `MiniMapFragment` implements `OnMapReadyCallback`. – MyDogTom Dec 18 '14 at 11:53
  • can any buddy help me Please, how to use above solution inside view pager. I have tried this, but no success. – Ankita Sinha Jan 16 '15 at 09:18
  • @Sloy is there a way to send the google map fragment to back of the menu fragment (works great at the first few seconds, then "refresh" and is brought back to front therefore blocking the menu fragment)? – sitilge Feb 03 '15 at 13:17
26

Thanks, found this very helpful. Am posting my slightly modified solution, as it was cleaner for me to tell the parent Fragment when the map was ready. This method also works with a saveInstanceState / restoreInstanceState cycle.

public class CustomMapFragment extends SupportMapFragment {

    private static final String LOG_TAG = "CustomMapFragment";

    public CustomMapFragment() {
        super();

    }

    public static CustomMapFragment newInstance() {
        CustomMapFragment fragment = new CustomMapFragment();
        return fragment;
    }

    @Override
    public View onCreateView(LayoutInflater arg0, ViewGroup arg1, Bundle arg2) {
        View v = super.onCreateView(arg0, arg1, arg2);
        Fragment fragment = getParentFragment();
        if (fragment != null && fragment instanceof OnMapReadyListener) {
            ((OnMapReadyListener) fragment).onMapReady();
        }
        return v;
    }



    /**
     * Listener interface to tell when the map is ready
     */
    public static interface OnMapReadyListener {

        void onMapReady();
    }
}

To use as a nested Fragment:-

public class ParentFragment extends Fragment implements OnMapReadyListener {

    ...

    mMapFragment = CustomMapFragment.newInstance();
    getChildFragmentManager().beginTransaction().replace(R.id.mapContainer, mMapFragment).commit();

    @Override
    public void onMapReady() {
        mMap = mMapFragment.getMap();
    }
    ...
}

Hope it helps someone.

Ryan
  • 2,240
  • 17
  • 17
  • I have just added this code to my app, but the CustomMapFragment class complains that `getParentFragment()` is undefined! Any ideas why? – Darren Apr 21 '13 at 13:55
  • This is a great solution! Clean and abstracted as you can load any pins and other stuff from the parent activity! – speedynomads Jul 30 '13 at 17:39
  • can you paste your full code here?? i stuck in google map v2 when user switch the tab map reload again and again. – Ketan Mehta Aug 07 '13 at 05:31
  • Android guidelines advice that two fragments should not directly communicate with each other and all inter-fragment communication should be through an Activity. In this case, the child map fragment is directly communicating with the parent fragment through an interface - will this have any downsides? – Price Jul 24 '14 at 12:03
  • I understand why fragments should not directly communicate if they are are not nested. But since the support of child fragments it makes sense (at least to me) to have them communicate as the Activity is unaware of the child fragments. Unless you can point me to the guidelines that say otherwise, I would say it's safe to do so. – Ryan Jul 30 '14 at 09:36
  • What do you need this public CustomMapFragment() { super(); } for? – Marian Paździoch Sep 29 '15 at 08:25
15

Here's my solution to this, I took inspiration from the code previously posted and cleaned it up. I also added the static methods with and without the GoogleMapOptions parameters.

public class GoogleMapFragment extends SupportMapFragment {

    private static final String SUPPORT_MAP_BUNDLE_KEY = "MapOptions";

    public static interface OnGoogleMapFragmentListener {
        void onMapReady(GoogleMap map);
    }

    public static GoogleMapFragment newInstance() {
        return new GoogleMapFragment();
    }

    public static GoogleMapFragment newInstance(GoogleMapOptions options) {
        Bundle arguments = new Bundle();
        arguments.putParcelable(SUPPORT_MAP_BUNDLE_KEY, options);

        GoogleMapFragment fragment = new GoogleMapFragment();
        fragment.setArguments(arguments);
        return fragment;
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        try {
            mCallback = (OnGoogleMapFragmentListener) getActivity();
        } catch (ClassCastException e) {
            throw new ClassCastException(getActivity().getClass().getName() + " must implement OnGoogleMapFragmentListener");
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = super.onCreateView(inflater, container, savedInstanceState);
        if (mCallback != null) {
            mCallback.onMapReady(getMap());
        }
        return view;
    }

    private OnGoogleMapFragmentListener mCallback;
}

The usage pattern is as follows:

public class MyMapActivity implements OnGoogleMapFragmentListener {

    ...

    @Override
    public void onMapReady(GoogleMap map) {
        mUIGoogleMap = map;

        ...

    }

    ...

    private GoogleMap mUIGoogleMap;
}
Matteo Giaccone
  • 151
  • 1
  • 3
  • Just curious, since I'm pretty new to Android dev - is there a reason you declare your class properties at the bottom instead of the top? – ashack Jun 28 '13 at 16:08
  • I got used to it when reading the Android source code. I like to have important things on the top to save my finger from scrolling too much ;-) – Matteo Giaccone Jul 09 '13 at 18:45
  • 1
    Have been looking for this, thanks @MatteoGiaccone for posting it. I'm wondering about the _MapOptions_ key though. Is this documented somewhere? – Hassan Ibraheem Dec 17 '13 at 16:29
  • A big thank you, Matt! From your answer, I discovered that the trick is to call `getMap()` only after the line: `View view = super.onCreateView(inflater, container, savedInstanceState);` in the onCreateView() of the MapFragment has been executed. – Price Jul 24 '14 at 12:47
4

No need to cutomize SupportMapFragment you can do this directly by using following piece of code,

FragmentManager fm = getSupportFragmentManager(); // getChildFragmentManager inside fragments.
CameraPosition cp = new CameraPosition.Builder()
                    .target(initialLatLng) // your initial co-ordinates here. like, LatLng initialLatLng
                    .zoom(zoom_level)
                    .build();
SupportMapFragment mapFragment = SupportMapFragment.newInstance(new GoogleMapOptions().camera(cp));
fm.beginTransaction().replace(R.id.rl_map, mapFragment).commit();

Add this piece of code for layout

<RelativeLayout
       android:id="@+id/rl_map"
       android:layout_width="fill_parent"
       android:layout_height="fill_parent" />

This will load GoogleMap at particular Location directly i.e, initialLatLng.

Jaydipsinh Zala
  • 16,558
  • 7
  • 41
  • 45