为移动目标 lat/lng 和缩放级别的动画偏移地图片段的中心
Posted
技术标签:
【中文标题】为移动目标 lat/lng 和缩放级别的动画偏移地图片段的中心【英文标题】:Offseting the center of the MapFragment for an animation moving both the target lat/lng and the zoom level 【发布时间】:2013-03-15 14:14:15 【问题描述】:我的 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));
但是,如果您同时更改缩放级别,则上述计算不起作用(因为 lat/lng 偏移在不同的缩放级别上发生变化)。
让我列出尝试修复的列表:
快速更改缩放级别,进行计算,缩放回原始相机位置,然后制作动画。不幸的是,突然的相机变化(即使只是瞬间)非常明显,我想避免闪烁。
将两个 MapFragment 叠加在一起,一个进行计算,另一个显示。我发现 MapFragment 并不是真正构建为相互叠加的(这条路线存在不可避免的错误)。
通过缩放级别平方的差异来修改屏幕位置的 x/y。理论上这应该可行,但它总是偏离很多(~.1 纬度/经度,这足以偏离)。
即使缩放级别发生变化,有没有办法计算 offsetTarget?
【问题讨论】:
“因为 lat/lng 需要在不同的缩放级别进行更改”——这对我来说并没有多大意义。除此之外,我不太确定你为什么要浏览所有屏幕坐标的东西。 “左三分之一”是“左三分之一”,无论您以像素、度、毫米、秒差距还是其他单位测量它。将所有计算都保留在纬度和经度方面会有帮助吗? 我认为这很难用语言来解释;让我试着画一张说明问题的图片。 那里,同样的问题,但有视觉辅助。使用所有纬度/经度进行计算的问题在于,我想要的偏移量纯粹以像素为单位(基于屏幕大小和 MapFragment 的可见程度)。 “我想要的偏移量纯粹以像素为单位”——所以当你写“视图只是屏幕的左三分之一”时,这不是一个准确的说法吗?因为,如果它是准确的,像素是无关紧要的,因为无论测量单位如何,三分之一就是三分之一。 占屏幕 1/3 的像素随屏幕大小而变化。所有相机更改都需要一个目标 LatLng,因此我必须能够将所有内容转换为 LatLng。本质上,我正在尝试将屏幕上的像素转换为偏移纬度/经度。该 lat/lng 偏移量会根据缩放级别而变化,因为您放大的越多,您在地图上移动所需的度数变化就越大。 【参考方案1】:播放服务库的最新版本在GoogleMap
中添加了setPadding()
函数。此填充将所有相机移动投影到偏移地图中心。 This doc 进一步解释了地图填充的行为方式。
这里最初提出的问题现在可以通过添加与左侧布局宽度相等的左侧填充来解决。
mMap.setPadding(leftPx, topPx, rightPx, bottomPx);
【讨论】:
这是要走的路。 这在大多数情况下都有效,但请注意,在 Play Lib 中点击并拖动缩放被破坏 - 它将继续使用 MapView 中心的枢轴,而不是填充区域。这会造成糟糕的用户体验。【参考方案2】:我找到了一个真正适合该问题的解决方案。 解决方案是从原始偏移量计算所需缩放中偏移量的中心,然后为地图的相机设置动画。 为此,首先将地图的相机移动到所需的缩放,计算该缩放级别的偏移量,然后恢复原始缩放。计算新中心后,我们可以使用 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));
【讨论】:
好方法!谢谢! 至于今天,这应该是例外的答案! 这是达到预期效果的最简单但最有效的方法。 你救了我的朋友 :)【参考方案3】:编辑:以下代码适用于已弃用的 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>
如果这不能完全回答您的问题,请告诉我,因为这是一个有趣的实验。
【讨论】:
您正在使用已弃用的 API v1。 感谢您指出这一点,我认为实现应该几乎相同,因为 v2 也具有与 v1 类似的 API。我编辑了我的答案以注意这一点 V2 有新的 API,它绝不是基于 V1。 另外,关键是一次尝试就获得正确的纬度/经度/缩放,而不是两次动画。 作为一种技巧,您可以在顶部地图视图下拥有一个单独的地图视图。放大隐藏的地图视图,使用上面的计算得到中心,然后在可见的地图视图上做一个结合缩放/重新居中的动画(不理想)。【参考方案4】: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 中使用新方位。
【讨论】:
【参考方案5】:最简单的方法是链接相机动画。首先,您使用animateCamera with 3 parameters 正常缩放到您的图片#2,并在CancellableCallback.onFinish
中进行投影计算并使用您的代码设置动画以更改中心。
我相信(未测试)如果您为两个动画的动画持续时间设置了一些好的值,这看起来会非常好。也许第二个动画应该比第一个快得多。此外,如果在未选择标记时您的透明叠加视图不可见,则一个不错的用户体验将是在第二个动画期间从左侧对其进行动画处理。
困难的方法是实现您自己的球形墨卡托投影,就像 Maps API v2 内部使用的那样。如果您只需要经度偏移,那应该不难。如果您的用户可以更改方位,并且您不想在缩放时将其重置为 0,则您也需要纬度偏移,这可能会更复杂一些。
我还认为拥有 Maps API v2 功能会很好,您可以在其中获取任何 CameraPosition 的投影,而不仅仅是当前的,因此您可能希望在此处提交功能请求:http://code.google.com/p/gmaps-api-issues/issues/list?can=2&q=apitype=Android2。您可以轻松地在代码中使用它来获得所需的效果。
【讨论】:
我已经链接了相机动画(作为另一种可能的解决方案)。结果不是很漂亮。 你能分享动画持续时间吗?你在第一个动画结束后开始第二个动画之前使用延迟吗?总是有艰难的道路。我在网格聚类算法中使用球形墨卡托。使用非常简单:计算在任何缩放级别为给定经度跨度创建正方形的纬度:SphericalMercator.java 关键是要有一个从CameraPosition A到CameraPosition B的漂亮动画。持续时间并不重要 - 如果你有一个插页式CameraPosition它看起来很有趣(从尝试过的经验来看这个解决方案已经)。【参考方案6】:为了设置标记,我们在Android中使用Overlays,在overlay class==> on draw方法 放置这个东西
boundCenter(标记);
在构造函数中也这样提到:
public xxxxxxOverlay(Drawable marker, ImageView dragImage, MapView map)
super(boundCenter(marker));
.
.
.
.
.
【讨论】:
【参考方案7】:我的问题和你几乎一模一样(只是我的偏移是垂直的),我尝试了一个没有按预期工作的解决方案,但可以帮助你(我们)找到更好的解决方案。
我所做的是移动地图的相机以获得目标上的投影,并在那里应用偏移量。
我将代码放在一个 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 无法使用) 这是一种可能在今天有效但明天可能无效的 hack(例如,我知道同样的算法适用于 ios)。所以我没有这样做,但它让我想到了一个替代方案:如果您有一个相同的地图但隐藏了,您可以使用该地图进行中间计算,然后在可见地图上使用最终结果。
我认为这种替代方案可行,但它很糟糕,因为您需要维护两个相同的地图,以及随之而来的开销,为您和设备。
正如我所说,我对此没有明确的解决方案,但也许这会有所帮助。
我希望这会有所帮助!
【讨论】:
Camera Builder 到底是什么?【参考方案8】:我已通过使用地图上两个固定点的 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);
【讨论】:
【参考方案9】:我正在处理相同的任务,并以与您最后一个选项类似的解决方案结束:
通过缩放级别平方的差异来修改屏幕位置的 x/y。理论上这应该可行,但它总是偏离很多(~.1 纬度/经度,这足以偏离)。
但不是在2<<zoomDifference
上划分点的偏移量,而是将偏移量计算为经度和纬度值之间的差异。他们是double
s,所以它给了我足够好的准确性。因此,请尝试将您的偏移量转换为 lat/lng 而不是您的位置转换为像素,然后进行相同的计算。
已编辑
最后我设法以另一种方式解决了它(我认为最合适的方式)。 Map API v2 有padding parameters。只需设置它以使相机拍摄MapView
的所有未隐藏部分。然后相机会在animateCamera()
之后反射填充区域的中心
【讨论】:
以上是关于为移动目标 lat/lng 和缩放级别的动画偏移地图片段的中心的主要内容,如果未能解决你的问题,请参考以下文章
MaxMind 从 IP 地址获取 Lat/Lng,InvalidDatabaseException 错误