MoveCamera с ошибками CameraUpdateFactory.newLatLngBounds

Я использую новый 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)

Если - вместо метода newLatLngBounds() factory я использую метод newLatLngZoom(), то та же самая ловушка не возникает.

Является ли onResume лучшим местом для рисования маркеров на объекте GoogleMap или я должен рисовать маркеры и устанавливать положение камеры где-то еще?

Ответ 1

ОК, я это продумал. Поскольку здесь документировано, API не может быть использован для предварительной компоновки.

Правильный API для использования описывается как:

Примечание. Используйте только более простой метод newLatLngBounds (граница, дополнение) для создания CameraUpdate, если он будет использоваться для перемещения камера после того, как карта претерпела макет. Во время компоновки API вычисляет границы отображения карты, которые необходимы для правильно спроектируйте ограничивающий прямоугольник. Для сравнения вы можете использовать CameraUpdate возвращается более сложным методом newLatLngBounds (граница, ширина, высота, отступы) в любое время, даже до того, как карта претерпела макет, поскольку API вычисляет отображать границы из аргументов, которые вы передаете.

Чтобы устранить проблему, я вычислил размер экрана и предоставил ширину и высоту

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

Это позволило мне указать предварительную компоновку рамки.

Ответ 2

Вы можете использовать простой метод newLatLngBounds в OnCameraChangeListener. Все будет работать отлично, и вам не нужно вычислять размер экрана. Это событие происходит после вычисления размера карты (как я понимаю).

Пример:

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

Ответ 3

Эти ответы просто прекрасны, но я бы пошел на другой подход, более простой. Если метод работает только после того, как карта выложена, просто подождите:

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

Ответ 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));
            }
        });
    }
}

Catch IllegalStateException и вместо этого используйте глобальный прослушиватель. Установка размера привязки заранее (предварительная компоновка) с использованием других методов означает, что вам нужно вычислить размер ПРОСМОТРА, а не экранное устройство. Они соответствуют только тому, что вы идете в полноэкранном режиме с вашей картой и не используете фрагменты.

Ответ 5

Добавление и удаление маркеров может быть выполнено до завершения макета, но перемещение камеры невозможно (кроме использования newLatLngBounds(boundary, padding), как указано в ответе OP).

Вероятно, лучшее место для первоначального обновления камеры - использовать одноразовый OnGlobalLayoutListener, как показано в пример кода Google, например. см. следующий отрывок из setUpMap() в 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));
        }
    });
}

Ответ 6

Это мое исправление, просто подождите, пока карта не будет загружена в этом случае.

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

Ответ 7

Принятый ответ не будет работать для меня, потому что мне пришлось использовать тот же код в разных местах моего приложения.

Вместо того, чтобы ждать, пока камера изменится, я создал простое решение, основанное на предположении Ли, указать размер карты. Это в случае, если ваша карта является размером экрана.

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

Надеюсь, что это поможет кому-то еще!

Ответ 8

Принятый ответ, как указано в комментариях, немного взломан. После его реализации я заметил более приемлемое количество журналов IllegalStateException в аналитике. Решением, которое я использовал, является добавление OnMapLoadedCallback, в котором выполняется CameraUpdate. Обратный вызов добавляется к GoogleMap. После полной загрузки карты обновление камеры будет выполнено.

Это приводит к тому, что карта будет кратковременно показывать снимок (0,0) перед выполнением обновления камеры. Я считаю, что это более приемлемо, чем вызывать сбои или полагаться на недокументированное поведение.

Ответ 9

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

используйте это, чтобы он работал на mee

Ответ 10

В очень редких случаях макет MapView завершен, но макет GoogleMap не завершен, ViewTreeObserver.OnGlobalLayoutListener сам не может остановить сбой. Я видел сбой с пакетом услуг Google Play версии 5084032. Этот редкий случай может быть вызван динамическим изменением видимости моего MapView.

Чтобы решить эту проблему, я вложил GoogleMap.OnMapLoadedCallback в 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));
                    }
                });
            }
        }
    });
}

Ответ 11

Я использовал другой подход, который работает для последних версий SDK Google Maps (9.6+) и на основе onCameraIdleListener. Как я вижу до сих пор метод callback 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 в один единственный наблюдаемый, который будет испускать только тогда, когда оба события были запущены.

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

Ответ 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...
}

И, между прочим, я вызываю resetCamera() из onCreateView после раздувания и перед возвратом.
То, что это делает, заключается в том, чтобы сначала поймать исключение (в то время как карта "получает размер" как способ сказать это...), а затем, иногда мне нужно reset камеру, карта уже имеет размер и делает это через прописку.

Проблема объясняется в documentation, в ней говорится:

Не меняйте камеру с обновлением этой камеры, пока карта не будет (чтобы этот метод правильно определял соответствующий ограничивающий прямоугольник и уровень масштабирования, карта должна иметь размер). В противном случае будет выбрано значение IllegalStateException. Это не достаточное для того, чтобы карта была доступна (т.е. getMap() возвращает непустой объект); вид, содержащий карту, также должен был претерпеть чтобы его размеры были определены. Если вы не можете быть убедитесь, что это произошло, используйте newLatLngBounds(LatLngBounds, int, int, int) и укажите размеры карты вручную.

Я думаю, что это довольно приличное решение. Надеюсь, это поможет кому-то.

Ответ 14

Если вам нужно дождаться появления возможных взаимодействий с пользователем, используйте OnMapLoadedCallback, как описано в предыдущих ответах. Но если вам нужно указать местоположение карты по умолчанию, нет необходимости в каких-либо решениях, изложенных в этих ответах. Оба MapFragment и MapView могут принимать в начале GoogleMapOptions, которые могут обеспечить правильное расположение по умолчанию. Единственный трюк заключается не в том, чтобы включать их непосредственно в ваш макет, потому что система будет вызывать их без параметров, а затем инициализировать их динамически.

Используйте это в своем макете:

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

и замените фрагмент на 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

Другой подход будет примерно таким: (Предполагая, что ваш самый верхний вид - это FrameLayout с именем rootContainer, хотя он будет работать до тех пор, пока вы всегда выбираете свой самый верхний контейнер независимо от того, какой тип или имя он имеет):

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

Изменение функций вашей камеры работает только в том случае, если layoutDone true решит все ваши проблемы, не добавив дополнительных функций или подключив логику к обработчику 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);
}

Ответ 18

Как указано в OnCameraChangeListener() устарела, 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).

Ответ 19

В репозитории 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);
    }
}
}