将 MKMapView 居中在 pin 下方的点 N 像素上

Posted

技术标签:

【中文标题】将 MKMapView 居中在 pin 下方的点 N 像素上【英文标题】:Centering MKMapView on spot N-pixels below pin 【发布时间】:2013-03-14 22:06:09 【问题描述】:

希望将 MKMapView 置于 在给定图钉下方 N 像素处在当前 MapRect 中可能可见也可能不可见)。

我一直在尝试使用-(CLLocationCoordinate2D)convertPoint:(CGPoint)point toCoordinateFromView:(UIView *)view 的各种游戏来解决这个问题,但没有成功。

有人走过这条路吗(没有双关语)?

【问题讨论】:

【参考方案1】:

最简单的技术就是将地图向下移动,例如从 coordinate 的位置向下移动 40%,利用 regionMKMapViewspan。如果您不需要实际像素,而只需要将其向下移动,以使相关的CLLocationCoordinate2D 靠近地图顶部(例如距离顶部 10%):

CLLocationCoordinate2D center = coordinate;
center.latitude -= self.mapView.region.span.latitudeDelta * 0.40;
[self.mapView setCenterCoordinate:center animated:YES];

如果您想考虑摄像机的旋转和俯仰,上述技术可能不够用。在这种情况下,您可以:

确定视图中要将用户位置移动到的位置;

将其转换为CLLocation

计算当前用户位置与新的期望位置的距离;

将相机沿地图相机当前航向的 180° 方向移动该距离。

例如在 Swift 3 中,类似于:

var point = mapView.convert(mapView.centerCoordinate, toPointTo: view)
point.y -= offset
let coordinate = mapView.convert(point, toCoordinateFrom: view)
let offsetLocation = coordinate.location

let distance = mapView.centerCoordinate.location.distance(from: offsetLocation) / 1000.0

let camera = mapView.camera
let adjustedCenter = mapView.centerCoordinate.adjust(by: distance, at: camera.heading - 180.0)
camera.centerCoordinate = adjustedCenter

其中CLLocationCoordinate2D 有以下extension

extension CLLocationCoordinate2D 
    var location: CLLocation 
        return CLLocation(latitude: latitude, longitude: longitude)
    

    private func radians(from degrees: CLLocationDegrees) -> Double 
        return degrees * .pi / 180.0
    

    private func degrees(from radians: Double) -> CLLocationDegrees 
        return radians * 180.0 / .pi
    

    func adjust(by distance: CLLocationDistance, at bearing: CLLocationDegrees) -> CLLocationCoordinate2D 
        let distanceRadians = distance / 6_371.0   // 6,371 = Earth's radius in km
        let bearingRadians = radians(from: bearing)
        let fromLatRadians = radians(from: latitude)
        let fromLonRadians = radians(from: longitude)

        let toLatRadians = asin( sin(fromLatRadians) * cos(distanceRadians)
            + cos(fromLatRadians) * sin(distanceRadians) * cos(bearingRadians) )

        var toLonRadians = fromLonRadians + atan2(sin(bearingRadians)
            * sin(distanceRadians) * cos(fromLatRadians), cos(distanceRadians)
                - sin(fromLatRadians) * sin(toLatRadians))

        // adjust toLonRadians to be in the range -180 to +180...
        toLonRadians = fmod((toLonRadians + 3.0 * .pi), (2.0 * .pi)) - .pi

        let result = CLLocationCoordinate2D(latitude: degrees(from: toLatRadians), longitude: degrees(from: toLonRadians))

        return result
    

因此,即使相机倾斜并且朝向不是正北方向,这也会将用户的位置(居中,下部十字准线所在的位置)向上移动 150 像素(上部十字准线所在的位置),产生如下内容:

显然,您应该注意退化的情况(例如,您距离南极 1 公里,并且您尝试将地图向上移动 2 公里米;您使用的摄像机角度倾斜到所需的屏幕位置超出了地平线;等等),但对于实际的、真实的场景,类似上面的内容可能就足够了。显然,如果不让用户改变摄像头的俯仰角,答案就更简单了。


原答案:用于移动注解n像素

如果您有CLLocationCoordinate2D,您可以将其转换为CGPoint,将其移动x 个像素,然后将其转换回CLLocationCoordinate2D

- (void)moveCenterByOffset:(CGPoint)offset from:(CLLocationCoordinate2D)coordinate

    CGPoint point = [self.mapView convertCoordinate:coordinate toPointToView:self.mapView];
    point.x += offset.x;
    point.y += offset.y;
    CLLocationCoordinate2D center = [self.mapView convertPoint:point toCoordinateFromView:self.mapView];
    [self.mapView setCenterCoordinate:center animated:YES];

您可以通过以下方式调用它:

[self moveCenterByOffset:CGPointMake(0, 100) from:coordinate];

不幸的是,这仅在您开始之前可见 coordinate 时才有效,因此您可能必须先转到原始坐标,然后再调整中心。

【讨论】:

如果有人需要以像素为单位偏移地图,请阅读此答案的原始编辑 @Rob 你能帮我解决这个 MKMapView 缩放/中心问题吗:***.com/questions/30831318/… 如果可以的话,我将不胜感激 :) 这是一个绝妙的答案,对我也很有帮助:) 这个答案的唯一问题是它假设地图的方向是北朝上。这对于某些场景(例如地图的初始视图)来说是可以的。但是,如果您在设置中心之前用户可以以不同的方式定位地图(即,将地图旋转 90 度并让东方朝上),它会将其居中向右。这是因为纬度是南北数据。为了使用相同的算法,您需要计算旋转并根据方向调整纬度和经度。 @Jaro - 首先,我不是在乘以坐标。我减去了一个跨度的一小部分(如果你指向北方,那很好)。第二,关于退化的情况,问题不是北极,而是南极。但是这种退化的情况不仅仅是一个数学问题,也是一个函数问题(例如,当你离南极只有 1 公里时,将地图上移 2 公里意味着什么?!)。无论如何,我添加了另一个替代方案,它不仅可以处理简单的非北方场景,还可以处理更复杂的地图视图相机倾斜的情况。【参考方案2】:

可靠地做到这一点的唯一方法是使用以下方法:

- (void)setVisibleMapRect:(MKMapRect)mapRect edgePadding:(UIEdgeInsets)insets animated:(BOOL)animate

为了做到这一点,给定一个您想要居中的地图区域,您必须将地图区域转换为 MKMapRect。显然,使用边缘填充作为像素偏移。

请看这里: Convert MKCoordinateRegion to MKMapRect

评论:我觉得这很奇怪,这是唯一的方法,因为 MKMapRect 不是通常与 MKMapView 一起使用的东西——所有的转换方法都是针对 MKMapRegion 的。但是,好的,至少它有效。在我自己的项目中测试过。

【讨论】:

@n13 你能详细说明一下吗,我有用户位置图钉(带有自定义图钉)我希望它位于地图底部(从底部 20 像素开始)我该怎么做?非常感谢任何帮助【参考方案3】:

对于斯威夫特:

import MapKit

extension MKMapView 

func moveCenterByOffSet(offSet: CGPoint, coordinate: CLLocationCoordinate2D) 
    var point = self.convert(coordinate, toPointTo: self)

    point.x += offSet.x
    point.y += offSet.y

    let center = self.convert(point, toCoordinateFrom: self)
    self.setCenter(center, animated: true)


func centerCoordinateByOffSet(offSet: CGPoint) -> CLLocationCoordinate2D 
    var point = self.center

    point.x += offSet.x
    point.y += offSet.y

    return self.convert(point, toCoordinateFrom: self)


【讨论】:

我将自定义图像设置为用户当前位置,我希望它位于(中心)正上方 20 像素,所以我需要调用此方法,你能帮我解决吗 你能指导我吗,我需要澄清一下 嗨@ShobhakarTiwari,当然。明天我会回复你的。忙了一天的工作,现在要睡觉了。你是我明天清单上的第一个项目。 :) func centerMapOnLocation(coordinate: CLLocationCoordinate2D) var region = MKCoordinateRegion(center: coordinate, span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)) region.center = coordinate mapView.setRegion(region, animated: true) mapView.moveCenterByOffSet(CGPoint(x: 0, y: -30), coordinate: coordinate) 让我检查一下,感谢您的努力【参考方案4】:

一个简单的解决方案是使地图视图的框架大于可见区域。然后将您的图钉放置在地图视图的中心,并将所有不需要的区域隐藏在另一个视图后面或屏幕边界之外。

让我详细说明。如果我查看您的屏幕截图,请执行以下操作:

你的 pin 和底部之间的距离是 353 像素。因此,让您的地图视图框架的高度是两倍:706 像素。您的屏幕截图高度为 411 像素。将框架定位在 706px - 411px = -293 像素的原点。 现在,您的地图视图以图钉的坐标为中心,您就完成了。

2014 年 3 月 4 日更新:

我用 Xcode 5.0.2 创建了一个小示例应用程序来演示这个:http://cl.ly/0e2v0u3G2q1d

【讨论】:

即使地图倾斜,此解决方案也能正常工作(其他解决方案不能)。 Erm - 不起作用,因为 setCenterCoordinate 以地图的 可见 部分为中心。因此,如果地图伸出屏幕,ios忽略并仅在可见部分上工作。 @n13 你试过了吗??地图的“可见”部分是指地图视图的框架和边界。那里没有与屏幕相关的内容。 ^ 是的,我试过了,你呢?不幸的是,iOS 似乎对视图的可见部分太聪明了。它似乎与可见的屏幕矩形相交。解决方案见我的回复,有一个 map 方法以 insets 作为参数,这正是我们所需要的。 @n13 是的,我多次使用成功。请记住,在 iOS 7 下,布局指南也会影响地图的中心。【参考方案5】:

SWIFT 3 更新

更新了缩放功能

