Search code examples
androidgoogle-mapsscreen-rotationsupportmapfragment

SupportMapFragment map disappears on rotation


I am working with a map based application that creates an album view that includes a map and a web view. The issue occurs during rotation and had been working fine with com.android.support:support-v4:19.1, but is not working once I updated to 22.2.0.

The map appears as expected in the initial view, but after rotation, the map no longer appears. I verified the GoogleMap, container View (that holds the map), and SupportMapFragment are all valid.

While debugging, I decided to check the view of the SupportMapFragment to see if that might be the cause. After rotation, a call to getView() on the SupportMapFragment returns null (which I suspect is why the map does not appear, but I do not understand why it is valid initially and is null after rotation).

I am at a loss because as far as I can tell, I have a valid SupportMapFragment, a valid GoogleMap and a container view with the appropriate size (in this case 680x800). As I said above, with the previous version I was using 19.1, this code worked as expected.

Below are relevant snippets of the code:

SupportMapManager creation is in the EventReportAlbumActivity class

public class EventReportAlbumActivity extends FragmentActivity
{
   @Nullable protected SupportMapFragment m_mapFragment;

   @NotNull
   public SupportMapFragment getMapFragment ()
   {
     if( m_mapFragment == null )
     {
        GoogleMapOptions options = new GoogleMapOptions();
        options.compassEnabled( false );
        options.rotateGesturesEnabled( false );
        options.scrollGesturesEnabled( false );
        options.tiltGesturesEnabled( false );
        options.zoomControlsEnabled( false );
        options.zoomGesturesEnabled( false );

        m_mapFragment = SupportMapFragment.newInstance( options );
        m_mapFragment.setRetainInstance( true );
     }
     return m_mapFragment;
  }
}

The actual Fragment instance that displays the map and web view

public class EventAlbumItemFragment extends Fragment
{
   @Nullable
   protected SupportMapFragment m_mapFragment;

  public static EventAlbumItemFragment newInstance ( )
  {
     final EventAlbumItemFragment fragment = new EventAlbumItemFragment();
     fragment.setRetainInstance( true );

     return fragment;
  }

    @Override
    public View onCreateView ( @NotNull LayoutInflater inflater,
                               @NotNull ViewGroup container,
                               @Nullable Bundle savedInstanceState )
    {
       final View view = inflater.inflate( R.layout.event_album_item, container, false );

       View mapContainer = view.findViewById( R.id.mapview_container );

       // make map fragment view invisible until we configure it so the coordinates will be correct
       mapContainer.setVisibility( View.INVISIBLE );

       // other code removed for brevity...
   }

  /**
   * Setter for the map fragment and its UI
   *
   * @param mapFragment The map fragment which should be added to this fragment's view, or null to remove the current fragment
   */
  public void setMapFragment ( @Nullable SupportMapFragment mapFragment )
  {
     final FragmentManager fragmentManager = getChildFragmentManager();

     if( m_mapFragment != null && m_mapFragment.isAdded() )
     {
        // Remove the existing map manager
        fragmentManager.beginTransaction()
           .remove( m_mapFragment )
           .commit();
     }

     if( mapFragment != null )
     {
        // Add the map view to the current view
        fragmentManager.beginTransaction()
           .add( R.id.mapview_container, mapFragment, MAP_FRAGMENT_TAG )
           .commit();
     }

     fragmentManager.executePendingTransactions();

     m_mapFragment = mapFragment;
     configureMap();
  }

