web-dev-qa-db-fra.com

Décalage du centre de MapFragment pour une animation déplaçant le niveau de zoom cible

J'ai une interface utilisateur qui a un MapFragment avec une vue transparente superposée dessus. La carte occupe tout l'écran, alors que la vue ne représente que le tiers gauche de l'écran. Par conséquent, le "centre" par défaut de la carte est désactivé. Lorsque quelqu'un clique sur un marqueur, je souhaite centrer ce marqueur dans la zone entièrement visible de MapFragment (et non au centre de MapFragment lui-même).

Puisqu'il est difficile de décrire avec des mots, laissez-moi utiliser quelques images. Supposons que cela ressemble à mon interface utilisateur:

Default Map

Lorsque l'utilisateur clique sur un marqueur, je souhaite le centrer et pour le voir de plus près. Sans aucun ajustement, voici ce que vous obtiendrez:

Map with incorrectly centered marker

Ce que je veux, c'est que le marqueur soit centré, mais dans l'espace à droite, comme ceci:

Map with correctly centered marker

Il est très facile de réaliser cet exploit si vous ne modifiez pas le niveau de zoom à l'aide de la projection de la carte:

// Offset the target latitude/longitude by preset x/y ints
LatLng target = new LatLng(latitude, longitude);
Projection projection = getMap().getProjection();
Point screenLocation = projection.toScreenLocation(target);
screenLocation.x += offsetX;
screenLocation.y += offsetY;
LatLng offsetTarget = projection.fromScreenLocation(screenLocation);

// Animate to the calculated lat/lng
getMap().animateCamera(CameraUpdateFactory.newLatLng(offsetTarget));

Cependant, si vous modifiez le niveau de zoom en même temps, les calculs ci-dessus ne fonctionnent pas (car les décalages lat/lng changent à différents niveaux de zoom).

Permettez-moi de passer en revue une liste de tentatives de corrections:

  • Modifier rapidement le niveau de zoom, effectuer les calculs, revenir à la position d'origine de la caméra, puis animer. Malheureusement, le changement soudain de l'appareil photo (même s'il ne dure qu'une fraction de seconde) est malheureusement très évident et j'aimerais éviter le scintillement.

  • Superposant deux MapFragments l'un sur l'autre, l'un faisant les calculs, l'autre étant affiché. J'ai constaté que les MapFragments ne sont pas vraiment conçus pour être superposés (il y a des bogues inévitables sur cette route).

  • Modification du x/y de l'emplacement de l'écran par la différence de niveau de zoom au carré. Théoriquement, cela devrait fonctionner, mais il est toujours désactivé (environ .1 latitude/longitude, ce qui est suffisant pour être complètement désactivé).

Existe-t-il un moyen de calculer l’objectif offset même lorsque le niveau de zoom change?

48
Daniel Lew

La version la plus récente de la bibliothèque de services de lecture ajoute une fonction setPadding() à GoogleMap. Ce remplissage projettera tous les mouvements de la caméra vers le centre de la carte offset. Ce document décrit plus en détail le comportement du remplissage de la carte. 

La question posée à l'origine ici peut maintenant être simplement résolue en ajoutant un remplissage à gauche équivalent à la largeur de la présentation de gauche.

mMap.setPadding(leftPx, topPx, rightPx, bottomPx);
34
Kyle Ivey

J'ai trouvé une solution qui s'adapte vraiment au problème ... La solution consiste à calculer le centre du décalage dans le zoom requis par rapport au décalage d'origine, puis à animer la caméra de la carte. .__ Pour cela, déplacez d'abord la caméra de la carte sur le zoom souhaité, calculez le décalage pour ce niveau de zoom, puis rétablissez le zoom d'origine. Après avoir calculé le nouveau centre, nous pouvons créer l’animation avec CameraUpdateFactory.newLatLngZoom.

J'espère que ça aide!

