web-dev-qa-db-ja.com

ターゲットの緯度/経度とズームレベルの両方を移動するアニメーションのMapFragmentの中心をオフセットする

MapFragmentの上に透明なビューがオーバーレイされたUIがあります。マップは画面全体を占めますが、ビューは画面の左3分の1です。その結果、マップのデフォルトの「中心」はオフになります。誰かがマーカーをクリックすると、そのマーカーをMapFragmentの完全に見える領域(MapFragment自体の中心ではない)に中央に配置します。

これは言葉で説明するのが難しいため、いくつかの写真を使用します。これが私のUIの外観であると仮定します。

Default Map

ユーザーがマーカーをクリックすると、両方のマーカーを中央に配置しますおよびズームインして、近くに表示します。調整なしで、これが得られます:

Map with incorrectly centered marker

私が望むのは、マーカーを中央に配置することですが、次のように右側のスペースに配置します。

Map with correctly centered marker

マップの投影を使用してズームレベルを変更しない場合、この偉業を達成するのは非常に簡単です。

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

ただし、ズームレベルを同時に変更する場合、上記の計算は機能しません(異なるズームレベルで緯度/経度のオフセットが変化するため)。

修正の試みのリストを見てみましょう。

  • ズームレベルをすばやく変更し、計算を行い、元のカメラ位置にズームバックしてからアニメーション化します。残念ながら、突然のカメラの変更(ほんの一瞬であっても)は残念ながら非常に明白であり、ちらつきを避けたいと思います。

  • 2つのMapFragmentsを重ねて表示し、一方が計算を行い、他方が表示されるようにします。私は、MapFragmentsが実際に互いの上に階層化されるように構築されていないことを発見しました(このルートには避けられないバグがあります)。

  • ズームレベルの2乗の違いによる画面の位置のx/yの変更。理論的にはこれは機能するはずですが、常にかなり離れています(〜.1緯度/経度。これは十分に外れています)。

ズームレベルを変更してもoffsetTargetを計算する方法はありますか?

49
Daniel Lew

プレイサービスライブラリの最新バージョンは、 setPadding() 関数をGoogleMapに追加します。このパディングは、すべてのカメラの動きをオフセットマップの中心に投影します。 このドキュメント では、マップパディングの動作についてさらに説明しています。

ここで最初に尋ねられた質問は、左側のレイアウトの幅に相当する左側のパディングを追加することで簡単に解決できます。

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

問題に本当に適合する解決策を見つけました。解決策は、元のオフセットから必要なズームでオフセットの中心を計算し、マップのカメラをアニメーション化することです。そのためには、まずマップのカメラを目的のズームに移動し、そのズームレベルのオフセットを計算してから、元のズームに戻します。新しい中心を計算した後、CameraUpdateFactory.newLatLngZoomを使用してアニメーションを作成できます。

これがお役に立てば幸いです!

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

編集:以下のコードは、非推奨のv1マップ用です。このSO回答は、v2で同様のアクションを実行する方法を示しています。 Android向けGoogleマップV2で緯度/経度のスパンを取得する方法

必要なキーメソッドは、ピクセルではなくlong/latおよびパーセンテージで機能する必要があります。現在、mapviewはマップの中心を中心にズームしてから、露出した領域のピンを「リセンター」します。ズーム後の幅を取得し、画面のバイアスを考慮して、マップを再センタリングできます。 「+」を押した後の動作中の以下のコードのサンプルスクリーンショットは次のとおりです。 enter image description here 後enter image description here メインコード:

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

            }

すべてのコード:

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

}

そして基本的なレイアウト:

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

これであなたの質問に完全に答えられない場合、これはいじって楽しい実験だったので私に知らせてください。

11
munch1324

APIの投影機能はそれほど便利ではないので、同じ問題にぶつかり、自分の問題を解決しました。

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

このコードを使用して、新しいターゲット(中心ではない)を中心にマップを変換できます。アンカーポイントシステムの使用を選択しました。(0.5、0.5)はデフォルトで、画面の中央を表します。 (0,0)は左上、(1、1)は左下です。

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

基本的に、投影を使用してワールドXY座標を取得し、そのポイントをオフセットしてLatLngに戻します。これをマップに渡すことができます。 PointDは、x、yをdoubleとして含み、回転も行う単純なタイプです。

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

