带有 CameraUpdateFactory.newLatLngBounds 的 moveCamera 崩溃

Posted

技术标签:

【中文标题】带有 CameraUpdateFactory.newLatLngBounds 的 moveCamera 崩溃【英文标题】:moveCamera with CameraUpdateFactory.newLatLngBounds crashes 【发布时间】:2012-11-21 11:16:01 【问题描述】:

我正在使用新的android Google Maps API。

我创建了一个包含 MapFragment 的活动。在活动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)

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

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

【问题讨论】:

【参考方案1】:

您可以在 OnCameraChangeListener 中使用简单的 newLatLngBounds 方法。一切都将完美运行,您无需计算屏幕尺寸。此事件发生在地图大小计算之后(据我了解)。

例子:

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

【讨论】:

这是有效的,因为 setOnCameraChangeListener 保证在 Map 完成布局后至少运行一次?这是未来的证明吗? @SteelRat 这就是我反对使用该解决方案的原因。你永远不知道谷歌什么时候改变了这一点,或者即使它在所有安卓版本或设备上都以这种方式工作。 @SteelRat 我同意,但是在活动的生命周期中可能会多次调用它?我只是在论证,即使它在这种情况下有效,它也不是一个非常优雅的解决方案。我认为 OnGlobalLayoutListener/Treelistener 是一种更正确的方法。 也许在上面的代码之后添加 map.moveCamera(CameraUpdateFactory.scrollBy(1, 1)); 会成功吗? setOnCameraChangeListener 现已弃用【参考方案2】:

这些答案很好,但我会采用不同的方法,一种更简单的方法。 如果该方法只有在地图布局后才有效,那就等着吧:

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

【讨论】:

经过大量实验,我发现我需要实现此答案和 Daniele 上述答案的组合。有时地图还没有加载,有时布局还没有完成。在这两种情况发生之前,我无法随意移动相机。 此方法的警告,来自文档:“如果地图由于连接问题而从未加载,则不会触发此事件,或者如果地图不断变化并且由于用户不断与地图交互。” (强调我的)。 这会延迟超过必要的时间,因为它会等待地图图块加载到错误的位置,然后再移动到正确的位置。 大多数情况下需要 3-10 秒才能触发,这是不可接受的。 这里是移动相机的地方。我建议使用 map.animateCamera 而不是 map.moveCamera【参考方案3】:

好的,我解决了这个问题。正如documented here 那样,API 不能用于预布局。

正确使用的 API 描述为:

注意:只使用更简单的方法 newLatLngBounds(boundary, padding) 生成一个 CameraUpdate 如果它将用于移动 地图经过布局后的相机。在布局期间,API 计算需要的地图显示边界 正确投影边界框。相比之下,您可以使用 更复杂的方法返回的 CameraUpdate newLatLngBounds(boundary, width, height, padding) 任何时候,甚至 在地图进行布局之前,因为 API 会计算 显示您传递的参数的边界。

为了解决这个问题,我计算了我的屏幕尺寸并将宽度和高度提供给

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

这让我可以指定边界框预布局。

【讨论】:

但是如果地图没有占据整个屏幕呢? 在我的例子中,我只是假设了一个比实际更大的屏幕,并且需要更仔细地计算填充,所以实际上它没有太大。一个更简单的原因。 你能说明如何根据屏幕尺寸计算它吗? 这不会崩溃,但是要使用的宽度和高度接近于纯猜测,要绘制的确切区域确实无法预测。【参考方案4】:

解决方案比那更简单......

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

捕获IllegalStateException 并在视图上使用全局侦听器。 使用其他方法预先设置绑定大小(预布局)意味着您必须计算 THE VIEW 的大小,而不是屏幕设备。只有当您全屏查看地图并且不使用片段时,它们才会匹配。

【讨论】:

直到现在我一直在使用你的小sn-p,但它仍然失败......正确答案是第一个:-) 如何/何时失败?从来没有这个问题。第一个答案只是将硬编码的大小作为后备传递给它。 “它可以工作”就像“它不再崩溃”一样,但它没有做同样的事情。 @andrea.spot 我不记得为什么我说得更简单了。我想同时已对接受的解决方案进行了编辑。 当前 接受的解决方案假设您的地图占据了所有屏幕,但并非总是如此。如果你不属于这种情况,你要么需要知道地图的大小,要么使用我的方法或类似的方法。 也看看下面黄新刚的回答,加了一点。【参考方案5】:

