51

我有一个 UI,它有一个 MapFragment,上面覆盖了一个透明的 View。地图占据了整个屏幕,而视图只是屏幕的左三分之一。结果,地图的默认“中心”处于关闭状态。当有人单击标记时,我想将该标记置于 MapFragment 的完全可见区域(而不是 MapFragment 本身的中心)的中心。

由于这很难用文字来描述,所以让我用几张图片。假设这是我的 UI 的外观:

默认地图

当用户单击标记时,我想将它居中放大以更近地看到它。没有任何调整,你会得到:

带有不正确居中标记的地图

我想要的是使标记居中,但在右侧的空间中,如下所示:

带有正确居中标记的地图

如果您不使用地图的投影更改缩放级别,则很容易实现这一壮举:

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

但是,如果您同时更改缩放级别,则上述计算不起作用(因为纬度/经度偏移在不同的缩放级别上发生变化)。

让我列出尝试修复的列表:

  • 快速更改缩放级别,进行计算,缩放回原始相机位置,然后制作动画。不幸的是,突然的相机变化(即使只是一瞬间)非常明显,我想避免闪烁。

  • 将两个 MapFragment 相互叠加,一个进行计算,另一个显示。我发现 MapFragments 并没有真正构建为相互叠加(这条路线上存在不可避免的错误)。

  • 通过缩放级别平方的差异修改屏幕位置的 x/y。理论上这应该可行,但它总是偏离很多(~.1 纬度/经度,这足以偏离)。

即使缩放级别发生变化,有没有办法计算 offsetTarget ?

4

9 回答 9

40

最新版本的播放服务库setPadding()GoogleMap. 此填充将所有相机移动投影到偏移地图中心。该文档进一步解释了地图填充的行为方式。

现在可以通过添加与左侧布局宽度等效的左侧填充来简单地解决最初在这里提出的问题。

mMap.setPadding(leftPx, topPx, rightPx, bottomPx);
于 2013-12-19T01:36:43.830 回答
19

我找到了一个真正适合该问题的解决方案。解决方案是根据原始偏移量计算所需缩放中偏移量的中心,然后为地图的相机设置动画。为此,首先将地图的相机移动到所需的缩放,计算该缩放级别的偏移量,然后恢复原始缩放。计算新中心后,我们可以使用 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));

}
于 2014-11-19T17:03:25.063 回答
11

编辑:以下代码适用于已弃用的 v1 地图。这个 SO 答案说明了如何在 v2 中执行类似的操作:How to get Latitude/Longitude span in Google Map V2 for Android

您需要的关键方法应该在 long/lat 和百分比而不是像素中工作。现在,地图视图将围绕地图中心进行缩放,然后移动到暴露区域中的“重新居中”图钉。您可以在放大度数后获得宽度,考虑一些屏幕偏差,然后重新定位地图。这是按下“+”后以下代码的示例屏幕截图:之前 在此处输入图像描述在此处输入图像描述 主代码之后:

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

如果这不能完全回答您的问题,请告诉我,因为这是一个有趣的实验。

于 2013-03-29T21:04:38.240 回答
5

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 作为双精度值,并且还进行旋转。

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 中使用新方位。

于 2014-05-20T20:51:50.173 回答
2

最简单的方法是链接相机动画。首先,您使用带有 3 个参数的animateCamera正常缩放到您的图片#2 ,CancellableCallback.onFinish然后使用您的代码进行投影计算和动画以更改中心。

我相信(没有测试过)如果你为两个动画的动画持续时间设置了一些好的值,这看起来会很好。也许第二个动画应该比第一个快得多。此外,如果在未选择标记时您的透明叠加视图不可见,则一个不错的用户体验将是在第二个动画期间从左侧对其进行动画处理。

困难的方法是实现自己的 Spherical Mercator Projection,就像 Maps API v2 内部使用的那样。如果您只需要经度偏移,那应该不难。如果您的用户可以更改方位并且您不想在缩放时将其重置为 0,则您也需要纬度偏移,这可能会更复杂一些。

我还认为拥有 Maps API v2 功能会很好,您可以在其中获取任何 CameraPosition 的投影,而不仅仅是当前的,因此您可能希望在此处提交功能请求:http ://code.google.com/p/gmaps- api-issues/issues/list?can=2&q=apitype=Android2。您可以轻松地在代码中使用它来获得所需的效果。

于 2013-04-03T18:13:13.650 回答
0

为了设置标记,我们在Android中使用Overlays,在overlay class==> on draw方法放置这个东西

边界中心(标记);

在构造函数中也提到这样的:

    public xxxxxxOverlay(Drawable marker, ImageView dragImage, MapView map) {
            super(boundCenter(marker)); 
.
.
.
.
.
    }
于 2013-04-04T06:36:13.570 回答
0

我的问题和你几乎一模一样(只有我的偏移是垂直的),我尝试了一个没有按预期工作的解决方案,但可以帮助你(我们)找到更好的解决方案。

我所做的是移动地图的相机以获得目标上的投影,并在那里应用偏移量。

我将代码放在 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 用于动画,而不是原始(未偏移)目标。

它有效(或多或少),但它有两个大问题:

  • 地图有时会闪烁(这是最大的问题,因为它使黑客无法使用)
  • 这是一种可能在今天有效但明天可能无效的 hack(例如,我知道同样的算法适用于 iOS)。

所以我没有这样做,但它让我想到了一个替代方案:如果你有一个相同的地图但隐藏了,你可以使用该地图进行中间计算,然后在可见地图上使用最终结果。

我认为这种替代方案可行,但它很可怕,因为您需要维护两个相同的地图,以及随之而来的开销,用于您和设备。

正如我所说,我对此没有明确的解决方案,但也许这会有所帮助。

我希望这有帮助!

于 2013-05-29T19:29:40.780 回答
0

我通过使用地图上两个固定点的 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);
于 2013-09-13T10:59:00.053 回答
0

我正在处理相同的任务,并以与您最后一个选项类似的解决方案结束:

通过缩放级别平方的差异修改屏幕位置的 x/y。理论上这应该可行,但它总是偏离很多(~.1 纬度/经度,这足以偏离)。

但是,2<<zoomDifference我没有将点的偏移量划分为经度和纬度值之间的差异,而是计算偏移量。他们是doubles 所以它给了我足够好的准确性。因此,尝试将您的偏移量转换为 lat/lng 而不是您的位置转换为像素,然后进行相同的计算。


已编辑

最后,我设法以另一种方式解决了它(我相信最合适的方式)。Map API v2 具有填充参数。只需设置它以使相机拍摄所有未隐藏的部分MapView。然后相机会在之后反射填充区域的中心animateCamera()

于 2014-12-05T13:27:20.057 回答