マップの方位と位置を同時に更新する場合、getOffsetLocationで新しい方位を使用することが重要です。

4
Michael Fry

簡単な方法は、カメラアニメーションをチェーンすることです。最初に animateCamera with 3 parameters and in CancellableCallback.onFinish投影計算を行い、コードを使用して中心を変更するアニメーションを作成します。

両方のアニメーションのアニメーション期間に適切な値を設定すると、これは非常によく見えると思います(テストしませんでした)。おそらく、2番目のアニメーションは最初のアニメーションよりもはるかに速いはずです。また、マーカーが選択されていないときに透明なオーバーレイビューが表示されない場合は、2番目のアニメーション中にニースUXが左からアニメーション表示します。

難しい方法は、Maps API v2が内部的に使用するような独自の球面メルカトル図法を実装することです。経度オフセットのみが必要な場合、それほど難しくはありません。ユーザーが方位を変更でき、ズーム時に0にリセットしたくない場合は、緯度オフセットも必要であり、これはおそらく実装が少し複雑になります。

Maps API v2の機能を使用して、現在のカメラだけでなく任意のCameraPositionの投影を取得できるようにすることも良いと思うので、ここで機能リクエストを送信できます。 http://code.google.com/ p/gmaps-api-issues/issues/list?can = 2&q = apitype = Android2 。必要な効果を得るために、コードで簡単に使用できます。

2
MaciejGórski

私はあなたとほとんどまったく同じ問題を抱えています(私のオフセットのみが垂直です)、期待どおりに機能しなかったが、より良い解決策を見つけるのに役立つ解決策を試しました。

私がやったのは、マップのカメラを動かしてターゲットに投影し、そこにオフセットを適用することでした。

コードをUtilsクラスに配置すると、次のようになります。

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

次に、元の(オフセットされていない)ターゲットの代わりに、これらのメソッドによって返されるCameraBuilderをアニメーションに使用できます。

(多少は)動作しますが、2つの大きな問題があります。

  • マップが時々ちらつく(これは、ハックが使用できなくなるため、これが最大の問題です)
  • 今日はうまくいくかもしれないし、明日はうまくいかないかもしれないハックです(たとえば、この同じアルゴリズムがiOSでも機能することは知っています)。

だから、私はこれには行きませんでしたが、代替案を考えさせられました:同一のマップが隠されている場合、そのマップを中間計算に使用してから、可視マップで最終結果を使用できます。

この代替案は機能すると思いますが、恐ろしいことです。2つの同一のマップを、それに伴うオーバーヘッドとともに、あなたとデバイスのために保持する必要があるからです。

私が言ったように、これに対する決定的な解決策はありませんが、これは少し役立つかもしれません。

これがお役に立てば幸いです!

0
pablitar

マップ上の2つの固定ポイントのLatLng値を使用して距離ベクトルを計算し、マーカー位置、このベクトル、事前定義された比率値を使用して別のLatLngを作成することで問題を解決しました。このオブジェクトには多くのオブジェクトが関係していますが、現在のズームレベルとマップの回転に依存せず、あなたにぴったりの機能を発揮します。

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

マーカーを設定するには、AndroidのOverlaysを使用します。オーバーレイクラスで==>描画メソッドでこのことを配置します

boundCenter(marker);

コンストラクターでも次のように言及します。

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

私は同じタスクを扱っていましたが、最後のオプションと同様のソリューションで終了しました:

ズームレベルの2乗の違いによる画面の位置のx/yの変更。理論的にはこれは機能するはずですが、常にかなり離れています(〜.1緯度/経度。これは十分に外れています)。

しかし、_2<<zoomDifference_でポイントのオフセットを分割する代わりに、経度と緯度の値の差としてオフセットを計算しています。それらはdoublesなので、十分な精度が得られます。したがって、オフセットを緯度/経度に変換して、場所をピクセルに変換するのではなく、同じ計算を実行してください。


編集済み

最後に、別の方法で解決することができました(私が信じているように、最も適切な方法)。 Map API v2には padding parameters があります。 MapViewの隠されていない部分をすべて撮影するようにカメラを設定するだけです。その後、カメラはanimateCamera()の後にパディングされた領域の中心を反映します

0
birdy