moveCamera with CameraUpdateFactory.newLatLngBounds crashes

AndroidAndroid MapviewGoogle Maps-Android-Api-2

Android Problem Overview


I'm making use of the new Android Google Maps API.

I create an activity which includes a MapFragment. In the activity onResume I set the markers into the GoogleMap object and then define a bounding box for the map which includes all of the markers.

This is using the following pseudo code:

LatLngBounds.Builder builder = new LatLngBounds.Builder();
while(data) {
   LatLng latlng = getPosition();
   builder.include(latlng);
}
CameraUpdate cameraUpdate = CameraUpdateFactory
   .newLatLngBounds(builder.build(), 10);
map.moveCamera(cameraUpdate);

The call to map.moveCamera() causes my application to crash with the following stack:

Caused by: java.lang.IllegalStateException: 
    Map size should not be 0. Most likely, layout has not yet 

    at maps.am.r.b(Unknown Source)
    at maps.y.q.a(Unknown Source)
    at maps.y.au.a(Unknown Source)
    at maps.y.ae.moveCamera(Unknown Source)
    at com.google.android.gms.maps.internal.IGoogleMapDelegate$Stub
        .onTransact(IGoogleMapDelegate.java:83)
    at android.os.Binder.transact(Binder.java:310)
    at com.google.android.gms.maps.internal.IGoogleMapDelegate$a$a
        .moveCamera(Unknown Source)
    at com.google.android.gms.maps.GoogleMap.moveCamera(Unknown Source)
    at ShowMapActivity.drawMapMarkers(ShowMapActivity.java:91)
    at ShowMapActivity.onResume(ShowMapActivity.java:58)
    at android.app.Instrumentation
        .callActivityOnResume(Instrumentation.java:1185)
    at android.app.Activity.performResume(Activity.java:5182)
    at android.app.ActivityThread
        .performResumeActivity(ActivityThread.java:2732)

If - instead of the newLatLngBounds() factory method I use newLatLngZoom() method then the same trap does not occur.

Is the onResume the best place to draw the markers onto the GoogleMap object or should I be drawing the markers and setting the camera position somewhere else?

Android Solutions


Solution 1 - Android

You can use simple newLatLngBounds method in OnCameraChangeListener. All will be working perfectly and you don't need to calculate screen size. This event occurs after map size calculation (as I understand).

Example:

map.setOnCameraChangeListener(new OnCameraChangeListener() {
		
    @Override
    public void onCameraChange(CameraPosition arg0) {
        // Move camera.
        map.moveCamera(CameraUpdateFactory.newLatLngBounds(builder.build(), 10));
        // Remove listener to prevent position reset on camera move.
        map.setOnCameraChangeListener(null);
    }
});

Solution 2 - Android

Those answers are just fine, but I would go for a different approach, a simpler one. If the method only works after the map is layed out, just wait for it:

map.setOnMapLoadedCallback(new GoogleMap.OnMapLoadedCallback() {
    @Override
    public void onMapLoaded() {
        map.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 30));
    }
});

Solution 3 - Android

OK I worked this out. As documented here that API can't be used pre-layout.

The correct API to use is described as:

> Note: Only use the simpler method newLatLngBounds(boundary, padding) > to generate a CameraUpdate if it is going to be used to move the > camera after the map has undergone layout. During layout, the API > calculates the display boundaries of the map which are needed to > correctly project the bounding box. In comparison, you can use the > CameraUpdate returned by the more complex method > newLatLngBounds(boundary, width, height, padding) at any time, even > before the map has undergone layout, because the API calculates the > display boundaries from the arguments that you pass.

To fix the problem I calculated my screen size and provided the width and height to

public static CameraUpdate newLatLngBounds(
    LatLngBounds bounds, int width, int height, int padding)

This then allowed me to specify the bounding box pre-layout.

Solution 4 - Android

The solution is simpler than that....