  protected void configureMap ()
  {
     if ( m_mapFragment != null && m_eventMapFeature != null && m_eventMapFeature.haveValidLocation() == true )
     {
        final EventAlbumItemFragment localThis = this ;

        m_mapFragment.getMapAsync( new OnMapReadyCallback()
        {
           @Override
           public void onMapReady( GoogleMap googleMap )
           {
              final View mapContainer = getView().findViewById( R.id.mapview_container );
              final int mapWidth = mapContainer.getWidth();
              final int mapHeight = mapContainer.getHeight();

              // we have our map and the coordinates should be correct. Make map visible and proceed
              mapContainer.setVisibility( View.VISIBLE );

              // This line below crashes because view is NULL after rotation. 
              // Left here to illustrate what I believe to be the root cause of the issue
              // m_mapFragment.getView().setVisibility( View.VISIBLE );

              // Remove all overlays from the map
              googleMap.clear();

              final DisplayMetrics displayMetrics = getResources().getDisplayMetrics();

              final float zoom = (float)GeoUtils.getZoomForMetersWide( MAP_MINIMUM_METER_SPAN,
                      mapWidth / displayMetrics.scaledDensity,
                      m_eventMapFeature.getCoordinate().latitude );

              // Change the map bounds
              CameraUpdate cameraUpdate;

              // draw some items on the map
              final LatLngBounds bounds = m_eventMapFeature.getBounds();

              cameraUpdate = CameraUpdateFactory.newLatLngZoom( bounds.southwest, zoom );

              googleMap.moveCamera( cameraUpdate );
           }
        } );
     }
  }

  /** Assign the map fragment variable on resume. This is necessary for orientation change events */
  @Override
  public void onResume ()
  {
     super.onResume();

     // If the map fragment hasn't been set, but this is the current item, get the map fragment from the parent
    final EventReportAlbumActivity activity = (EventReportAlbumActivity)getActivity();

      if( m_mapFragment == null && this == activity.getCurrentItemFragment() )
      {
         setMapFragment( activity.getMapFragment() );
      }

     //
     configureMap();
  }

}

UPDATE: setMapFragment above also crashes when there are multiple pages and the user pages through the album after rotation

After some more testing, I also tested this code when there are multiple pages in the album. Sans rotation, I can scroll through an album with no problems. However, if I rotate the device and attempt to scroll through the album, the app crashes. Again, using version 19 of the support library, this code works as expected. This only started happening after upgrading to version 22.

in the method setMapFragment() in EventAlbumItemFragment:

fragmentManager.executePendingTransactions();

I get the following stack trace:

java.lang.IllegalStateException: Could not execute method of the activity
    at android.view.View$1.onClick(View.java:4020)
    at android.view.View.performClick(View.java:4780)
    at android.view.View$PerformClick.run(View.java:19866)
    at android.os.Handler.handleCallback(Handler.java:739)
    at android.os.Handler.dispatchMessage(Handler.java:95)
    at android.os.Looper.loop(Looper.java:135)
    at android.app.ActivityThread.main(ActivityThread.java:5254)
    at java.lang.reflect.Method.invoke(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:372)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698)
Caused by: java.lang.reflect.InvocationTargetException
    at java.lang.reflect.Method.invoke(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:372)
    at android.view.View$1.onClick(View.java:4015)
    at android.view.View.performClick(View.java:4780)
    at android.view.View$PerformClick.run(View.java:19866)
    at android.os.Handler.handleCallback(Handler.java:739)
    at android.os.Handler.dispatchMessage(Handler.java:95)
    at android.os.Looper.loop(Looper.java:135)
    at android.app.ActivityThread.main(ActivityThread.java:5254)
    at java.lang.reflect.Method.invoke(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:372)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698)
Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.Object java.util.ArrayList.set(int, java.lang.Object)' on a null object reference
    at android.support.v4.app.FragmentManagerImpl.makeInactive(FragmentManager.java:1192)
    at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1099)
    at android.support.v4.app.FragmentManagerImpl.removeFragment(FragmentManager.java:1235)
    at android.support.v4.app.BackStackRecord.run(BackStackRecord.java:710)
    at android.support.v4.app.FragmentManagerImpl.execPendingActions(FragmentManager.java:1501)
    at android.support.v4.app.FragmentManagerImpl.executePendingTransactions(FragmentManager.java:490)
    at crc.carsapp.fragments.MapAlbumItemFragment.setMapFragment(MapAlbumItemFragment.java:78)
    at crc.carsapp.listeners.OnMapAlbumScrollListener.onPageSelected(OnMapAlbumScrollListener.java:46)
    at crc.carsapp.listeners.OnEventAlbumViewScrollListener.onPageSelected(OnEventAlbumViewScrollListener.java:34)
    at android.support.v4.view.ViewPager.dispatchOnPageSelected(ViewPager.java:1786)
    at android.support.v4.view.ViewPager.scrollToItem(ViewPager.java:568)
    at android.support.v4.view.ViewPager.setCurrentItemInternal(ViewPager.java:552)
    at android.support.v4.view.ViewPager.setCurrentItemInternal(ViewPager.java:513)
    at android.support.v4.view.ViewPager.setCurrentItem(ViewPager.java:505)
    at crc.carsapp.activities.AlbumActivity.scrollToPrevious(AlbumActivity.java:133)
    at java.lang.reflect.Method.invoke(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:372)
    at android.view.View$1.onClick(View.java:4015)
    at android.view.View.performClick(View.java:4780)
    at android.view.View$PerformClick.run(View.java:19866)
    at android.os.Handler.handleCallback(Handler.java:739)
    at android.os.Handler.dispatchMessage(Handler.java:95)
    at android.os.Looper.loop(Looper.java:135)
    at android.app.ActivityThread.main(ActivityThread.java:5254)
    at java.lang.reflect.Method.invoke(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:372)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698)

