带有 CameraUpdateFactory 的 moveCamera 崩溃

我正在使用新的 Android 版谷歌地图 API

我创建了一个活动,其中包括一个地图片段。在活动 onResume中,我将标记设置到 GoogleMap 对象中,然后为包含所有标记的地图定义一个边界框。

它使用以下伪代码:

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

map.moveCamera()的调用会导致我的应用程序在以下堆栈中崩溃:

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)

如果-而不是 newLatLngBounds()工厂方法我使用 newLatLngZoom()方法,那么相同的陷阱不会发生。

onResume是在 GoogleMap 对象上绘制标记的最佳位置,还是我应该在其他地方绘制标记并设置摄像机位置?

85292 次浏览

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.

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);
}
});

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));
}
});
}

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.

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.

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.

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.

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));
}
});

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));
}
});
}
}
});
}

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.

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));
}
});
}
 map.moveCamera(CameraUpdateFactory.newLatLngZoom(bounds.getCenter(),10));

use this it worked for mee

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.

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!

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
*/
}

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

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);
}

As stated in OnCameraChangeListener() is deprecated, 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).

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);
}
}
}

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.