private void animateLatLngZoom(LatLng latlng, int reqZoom, int offsetX, int offsetY) {

    // Save current zoom
    float originalZoom = mMap.getCameraPosition().zoom;

    // Move temporarily camera zoom
    mMap.moveCamera(CameraUpdateFactory.zoomTo(reqZoom));

    Point pointInScreen = mMap.getProjection().toScreenLocation(latlng);

    Point newPoint = new Point();
    newPoint.x = pointInScreen.x + offsetX;
    newPoint.y = pointInScreen.y + offsetY;

    LatLng newCenterLatLng = mMap.getProjection().fromScreenLocation(newPoint);

    // Restore original zoom
    mMap.moveCamera(CameraUpdateFactory.zoomTo(originalZoom));

    // Animate a camera with new latlng center and required zoom.
    mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(newCenterLatLng, reqZoom));

}
16
santilod

Edit: Le code ci-dessous est destiné aux maps v1 déconseillées. Cette SO réponse indique comment effectuer des actions similaires dans la v2: Comment obtenir une étendue de latitude/longitude dans Google Map V2 pour Android

Les méthodes clés dont vous avez besoin doivent fonctionner en long/lat et en pourcentages au lieu de pixels. Pour le moment, la vue de la carte zoomera autour du centre de la carte, puis passera à "recentrer" la broche dans la zone exposée. Vous pouvez obtenir la largeur après le zoom en degrés, prendre en compte certains biais d’écran, puis recentrer la carte. Voici un exemple de capture d'écran du code ci-dessous en action après avoir appuyé sur le signe «+»: before enter image description hereaprèsenter image description hereCode principal:

@Override
            public void onZoom(boolean zoomIn) {
                // TODO Auto-generated method stub
                controller.setZoom(zoomIn ? map.getZoomLevel()+1 : map.getZoomLevel()-1);
                int bias = (int) (map.getLatitudeSpan()* 1.0/3.0); // a fraction of your choosing
                map.getLongitudeSpan();
                controller.animateTo(new GeoPoint(yourLocation.getLatitudeE6(),yourLocation.getLongitudeE6()-bias));

            }

Tous les codes:

    package com.example.testmapview;

import com.google.Android.maps.GeoPoint;
import com.google.Android.maps.ItemizedOverlay;
import com.google.Android.maps.MapActivity;
import com.google.Android.maps.MapController;
import com.google.Android.maps.MapView;

import Android.os.Bundle;
import Android.app.Activity;
import Android.graphics.drawable.Drawable;
import Android.view.Menu;
import Android.widget.ZoomButtonsController;
import Android.widget.ZoomButtonsController.OnZoomListener;

public class MainActivity extends MapActivity {
    MapView map;
    GeoPoint yourLocation;
    MapController controller;
    ZoomButtonsController zoomButtons;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        map=(MapView)findViewById(R.id.mapview);
        map.setBuiltInZoomControls(true);
        controller = map.getController();
        zoomButtons = map.getZoomButtonsController();

        zoomButtons.setOnZoomListener(new OnZoomListener(){

            @Override
            public void onVisibilityChanged(boolean arg0) {
                // TODO Auto-generated method stub

            }

            @Override
            public void onZoom(boolean zoomIn) {
                // TODO Auto-generated method stub
                controller.setZoom(zoomIn ? map.getZoomLevel()+1 : map.getZoomLevel()-1);
                int bias = (int) (map.getLatitudeSpan()* 1.0/3.0); // a fraction of your choosing
                map.getLongitudeSpan();
                controller.animateTo(new GeoPoint(yourLocation.getLatitudeE6(),yourLocation.getLongitudeE6()-bias));

            }
        });

        //Dropping a pin, setting center
        Drawable marker=getResources().getDrawable(Android.R.drawable.star_big_on);
        MyItemizedOverlay myItemizedOverlay = new MyItemizedOverlay(marker);
        map.getOverlays().add(myItemizedOverlay);
        yourLocation = new GeoPoint(0, 0);
        controller.setCenter(yourLocation);
        myItemizedOverlay.addItem(yourLocation, "myPoint1", "myPoint1");
        controller.setZoom(5);

    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.activity_main, menu);
        return true;
    }

    @Override
    protected boolean isRouteDisplayed() {
        // TODO Auto-generated method stub
        return false;
    }

}