Solution

  • So, there were 2 issues with the above code. First (and the most critical), the Activity (EventReportAlbumActivity) is responsible for the creation of the map fragment (and its associated fragments keep a reference to it). During rotation, the activity is (of course) destroyed and recreated. Thus, subsequent calls to getMapFragment will generate a new SupportMapFragment. Meanwhile, the fragments displaying the map still have a reference to the prior map fragment.

    Whenever the call to remove was being made in setMapFragment, it was referencing the old SupportMapFragment and thus throwing an exception.

    This also caused the map to not display properly after rotation.

    The fix is to take out the call to remove in setMapFragment (the ViewPager handles the remove from the prior page in onPageSelected) and to add the 'new' SupportMapFragment in setMapFragment as needed.

    How the original code was working before the upgrade to version 22 is a mystery as this appears to be a bug no matter the support version. And, the solution still does not feel quite right as SupportMapFragment has its setRetainInstance set to true which implies we want it to live throughout the lifecycle of the fragment, but the activity winds up overwriting it when it is recreated during rotation. So, if someone has a more elegant solution, I would love to hear it.

    Relevant updated code is below:

    public class EventAlbumItemFragment extends Fragment
    {
       public void setMapFragment ( @Nullable SupportMapFragment mapFragment )
      {
         final FragmentManager fragmentManager = getChildFragmentManager();
    
         // The working assumption is that our map fragment, if already added, is added to
         // our current fragment.
         if ( mapFragment != null && mapFragment.isAdded() == false )
         {
            // Add the map view to the current view
            fragmentManager.beginTransaction()
               .add( R.id.mapview_container, mapFragment, MAP_FRAGMENT_TAG )
               .commit() ;
            fragmentManager.executePendingTransactions();
         }
    
         m_mapFragment = mapFragment;
         configureMap();
      }
    
      @Override
      public void onResume ()
      {
         super.onResume();
    
         // We can get here in (at least) 3 different ways:
         // 1. The first time this fragment is created.
         //       In this case, we will not have a local reference to a map fragment and will will add
         //       it if we are the currently displaying fragment.
         // 2. After rotation
         //       Because the actual map fragment is maintained by our activity, the map fragment will
         //       have been recreated and our reference to it will no longer be valid.
         // 3. After a pause event (such as the device going into power saving mode).
         //       We should still have a reference to the current map fragment and it should be
         //       valid. We can test that by checking if the map fragment is attached. This is done
         //       in the setMapFragment() method.
         //
         final AlbumActivity activity = (AlbumActivity)getActivity();
    
         Fragment currentFragment = activity.getCurrentItemFragment() ;
    
         if ( m_mapFragment == null && this == currentFragment )
         {
            setMapFragment( activity.getMapFragment() );
         }
         else if ( m_mapFragment != null )
         {
            setMapFragment( activity.getMapFragment() );
         }
    
         configureMap();
       }
    }