这是我的解决方法,在这种情况下等到地图加载完毕。

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

【讨论】:

简单高效+1【参考方案6】:

接受的答案对我不起作用,因为我必须在应用程序的不同位置使用相同的代码。

我没有等待相机更换,而是根据 Lee 指定地图大小的建议创建了一个简单的解决方案。这是为了以防您的地图是屏幕的大小。

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

希望对其他人有所帮助!

【讨论】:

【参考方案7】:

添加和删除标记可以在布局完成前完成,但不能移动相机(除了使用newLatLngBounds(boundary, padding),如 OP 的回答中所述)。

执行初始相机更新的最佳位置可能是使用一次性OnGlobalLayoutListener,如Google's sample code 所示,例如请参阅 MarkerDemoActivity.javasetUpMap() 的以下摘录:

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

【讨论】:

我创建了一个gradlefied version of the Maps sample project。 我在方向更改时收到此错误。我也从示例代码中找到了这个,但这对我不起作用。我仍然遇到同样的错误。【参考方案8】:

正如 cmets 中所指出的,公认的答案有点老套。实施后,我注意到分析中的IllegalStateException 日志量高于可接受的数量。我使用的解决方案是添加一个OnMapLoadedCallback,其中执行CameraUpdate。回调被添加到GoogleMap。地图完全加载后,将执行相机更新。

这确实会导致地图在执行相机更新之前短暂显示缩小的 (0,0) 视图。我觉得这比导致崩溃或依赖无证行为更容易接受。

【讨论】:

【参考方案9】:
 map.moveCamera(CameraUpdateFactory.newLatLngZoom(bounds.getCenter(),10));

使用它对我有用

【讨论】:

这只是将地图居中而不是动态调整缩放到边界。 我们可以使用 "LatLngBounds.Builder" 计算更多 matkers 的边界。 @RameshBhupathi 但它仍将是一个带有中心和缩放的地图视图。【参考方案10】:

在极少数情况下,MapView 布局完成,但 GoogleMap 布局未完成,ViewTreeObserver.OnGlobalLayoutListener 本身无法阻止崩溃。我看到了 Google Play 服务包版本 5084032 的崩溃。这种罕见的情况可能是由于我的 MapView 可见性的动态变化引起的。

为了解决这个问题,我在onGlobalLayout()中嵌入了GoogleMap.OnMapLoadedCallback

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

【讨论】:

有趣:如果我将 mapView.getViewTreeObserver() 重构为 局部变量,则会发生以下 错误"IllegalStateException: This ViewTreeObserver is not alive ,再次调用getViewTreeObserver()”。你试过了吗? 如果 mapView.getViewTreeObserver().isAlive() 碰巧是假的,我会为 map.setOnMapLoadedCallback() 再放一个后备【参考方案11】:

我使用了适用于最新版本的 Google Maps SDK (9.6+) 并基于 onCameraIdleListener 的不同方法。正如我所看到的,它的回调方法onCameraIdle 总是在onMapReady 之后调用。所以我的方法看起来像这段代码(考虑到它放在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
    */

【讨论】:

【参考方案12】:

我已经创建了一种方法来将两个回调:onMapReady 和 onGlobalLayout 组合成一个单独的 observable,只有当这两个事件都被触发时才会发出。

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

【讨论】:

这实际上是解决这个问题的最干净的方法。我已经这样做了很长时间,即使在每天 1k 用户的应用程序中,分析也没有返回错误。 +1【参考方案13】:

好的,我面临同样的问题。我的片段带有 SupportmapFragment、ABS 和导航抽屉。我所做的是:

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...

顺便说一句,我在充气后返回之前从onCreateView 调用resetCamera()。 这样做是第一次捕获异常(虽然地图“获取大小”作为一种说法......)然后,其他时候我需要重置相机,地图已经有大小并通过填充来完成。

问题在documentation 中有解释,它说:

在地图更新之前,请勿使用此相机更新更改相机 经历了布局(为了使这种方法能够正确地确定 适当的边界框和缩放级别,地图必须有大小)。 否则将抛出 IllegalStateException。它不是 足以使地图可用(即getMap() 返回一个 非空对象);包含地图的视图也必须经过 布局,使其尺寸已确定。如果你不能 确保发生这种情况,请改用newLatLngBounds(LatLngBounds, int, int, int) 并手动提供地图的尺寸​​。

我认为这是一个相当不错的解决方案。 希望它可以帮助某人。

【讨论】:

布局完全加载时是否有任何监听器可以监听。 ? 是在newLatLngBounds()animateCamera() 中引发了异常,还是在两者中都引发了异常? catch-branch 不会导致新的异常吗?【参考方案14】:

如果您需要等待可能的用户交互开始,请使用OnMapLoadedCallback,如前面的答案中所述。但是,如果您只需要为地图提供默认位置,则不需要这些答案中概述的任何解决方案。 MapFragmentMapView都可以在开始时接受GoogleMapOptions,它可以提供默认位置。唯一的技巧是不要将它们直接包含在您的布局中,因为系统会在没有选项的情况下调用它们,而是动态初始化它们。

在你的布局中使用它:

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

并替换您的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();

除了启动速度更快之外,不会先显示世界地图,然后再移动相机。地图将直接从指定位置开始。

【讨论】:

【参考方案15】:

另一种方法类似于(假设您的最顶层视图是一个名为 rootContainer 的 FrameLayout,尽管只要您始终选择最顶层的容器,无论它具有哪种类型或名称,它都会起作用):

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

将您的相机功能修改为仅在 layoutDonetrue 时才可以解决您的所有问题,而无需添加额外的功能或将逻辑连接到 layoutListener 处理程序。

【讨论】:

【参考方案16】:

我发现它比其他解决方案更有效并且更简单:

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

这会在布局运行后再次尝试调用。可能包括一个安全性来避免无限循环。

【讨论】:

【参考方案17】:

为什么不直接使用这样的东西:

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

【讨论】:

为什么newLatLngBounds(builder.build(), padding)不在try-catch里面? moveCamera()中是否出现此异常?【参考方案18】:

您可以利用 Google Maps 存储库中的一个帮助程序类 - 它会等待布局和地图都准备好,然后再通过 GoogleMap 通知回调:

原文出处:

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

还有一个 Kotlin 实现:

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


【讨论】:

【参考方案19】:

如OnCameraChangeListener() is deprecated 中所述,setOnCameraChangeListener 现在已弃用。所以你应该用三种方法之一替换它:

GoogleMap.OnCameraMoveStartedListener GoogleMap.OnCameraMoveListener GoogleMap.OnCameraIdleListener

在我的例子中,我使用了 OnCameraIdleListener 并在里面删除了它,因为它在任何动作中被一次又一次地调用。

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)

更新

我在我的项目中删除了googleMap.setOnCameraIdleListener,因为有时在显示地图时不会调用它,但保留了googleMap.setOnCameraIdleListener(clusterManager)

【讨论】:

感谢您的回复。我已经想出了几乎相同的解决方案。 @ArslanMaqbool,谢谢!我现在正在处理中,如果您分享您的变体,我将不胜感激。 我很高兴@CoolMind【参考方案20】:

我在尝试调用 mMap.animateCamera 时遇到了同样的错误。我通过在我的 onMapReady 函数中调用 setOnMapLoadedCallback 回调来解决它,如下所示

public void onMapReady(GoogleMap googleMap) 
        mMap = googleMap;

   // other codes go there

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

这应该可以正常工作。

【讨论】:

以上是关于带有 CameraUpdateFactory.newLatLngBounds 的 moveCamera 崩溃的主要内容,如果未能解决你的问题,请参考以下文章

带有和不带有聚合的 sql 查询

如何翻转正面带有标签而背面带有另一个标签的视图 - 参见图片

CakePHP 如何处理带有/不带有 'id' 字段的 HABTM 表?

带有滚动的 Div 和带有绝对位置的内容

带有 RecyclerView 的 DialogFragment 比带有 Recyclerview 的 Fragment 慢

访问控制允许带有和不带有 www 的来源