// Pan to see all markers in view.
try {
	this.gmap.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 50));
} catch (IllegalStateException e) {
	// layout not yet initialized
	final View mapView = getFragmentManager()
       .findFragmentById(R.id.map).getView();
	if (mapView.getViewTreeObserver().isAlive()) {
		mapView.getViewTreeObserver().addOnGlobalLayoutListener(
        new OnGlobalLayoutListener() {
			@SuppressWarnings("deprecation")
			@SuppressLint("NewApi")
			// We check which build version we are using.
			@Override
			public void onGlobalLayout() {
				if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
					mapView.getViewTreeObserver()
					    .removeGlobalOnLayoutListener(this);
				} else {
					mapView.getViewTreeObserver()
					    .removeOnGlobalLayoutListener(this);
				}
				gmap.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 50));
			}
		});
	}
}

Catch IllegalStateException and use the global listener on the view instead. Setting the bound size in advance (pre-layout) using the other methods means you have to compute the size of THE VIEW, not the screen device. They only match if you go full screen with your map and do not use fragments.

Solution 5 - Android

This is my fix, just wait untill the map is loaded in that case.

final int padding = getResources().getDimensionPixelSize(R.dimen.spacing);    

try {
    mMap.animateCamera(CameraUpdateFactory.newLatLngBounds(pCameraBounds, padding));
} catch (IllegalStateException ise) {

	mMap.setOnMapLoadedCallback(new GoogleMap.OnMapLoadedCallback() {

	    @Override
		public void onMapLoaded() {
			mMap.animateCamera(CameraUpdateFactory.newLatLngBounds(pCameraBounds, padding));
		}
	});
}

Solution 6 - Android

The accepted answer wouldn't work for me because I had to use the same code in various places of my app.

Instead of waiting the camera to change, I created a simple solution based on Lee's suggestion of specifying the map size. This is in case your map is the size of the screen.

// Gets screen size
int width = getResources().getDisplayMetrics().widthPixels;
int height = getResources().getDisplayMetrics().heightPixels;
// Calls moveCamera passing screen size as parameters
map.moveCamera(CameraUpdateFactory.newLatLngBounds(builder.build(), width, height, 10));

Hope it helps someone else!

Solution 7 - Android

Adding and removing markers can be done pre-layout completion, but moving the camera cannot (except using newLatLngBounds(boundary, padding) as noted in the OP's answer).

Probably the best place to perform an initial camera update is using a one-shot OnGlobalLayoutListener as shown in Google's sample code, e.g. see the following excerpt from setUpMap() in MarkerDemoActivity.java:

// Pan to see all markers in view.
// Cannot zoom to bounds until the map has a size.
final View mapView = getSupportFragmentManager()
    .findFragmentById(R.id.map).getView();
if (mapView.getViewTreeObserver().isAlive()) {
    mapView.getViewTreeObserver().addOnGlobalLayoutListener(
    new OnGlobalLayoutListener() {
        @SuppressLint("NewApi") // We check which build version we are using.
        @Override
        public void onGlobalLayout() {
            LatLngBounds bounds = new LatLngBounds.Builder()
                    .include(PERTH)
                    .include(SYDNEY)
                    .include(ADELAIDE)
                    .include(BRISBANE)
                    .include(MELBOURNE)
                    .build();
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
              mapView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
            } else {
              mapView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            }
            mMap.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 50));
        }
    });
}

Solution 8 - Android

The accepted answer is, as pointed out in the comments, a little hacky. After implementing it I noticed an above-acceptable amount of IllegalStateException logs in analytics. The solution I used is to add an OnMapLoadedCallback in which the CameraUpdate is performed. The callback is added to the GoogleMap. After your map fully loads the camera update will be performed.

This does cause the map to briefly show the zoomed out (0,0) view before performing the camera update. I feel that this is more acceptable than causing crashes or relying on undocumented behavior though.

Solution 9 - Android

 map.moveCamera(CameraUpdateFactory.newLatLngZoom(bounds.getCenter(),10));

use this it worked for mee

Solution 10 - Android

In very rare cases, MapView layout is finished, but GoogleMap layout is not finished, ViewTreeObserver.OnGlobalLayoutListener itself can't stop the crash. I saw the crash with Google Play services package version 5084032. This rare case may be caused by the dynamic change of the visibility of my MapView.