et une mise en page de base:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:Android="http://schemas.Android.com/apk/res/Android"
    package="com.example.testmapview"
    Android:versionCode="1"
    Android:versionName="1.0" >
<meta-data
    Android:name="com.google.Android.maps.v2.API_KEY"
    Android:value="YOURKEYHERE:)"/>
    <uses-sdk
        Android:minSdkVersion="8"
        Android:targetSdkVersion="16" />

<uses-permission Android:name="Android.permission.INTERNET" /> 
    <application
        Android:allowBackup="true"
        Android:icon="@drawable/ic_launcher"
        Android:label="@string/app_name"
        Android:theme="@style/AppTheme" >
        <uses-library Android:name="com.google.Android.maps"/>

        <activity
            Android:name="com.example.testmapview.MainActivity"
            Android:label="@string/app_name" >
            <intent-filter>
                <action Android:name="Android.intent.action.MAIN" />

                <category Android:name="Android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Si cela ne répond pas complètement à votre question, faites-le moi savoir car il s'agissait d'une expérience amusante à jouer. 

11
munch1324

Le matériel de projection de l’API n’est pas très utile, j’ai rencontré le même problème et j’ai lancé le mien.

public class SphericalMercatorProjection {

    public static PointD latLngToWorldXY(LatLng latLng, double zoomLevel) {
        final double worldWidthPixels = toWorldWidthPixels(zoomLevel);
        final double x = latLng.longitude / 360 + .5;
        final double siny = Math.sin(Math.toRadians(latLng.latitude));
        final double y = 0.5 * Math.log((1 + siny) / (1 - siny)) / -(2 * Math.PI) + .5;
        return new PointD(x * worldWidthPixels, y * worldWidthPixels);
    }

    public static LatLng worldXYToLatLng(PointD point, double zoomLevel) {
        final double worldWidthPixels = toWorldWidthPixels(zoomLevel);
        final double x = point.x / worldWidthPixels - 0.5;
        final double lng = x * 360;

        double y = .5 - (point.y / worldWidthPixels);
        final double lat = 90 - Math.toDegrees(Math.atan(Math.exp(-y * 2 * Math.PI)) * 2);

        return new LatLng(lat, lng);
    }

    private static double toWorldWidthPixels(double zoomLevel) {
        return 256 * Math.pow(2, zoomLevel);
    }
}

En utilisant ce code, vous pouvez transformer la carte autour d’une nouvelle cible (c’est-à-dire pas le centre). J'ai choisi d'utiliser un système de points d'ancrage, où (0.5, 0.5) est la valeur par défaut et représente le milieu de l'écran. (0,0) est en haut à gauche et (1, 1) en bas à gauche.

private LatLng getOffsetLocation(LatLng location, double zoom) {
    Point size = getSize();
    PointD anchorOffset = new PointD(size.x * (0.5 - anchor.x), size.y * (0.5 - anchor.y));
    PointD screenCenterWorldXY = SphericalMercatorProjection.latLngToWorldXY(location, zoom);
    PointD newScreenCenterWorldXY = new PointD(screenCenterWorldXY.x + anchorOffset.x, screenCenterWorldXY.y + anchorOffset.y);
    newScreenCenterWorldXY.rotate(screenCenterWorldXY, cameraPosition.bearing);
    return SphericalMercatorProjection.worldXYToLatLng(newScreenCenterWorldXY, zoom);
}

Fondamentalement, vous utilisez la projection pour obtenir la coordination mondiale XY, puis décalez ce point et reconvertissez-le en LatLng. Vous pouvez ensuite le transmettre à votre carte. PointD est un type simple qui contient x, y en tant que double et effectue également une rotation.

public class PointD {

    public double x;
    public double y;