func zoomToPos() 

        let span = MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta:  0.1)

        // Create a new MKMapRegion with the new span, using the center we want.
        let coordinate = moveCenterByOffset(offset: CGPoint(x: 0, y: 100), coordinate: (officeDetail?.coordinate)!)
        let region = MKCoordinateRegion(center: coordinate, span: span)

        mapView.setRegion(region, animated: true)


    

    func moveCenterByOffset (offset: CGPoint, coordinate: CLLocationCoordinate2D) -> CLLocationCoordinate2D 
        var point = self.mapView.convert(coordinate, toPointTo: self.mapView)
        point.x += offset.x
        point.y += offset.y
        return self.mapView.convert(point, toCoordinateFrom: self.mapView)
    

【讨论】:

在哪个委托方法中我需要使用这个方法 你不需要任何委托方法,当你想缩放时只调用函数,例如我缩放到officeDetail?.coordinate,这里你可以缩放到任何适合你的坐标。 所以在缩放委托中这个方法需要调用 rit 吗?而且我想把我当前的位置放在地图底部(距离地图底部 100 像素)请建议【参考方案6】:

在阅读了这个帖子广告后,尤其是放大注释后,我最终得到了以下过程:

** 以注解为中心:**

- (void) centerOnSelection:(id<MKAnnotation>)annotation

    MKCoordinateRegion region = self.mapView.region;
    region.center = annotation.coordinate;

    CGFloat per = ([self sizeOfBottom] - [self sizeOfTop]) / (2 * self.mapView.frame.size.height);
    region.center.latitude -= self.mapView.region.span.latitudeDelta * per;

    [self.mapView setRegion:region animated:YES];

** 放大注释:**

- (void) zoomAndCenterOnSelection:(id<MKAnnotation>)annotation

    DLog(@"zoomAndCenterOnSelection");

    MKCoordinateRegion region = self.mapView.region;
    MKCoordinateSpan span = MKCoordinateSpanMake(0.005, 0.005);

    region.center = annotation.coordinate;

    CGFloat per = ([self sizeOfBottom] - [self sizeOfTop]) / (2 * self.mapView.frame.size.height);
    region.center.latitude -= self.mapView.region.span.latitudeDelta * span.latitudeDelta / region.span.latitudeDelta * per;

    region.span = span;

    [self.mapView setRegion:region animated:YES];

-(CGFloat) sizeOfBottom-(CGFloat) sizeOfTop 都返回布局指南中覆盖地图视图的面板高度

【讨论】:

不知道为什么这是 mod down,但这是在商业应用程序中使用的,并且正在解决在不使用 setCenterCoordinate 的情况下进行缩放的问题,这会导致缩放动画出现问题。尝试一下。【参考方案7】:

作为公认答案的替代方案,我建议您最初的直觉是正确的。您可以严格在地图视图像素坐标空间内工作以获得偏移和最终定位。然后使用从位置到屏幕视图的转换调用,您可以获得最终位置并设置地图中心。

这适用于旋转的相机并且相对于屏幕空间。在我的例子中,我需要将地图放在一个大头针的中心,并使用一个偏移量来说明一个地图抽屉。

以下是转化调用

func convert(_ coordinate: CLLocationCoordinate2D, toPointTo view: UIView?) -> CGPoint
func convert(_ point: CGPoint, toCoordinateFrom view: UIView?) -> CLLocationCoordinate2D

这是一个 swift 4 示例

//First get the position you want the pin to be (say 1/4 of the way up the screen)
let targetPoint = CGPoint(x: self.frame.width / 2.0, y: self.frame.height * CGFloat(0.25))

//Then get the center of the screen (this is used for calculating the offset as we are using setCenter to move the region
let centerPoint = CGPoint(x: self.frame.width / 2.0, y: self.frame.height / 2.0)

//Get convert the CLLocationCoordinate2D of the pin (or map location) to a screen space CGPoint
let annotationPoint = mapview.convert(myPinCoordinate, toPointTo: mapview)

//And finally do the math to set the offsets in screen space                
let mapViewPointFromAnnotation = CGPoint(x: annotationPoint.x + (centerPoint.x - targetPoint.x), y: annotationPoint.y + (centerPoint.y - targetPoint.y))

//Now convert that result to a Coordinate
let finalLocation = self.convert(mapViewPointFromAnnotation, toCoordinateFrom: mapview)

//And set the map center
mapview.setCenter(finalLocation, animated: true)

【讨论】:

【参考方案8】:

MKMapView这个方法:

- (void)setCenterCoordinate:(CLLocationCoordinate2D)coordinate animated:(BOOL)animated

【讨论】:

以上是关于将 MKMapView 居中在 pin 下方的点 N 像素上的主要内容,如果未能解决你的问题,请参考以下文章

自定义图钉动画 - MKMapView

MKMapView 点击 pin 会导致奇怪的帧变化

在当前用户位置放置 pin iphone mkmapview

防止 MKMapView 不断地重新缩放和重新居中到用户位置

MKMapView 第一次出现时未正确居中(但此后有效)

如何在自定义 UIView 下方的 MKMapview 中启用 touchEvents(滚动和平移)?