To solve this problem, I embedded GoogleMap.OnMapLoadedCallback in onGlobalLayout(),

if (mapView.getViewTreeObserver().isAlive()) {
    mapView.getViewTreeObserver().addOnGlobalLayoutListener(
    new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        @SuppressWarnings("deprecation")
        public void onGlobalLayout() {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
                mapView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
            } else {
                mapView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            }
            try {
                map.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 5));
            } catch (IllegalStateException e) {
                map.setOnMapLoadedCallback(new GoogleMap.OnMapLoadedCallback() {
                    @Override
                    public void onMapLoaded() {
                        Log.d(LOG_TAG, "move map camera OnMapLoadedCallback");
                        map.moveCamera(CameraUpdateFactory
                            .newLatLngBounds(bounds, 5));
                    }
                });
            }
        }
    });
}

Solution 11 - Android

I used different approach which works for recent versions of Google Maps SDK (9.6+) and based on onCameraIdleListener. As I see so far it's callback method onCameraIdle called always after onMapReady. So my approach looks like this piece of code (considering it put in Activity):

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // set content view and call getMapAsync() on MapFragment
}

@Override
public void onMapReady(GoogleMap googleMap) {
    map = googleMap;
    map.setOnCameraIdleListener(this);
    // other initialization stuff
}

@Override
public void onCameraIdle() {
    /* 
       Here camera is ready and you can operate with it. 
       you can use 2 approaches here:

      1. Update the map with data you need to display and then set
         map.setOnCameraIdleListener(null) to ensure that further events
         will not call unnecessary callback again.

      2. Use local boolean variable which indicates that content on map
         should be updated
    */
}

Solution 12 - Android

I've created a way to combine the two callbacks: onMapReady and onGlobalLayout into one single observable which will emit only when both the events have been triggered.

https://gist.github.com/abhaysood/e275b3d0937f297980d14b439a8e0d4a

Solution 13 - Android

Ok I'm facing same issue. I have my fragment with my SupportmapFragment, ABS, and navigation drawer. What I did was:

public void resetCamera() {

	LatLngBounds.Builder builderOfBounds = new LatLngBounds.Builder();
	// Set boundaries ...
	LatLngBounds bounds = builderOfBounds.build();
	CameraUpdate cu;
	try{
    	cu = CameraUpdateFactory.newLatLngBounds(bounds,10);
        // This line will cause the exception first times 
        // when map is still not "inflated"
    	map.animateCamera(cu); 
		System.out.println("Set with padding");
	} catch(IllegalStateException e) {
		e.printStackTrace();
    	cu = CameraUpdateFactory.newLatLngBounds(bounds,400,400,0);
    	map.animateCamera(cu);
		System.out.println("Set with wh");
	}
	
	//do the rest...
}

And, by the way, I'm calling resetCamera() from onCreateView after inflating and before returning.
What this does is catch the exception first time (while map "gets a size" as a way of saying it...) and then, other times I need to reset the camera, map already has size and does it through padding.

Issue is explained in documentation, it says:

> Do not change the camera with this camera update until the map has > undergone layout (in order for this method to correctly determine the > appropriate bounding box and zoom level, the map must have a size). > Otherwise an IllegalStateException will be thrown. It is NOT > sufficient for the map to be available (i.e. getMap() returns a > non-null object); the view containing the map must have also undergone > layout such that its dimensions have been determined. If you cannot be > sure that this has occured, use newLatLngBounds(LatLngBounds, int, int, int) instead and provide the dimensions of the map manually.

I think it's a pretty decent solution. Hope it helps someone.

Solution 14 - Android

If you need to wait for possible user interactions to start, use OnMapLoadedCallbackas described in the previous answers. But, if all you need is to provide a default location for the map, there is no need for any of the solutions outlined in those answers. Both MapFragmentand MapView can accept a GoogleMapOptions at start that can provide the default location all right. The only trick is not to include them directly in your layout because the system will call them without the options then but to initialize them dynamically.