    public PointD(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public void rotate(PointD Origin, float angleDeg) {
        double rotationRadians = Math.toRadians(angleDeg);
        this.x -= Origin.x;
        this.y -= Origin.y;
        double rotatedX = this.x * Math.cos(rotationRadians) - this.y * Math.sin(rotationRadians);
        double rotatedY = this.x * Math.sin(rotationRadians) + this.y * Math.cos(rotationRadians);
        this.x = rotatedX;
        this.y = rotatedY;
        this.x += Origin.x;
        this.y += Origin.y;
    }
}

Notez que si vous mettez à jour le relèvement de la carte et son emplacement en même temps, il est important que vous utilisiez le nouveau relèvement dans getOffsetLocation.

4
Michael Fry

Le moyen le plus simple serait d'enchaîner les animations de caméra. Tout d’abord, vous zoomez normalement sur votre image n ° 2 en utilisant animateCamera avec 3 paramètres et en CancellableCallback.onFinish vous effectuez vos calculs de projection et animez-vous en utilisant votre code pour changer de centre.

Je crois (cela n’a pas été testé) que cela aura l’air très bien si vous définissez de bonnes valeurs pour la durée de l’animation pour les deux animations. Peut-être que la deuxième animation devrait être beaucoup plus rapide que la première. De plus, si votre vue transparente superposée n'est pas visible lorsqu'aucun marqueur n'est sélectionné, une Nice UX consisterait à l'animer de gauche à droite lors de la deuxième animation.

Le plus difficile serait de mettre en œuvre votre propre projection Mercator sphérique comme celle utilisée par Maps API v2 en interne. Si vous avez seulement besoin d'un décalage de longitude, cela ne devrait pas être si difficile. Si vos utilisateurs peuvent modifier le relèvement et que vous ne souhaitez pas le réinitialiser à 0 lorsque vous effectuez un zoom, vous avez également besoin d'un décalage selon la latitude, ce qui sera probablement un peu plus complexe à mettre en œuvre.

Je pense également qu’il serait bon d’avoir la fonctionnalité Maps API v2 où vous pouvez obtenir une projection pour n’importe quel CameraPosition et pas seulement pour pouvoir soumettre une demande de fonctionnalité ici: http://code.google.com/p/ gmaps-api-issues/issues/liste? can = 2 & q = apitype = Android2 . Vous pouvez facilement l’utiliser dans votre code pour obtenir l’effet souhaité.

2
MaciejGórski

J'ai presque exactement le même problème que vous (mon décalage est uniquement vertical) et j'ai essayé une solution qui ne fonctionnait pas comme prévu, mais qui pourrait vous aider (nous) à trouver une meilleure solution.

Ce que j'ai fait est de déplacer la caméra de la carte pour obtenir la projection sur la cible et d'appliquer le décalage à cet endroit.

J'ai placé le code sur une classe Utils et cela ressemble à ceci:

public static CameraBuilder moveUsingProjection(GoogleMap map, CameraBuilder target, int verticalMapOffset) {
    CameraPosition oldPosition = map.getCameraPosition();
    map.moveCamera(target.build(map));
    CameraBuilder result = CameraBuilder.builder()
            .target(moveUsingProjection(map.getProjection(), map.getCameraPosition().target, verticalMapOffset))
            .zoom(map.getCameraPosition().zoom).tilt(map.getCameraPosition().tilt);
    map.moveCamera(CameraUpdateFactory.newCameraPosition(oldPosition));
    return result;
}

public static LatLng moveUsingProjection(Projection projection, LatLng latLng, int verticalMapOffset) {
    Point screenPoint = projection.toScreenLocation(latLng);
    screenPoint.y -= verticalMapOffset;
    return projection.fromScreenLocation(screenPoint);
}

Vous pouvez ensuite utiliser le CameraBuilder renvoyé par ces méthodes pour l'animation, au lieu de la cible d'origine (non décalée).

Cela fonctionne (plus ou moins), mais il a deux gros problèmes:

  • La carte scintille parfois (c'est le plus gros problème, car cela rend le hack inutilisable)
  • C'est un hack qui peut fonctionner aujourd'hui et peut ne pas fonctionner demain (je sais pertinemment que ce même algorithme fonctionne sur iOS, par exemple).

Donc, je n'y suis pas allé, mais cela m'a fait penser à une alternative: si vous aviez une carte identique mais cachée, vous pourriez utiliser cette carte pour des calculs intermédiaires, puis utiliser le résultat final sur la carte visible.

Je pense que cette alternative fonctionnerait, mais elle est horrible, car vous devrez conserver deux cartes identiques, avec les frais généraux qui l'accompagnent, pour vous et pour l'appareil.

Comme je l'ai dit, je n'ai pas de solution définitive à cela, mais peut-être que cela aide un peu.

J'espère que ça aide!

0
pablitar

J'ai résolu le problème en calculant un vecteur de distance en utilisant les valeurs LatLng de deux points fixes sur la carte, puis en créant un autre LatLng en utilisant la position du marqueur, ce vecteur et une valeur de rapport prédéfinie. Celle-ci comporte de nombreux objets. Toutefois, cela ne dépend pas du niveau de zoom actuel ni de la rotation de la carte. Il pourrait également fonctionner aussi bien pour vous!

// Just a helper function to obtain the device's display size in px
Point displaySize = UIUtil.getDisplaySize(getActivity().getWindowManager().getDefaultDisplay());

// The two fixed points: Bottom end of the map & Top end of the map,
// both centered horizontally (because I want to offset the camera vertically...
// if you want to offset it to the right, for instance, use the left & right ends)
Point up = new Point(displaySize.x / 2, 0);
Point down = new Point(displaySize.x / 2, displaySize.y);

// Convert to LatLng using the GoogleMap object
LatLng latlngUp = googleMap.getProjection().fromScreenLocation(up);
LatLng latlngDown = googleMap.getProjection().fromScreenLocation(down);

// Create a vector between the "screen point LatLng" objects
double[] vector = new double[] {latlngUp.latitude - latlngDown.latitude,
                                latlngUp.longitude - latlngDown.longitude};

// Calculate the offset position
LatLng latlngMarker = marker.getPosition();
double offsetRatio = 1.0/2.5;
LatLng latlngOffset = new LatLng(latlngMarker.latitude + (offsetRatio * vector[0]),
                                 latlngMarker.longitude + (offsetRatio * vector[1]));

// Move the camera to the desired position
CameraUpdate cameraUpdate = CameraUpdateFactory.newLatLng(latlngOffset);
googleMap.animateCamera(cameraUpdate);
0
Marcel Schnelle

Pour définir un marqueur, nous utilisons les superpositions dans Android, dans la méthode de superposition ==> sur la méthode dessinerPlace this thing

boundCenter (marqueur);

et dans le constructeur mentionnent aussi comme ceci:

    public xxxxxxOverlay(Drawable marker, ImageView dragImage, MapView map) {
            super(boundCenter(marker)); 
.
.
.
.
.
    }
0
VINIL SATRASALA

Je m'occupais de la même tâche et finissais par la même solution que votre dernière option:

Modification du x/y de l'emplacement de l'écran par la différence de niveau de zoom au carré. Théoriquement, cela devrait fonctionner, mais il est toujours désactivé (environ .1 latitude/longitude, ce qui est suffisant pour être complètement désactivé).

Mais au lieu de diviser le décalage du point sur 2<<zoomDifference, je calcule le décalage en tant que différences entre les valeurs de longitude et de latitude. Ils sont doubles donc ça me donne assez de précision. Donc, essayez de convertir vos décalages en lat/lng et non votre position en pixels et effectuez le même calcul. 


Édité

Enfin, j'ai réussi à le résoudre d'une autre manière (la plus appropriée à mon avis). L'API de carte v2 a paramètres de remplissage . Il suffit de le configurer pour que la caméra prenne toute partie non cachée de MapView. La caméra reflèterait alors le centre de la région matelassée après animateCamera()

0
birdy