moveCamera avec CameraUpdateFactory.newlatlngbounds se bloque
Je fais usage de la nouvelle API Android Google Maps .
Je crée une activité qui inclut un MapFragment. Dans l'activité onResume
, j'ai défini les marqueurs dans L'objet GoogleMap, puis défini une boîte englobante pour la carte qui inclut tous les marqueurs.
Cela utilise le pseudo code suivant:
LatLngBounds.Builder builder = new LatLngBounds.Builder();
while(data) {
LatLng latlng = getPosition();
builder.include(latlng);
}
CameraUpdate cameraUpdate = CameraUpdateFactory
.newLatLngBounds(builder.build(), 10);
map.moveCamera(cameraUpdate);
L'appel à map.moveCamera()
provoque le crash de mon application avec la pile suivante:
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-au lieu de la méthode d'usine newLatLngBounds()
j'utilise newLatLngZoom()
méthode alors le même piège ne se produit pas.
Le onResume
est-il le meilleur endroit pour dessiner les marqueurs sur L'objet GoogleMap ou devrais-je dessiner les marqueurs et définir la position de la caméra ailleurs?
17 réponses
Vous pouvez utiliser de simples newLatLngBounds méthode OnCameraChangeListener. Tout fonctionnera parfaitement et vous n'avez pas besoin de calculer la taille de l'écran. Cet événement se produit après le calcul de la taille de la carte (si je comprends bien).
Exemple:
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);
}
});
Ces réponses sont très bien, mais j'opterais pour une approche différente, une approche plus simple. Si la méthode ne fonctionne qu'une fois la carte affichée, attendez-la:
map.setOnMapLoadedCallback(new GoogleMap.OnMapLoadedCallback() {
@Override
public void onMapLoaded() {
map.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 30));
}
});
OK, je me suis débrouillé. Comme documenté ici cette API ne peut pas être utilisée avant la mise en page.
L'API correcte à utiliser est décrite comme suit:
Note: utilisez uniquement la méthode plus simple newLatLngBounds(boundary, padding) pour générer un CameraUpdate s'il va être utilisé pour déplacer le caméra après la carte a subi mise en page. Pendant la mise en page, L'API calcule les limites d'affichage de la carte qui sont nécessaires pour projeter correctement la boîte englobante. En comparaison, vous pouvez utiliser le CameraUpdate renvoyé par la méthode plus complexe newLatLngBounds (limite, largeur, hauteur, rembourrage) à tout moment, même avant que la carte ait subi la mise en page, car L'API calcule le afficher les limites à partir des arguments que vous passez.
Pour résoudre le problème, j'ai calculé la taille de mon écran et fourni la largeur et la hauteur à
public static CameraUpdate newLatLngBounds(
LatLngBounds bounds, int width, int height, int padding)
Cela m'a ensuite permis de spécifier la pré-mise en page de la boîte englobante.
La solution est plus simple que cela....
// 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 {[1] } et utilisez l'écouteur global sur la vue à la place. Définir la taille liée à l'avance (pré-mise en page) en utilisant les autres méthodes signifie que vous devez calculer la taille de la vue, pas le périphérique d'écran. Ils ne correspondent que si vous allez en plein écran avec votre carte et ne pas utiliser des fragments.
L'Ajout et la suppression de marqueurs peuvent être effectués avant la mise en page, mais le déplacement de la caméra ne peut pas être effectué (sauf en utilisant newLatLngBounds(boundary, padding)
comme indiqué dans la réponse de L'OP).
Probablement le meilleur endroit pour effectuer une mise à jour initiale de la caméra est d'utiliser un one-shot OnGlobalLayoutListener
comme indiqué dans exemple de code de Google, par exemple, voir l'extrait suivant de setUpMap()
dans 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));
}
});
}
C'est mon correctif, attendez que la carte soit chargée dans ce cas.
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));
}
});
}
La réponse acceptée ne fonctionnerait pas pour moi parce que je devais utiliser le même code à différents endroits de mon application.
Au lieu d'attendre que la caméra change, j'ai créé une solution simple basée sur la suggestion de Lee de spécifier la taille de la carte. C'est dans le cas où votre carte est la taille de l'écran.
// 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));
J'espère que ça aide quelqu'un d'autre!
La réponse acceptée est, comme indiqué dans les commentaires, un peu hacky. Après l'avoir implémenté, j'ai remarqué une quantité ci-dessus acceptable de journaux IllegalStateException
dans analytics. La solution que j'ai utilisée est d'ajouter un OnMapLoadedCallback
, dans lequel le CameraUpdate
est effectuée. Le rappel est ajouté au GoogleMap
. Une fois votre carte complètement chargée, la mise à jour de la caméra sera effectuée.
Cela fait que la carte affiche brièvement la vue agrandie (0,0) avant d'effectuer la mise à jour de la caméra. Je pense que c'est plus acceptable que provoquer des plantages ou s'appuyer sur un comportement non documenté.
map.moveCamera(CameraUpdateFactory.newLatLngZoom(bounds.getCenter(),10));
Utilisez ceci cela a fonctionné pour mee
Dans de très rares cas, la mise en page MapView est terminée, mais la mise en page GoogleMap n'est pas terminée, ViewTreeObserver.OnGlobalLayoutListener
lui-même ne peut pas arrêter le crash. J'ai vu le crash avec Google Play Services package Version 5084032. Ce cas rare peut être causé par le changement dynamique de la visibilité de mon MapView.
Pour résoudre ce problème, j'ai incorporé GoogleMap.OnMapLoadedCallback
dans 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));
}
});
}
}
});
}
J'ai utilisé une approche différente qui fonctionne pour les versions récentes de Google Maps SDK (9.6+) et basée sur onCameraIdleListener . Comme je le vois jusqu'à présent, c'est la méthode de rappel onCameraIdle
appelée toujours après onMapReady
. Donc, mon approche ressemble à ce morceau de code (considérant qu'il est mis en 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
*/
}
Ok, je suis confronté au même problème. J'ai mon fragment avec mon SupportmapFragment, ABS et tiroir de navigation. Ce que j'ai fait était:
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...
}
Et, au fait, j'appelle resetCamera()
de onCreateView
Après avoir gonflé et avant de revenir.
Ce que cela fait est d'attraper l'exception première fois (alors que la carte "obtient une taille" comme une façon de le dire...) et puis, d'autres fois, j'ai besoin de réinitialiser l'appareil photo, la carte a déjà la taille et le fait à travers le rembourrage.
Problème est expliqué dans documentation, il dit:
Ne changez pas la caméra avec cette mise à jour de la caméra tant que la carte n'a pas mise en page subie (pour que cette méthode détermine correctement l' zone de délimitation appropriée et niveau de zoom, la carte doit avoir une taille). Sinon, un
IllegalStateException
sera lancé. Il n'est PAS suffisante pour que la carte soit disponible (c'est-à-diregetMap()
renvoie un objet non null); la vue contenant la carte doit également avoir subi disposition telle que ses dimensions ont été déterminées. Si vous ne pouvez pas être assurer que cela s'est produit, UtiliseznewLatLngBounds(LatLngBounds, int, int, int)
à la place et fournissez les dimensions de la carte manuellement.
Je pense que c'est une solution assez décente. Espérons que cela aide quelqu'un.
J'ai créé un moyen de combiner les deux callbacks: onMapReady et onGlobalLayout en un seul observable qui n'émettra que lorsque les deux événements auront été déclenchés.
Https://gist.github.com/abhaysood/e275b3d0937f297980d14b439a8e0d4a
Si vous devez attendre que d'éventuelles interactions de l'utilisateur commencent, utilisez OnMapLoadedCallback
comme décrit dans les réponses précédentes. Mais, si tout ce dont vous avez besoin est de fournir un emplacement par défaut pour la carte, aucune des solutions décrites dans ces réponses n'est nécessaire. Les deux MapFragment
et MapView
peuvent accepter un GoogleMapOptions
Au début qui peut fournir l'emplacement par défaut tout droit. La seule astuce est de ne pas les inclure directement dans votre mise en page car le système les appellera sans les options alors mais de les initialiser dynamiquement.
Utilisez ceci dans votre mise en page:
<FrameLayout
android:id="@+id/map"
android:name="com.google.android.gms.maps.SupportMapFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
Et remplacer le fragment dans votre 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();
En plus d'être plus rapide à démarrer, il n'y aura pas de carte du monde affichée en premier et une caméra se déplace en second. La carte commencera par l'emplacement spécifié directement.
Une autre approche serait quelque chose comme (en supposant que votre vue la plus haute est un FrameLayout nommé rootContainer
, même si cela fonctionnera tant que vous choisissez toujours votre conteneur le plus haut quel que soit le type ou le nom qu'il a):
((FrameLayout)findViewById(R.id.rootContainer)).getViewTreeObserver()
.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
public void onGlobalLayout() {
layoutDone = true;
}
});
Modifier les fonctions de votre caméra pour ne fonctionner que si layoutDone
est true
résoudra tous vos problèmes sans avoir à ajouter des fonctions supplémentaires ou à connecter la logique au gestionnaire layoutListener
.
J'ai trouvé que cela fonctionnait et était plus simple que les autres 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);
}
});
}
}
Cela tente à nouveau l'appel après l'exécution de la mise en page. Pourrait probablement inclure une sécurité pour éviter une boucle infinie.
Pourquoi ne pas simplement utiliser quelque chose comme ceci:
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);
}