Use this in your layout:

<FrameLayout
    android:id="@+id/map"
    android:name="com.google.android.gms.maps.SupportMapFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

and replace the fragment in your onCreateView():

GoogleMapOptions options = new GoogleMapOptions();
options.camera(new CameraPosition.Builder().target(location).zoom(15).build());
// other options calls if required

SupportMapFragment fragment = (SupportMapFragment) getFragmentManager().findFragmentById(R.id.map);
if (fragment == null) {
  FragmentTransaction transaction = getFragmentManager().beginTransaction();
  fragment = SupportMapFragment.newInstance(options);
  transaction.replace(R.id.map, fragment).commit();
  getFragmentManager().executePendingTransactions();
}
if (fragment != null)
  GoogleMap map = fragment.getMap();

Besides being faster to start, there will be no world map shown first and a camera move second. The map will start with the specified location directly.

Solution 15 - Android

Another approach would something like (Assuming your topmost view is a FrameLayout named rootContainer, even though it will work as long as you always choose your topmost container no matter which type or name it has):

((FrameLayout)findViewById(R.id.rootContainer)).getViewTreeObserver()
    .addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
		public void onGlobalLayout() {
			layoutDone = true;
		}
	});

Modifying your camera functions to only work if layoutDone is true will solve all your problems without having to add extra functions or wire up logic to the layoutListener handler.

Solution 16 - Android

I found this to work and be more simple than the other solutions:

private void moveMapToBounds(final CameraUpdate update) {
	try {
		if (movedMap) {
			// Move map smoothly from the current position.
			map.animateCamera(update);
		} else {
			// Move the map immediately to the starting position.
			map.moveCamera(update);
			movedMap = true;
		}
	} catch (IllegalStateException e) {
		// Map may not be laid out yet.
		getWindow().getDecorView().post(new Runnable() {
			@Override
			public void run() {
				moveMapToBounds(update);
			}
		});
	}
}

This tries the call again after layout has run. Could probably include a safety to avoid an infinite loop.

Solution 17 - Android

Why not just use something like this:

CameraUpdate cameraUpdate = CameraUpdateFactory.newLatLngBounds(builder.build(), padding);
try {
	map.moveCamera(cameraUpdate);
} catch (Exception e) {
	int width = getResources().getDisplayMetrics().widthPixels;
	int height = getResources().getDisplayMetrics().heightPixels;
	cameraUpdate = CameraUpdateFactory.newLatLngBounds(builder.build(), width, height, padding);
	map.moveCamera(cameraUpdate);
}

Solution 18 - Android

As stated in https://stackoverflow.com/questions/38727517/oncamerachangelistener-is-deprecated/38732283, setOnCameraChangeListener is now deprecated. So you should replace it with one of three mtehods:

  • GoogleMap.OnCameraMoveStartedListener
  • GoogleMap.OnCameraMoveListener
  • GoogleMap.OnCameraIdleListener

In my case, I used OnCameraIdleListener and inside I removed it because it was invoked again and again on any movement.

googleMap.setOnCameraIdleListener {
    googleMap.setOnCameraIdleListener(null) // It removes the listener.
    googleMap.moveCamera(track)
    googleMap.cameraPosition
    clusterManager!!.cluster()
    // Add another listener to make ClusterManager correctly zoom clusters and markers.
    googleMap.setOnCameraIdleListener(clusterManager)
}

UPDATE

I removed googleMap.setOnCameraIdleListener in my project, because it wasn't called sometimes when a map was shown, but retained googleMap.setOnCameraIdleListener(clusterManager).

Solution 19 - Android

There is a helper class in the Google Maps repo that you can leverage - it waits for both the layout and map to be ready before notifying a callback with the GoogleMap:

The original source is here:

https://github.com/googlemaps/android-samples/blob/7ee737b8fd6d39c77f8b3716ba948e1ce3730e61/ApiDemos/java/app/src/main/java/com/example/mapdemo/OnMapAndViewReadyListener.java

There is a Kotlin implementation too:

https://github.com/googlemaps/android-samples/blob/master/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/OnMapAndViewReadyListener.kt

public class OnMapAndViewReadyListener implements OnGlobalLayoutListener, OnMapReadyCallback {

/** A listener that needs to wait for both the GoogleMap and the View to be initialized. */
public interface OnGlobalLayoutAndMapReadyListener {
    void onMapReady(GoogleMap googleMap);
}

private final SupportMapFragment mapFragment;
private final View mapView;
private final OnGlobalLayoutAndMapReadyListener devCallback;

private boolean isViewReady;
private boolean isMapReady;
private GoogleMap googleMap;

public OnMapAndViewReadyListener(
        SupportMapFragment mapFragment, OnGlobalLayoutAndMapReadyListener devCallback) {
    this.mapFragment = mapFragment;
    mapView = mapFragment.getView();
    this.devCallback = devCallback;
    isViewReady = false;
    isMapReady = false;
    googleMap = null;

    registerListeners();
}

private void registerListeners() {
    // View layout.
    if ((mapView.getWidth() != 0) && (mapView.getHeight() != 0)) {
        // View has already completed layout.
        isViewReady = true;
    } else {
        // Map has not undergone layout, register a View observer.
        mapView.getViewTreeObserver().addOnGlobalLayoutListener(this);
    }

    // GoogleMap. Note if the GoogleMap is already ready it will still fire the callback later.
    mapFragment.getMapAsync(this);
}

@Override
public void onMapReady(GoogleMap googleMap) {
    // NOTE: The GoogleMap API specifies the listener is removed just prior to invocation.
    this.googleMap = googleMap;
    isMapReady = true;
    fireCallbackIfReady();
}

@SuppressWarnings("deprecation")  // We use the new method when supported
@SuppressLint("NewApi")  // We check which build version we are using.
@Override
public void onGlobalLayout() {
    // Remove our listener.
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
        mapView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
    } else {
        mapView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
    }
    isViewReady = true;
    fireCallbackIfReady();
}

private void fireCallbackIfReady() {
    if (isViewReady && isMapReady) {
        devCallback.onMapReady(googleMap);
    }
}
}

Solution 20 - Android

I had the same error trying to call mMap.animateCamera. I solved it by calling setOnMapLoadedCallback callback in my onMapReady function as shown below

public void onMapReady(GoogleMap googleMap) {
        mMap = googleMap;

   // other codes go there

    mMap.setOnMapLoadedCallback(() -> {
            //Your code where exception occurs goes here...
            
        });
}

This should work fine.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionLeeView Question on Stackoverflow
Solution 1 - AndroidPaul AnnekovView Answer on Stackoverflow
Solution 2 - AndroidLeandroView Answer on Stackoverflow
Solution 3 - AndroidLeeView Answer on Stackoverflow
Solution 4 - AndroidDaniele SegatoView Answer on Stackoverflow
Solution 5 - Androidpl3kn0rView Answer on Stackoverflow
Solution 6 - AndroidTeo InkeView Answer on Stackoverflow
Solution 7 - AndroidstephentView Answer on Stackoverflow
Solution 8 - AndroidtheblangView Answer on Stackoverflow
Solution 9 - AndroidRamesh BhupathiView Answer on Stackoverflow
Solution 10 - AndroidAlpha HuangView Answer on Stackoverflow
Solution 11 - AndroidViacheslavView Answer on Stackoverflow
Solution 12 - AndroidAbhay SoodView Answer on Stackoverflow
Solution 13 - AndroidunmultimedioView Answer on Stackoverflow
Solution 14 - AndroidGáborView Answer on Stackoverflow
Solution 15 - AndroidMachinariusView Answer on Stackoverflow
Solution 16 - AndroidJohn PattersonView Answer on Stackoverflow
Solution 17 - AndroidHristo LalevView Answer on Stackoverflow
Solution 18 - AndroidCoolMindView Answer on Stackoverflow
Solution 19 - Androiddazza5000View Answer on Stackoverflow
Solution 20 - AndroidPaul KumchiView Answer on Stackoverflow