如何调整区域以适应刚刚出现的自定义注释标注?

我使用MKAnnotationView的自定义子类。 在mapView:didSelectAnnotationView:我的Map委托的方法我调用了这个类的方法,它将UIImageView与图像作为子视图添加 – 它作为我的自定义注释标注。

当使用默认的MKPinAnnotationView地图时,会自动调整地图区域以显示刚刚出现的注释标注。 如何使用自定义MKAnnotationView子类实现此行为?

当前解决方案

我已经制作了具有下面讨论的内容的演示项目:请参阅AdjustRegionToFitAnnotationCallout项目。

Map Kit的MKMapView渲染地图注释的最新iOS7变化让我重新审视了这个问题。 我已经对它做了更准确的思考,并提出了更好,更好的解决方案。 我将把上一个解决方案留在这个答案的底部,但请记住 – 当我这样做的时候,我错了。

首先,我们需要一个帮助器CGRectTransformToContainRect() ,它扩展给定的CGRect以包含另一个CGRect

注意:它的行为与CGRectUnion()的行为不同 – CGRectUnion()只返回包含两个CGRects的最小CGRect ,而下面的帮助器允许并行移动,即CGRectTransformToContainRect(CGRectMake(0, 0, 100, 100), CGRectMake(50, 50, 100, 100)) (CGRect){50, 50, 100, 100} CGRectTransformToContainRect(CGRectMake(0, 0, 100, 100), CGRectMake(50, 50, 100, 100))等于(CGRect){50, 50, 100, 100}而不是(CGRect){0, 0, 150, 150} CGRectUnion() (CGRect){0, 0, 150, 150}CGRectUnion()那样做。 当我们想要仅使用平行移动进行调整并且想要避免地图的缩放时,这种行为正是我们所需要

 static inline CGRect CGRectTransformToContainRect(CGRect rectToTransform, CGRect rectToContain) { CGFloat diff; CGRect transformedRect = rectToTransform; // Transformed rect dimensions should encompass the dimensions of both rects transformedRect.size.width = MAX(CGRectGetWidth(rectToTransform), CGRectGetWidth(rectToContain)); transformedRect.size.height = MAX(CGRectGetHeight(rectToTransform), CGRectGetHeight(rectToContain)); // Comparing max X borders of both rects, adjust if if ((diff = CGRectGetMaxX(rectToContain) - CGRectGetMaxX(transformedRect)) > 0) { transformedRect.origin.x += diff; } // Comparing min X borders of both rects, adjust if else if ((diff = CGRectGetMinX(transformedRect) - CGRectGetMinX(rectToContain)) > 0) { transformedRect.origin.x -= diff; } // Comparing max Y borders of both rects, adjust if if ((diff = CGRectGetMaxY(rectToContain) - CGRectGetMaxY(transformedRect)) > 0) { transformedRect.origin.y += diff; } // Comparing min Y borders of both rects, adjust if else if ((diff = CGRectGetMinY(transformedRect) - CGRectGetMinY(rectToContain)) > 0) { transformedRect.origin.y -= diff; } return transformedRect; } Adjust method wrapped into an Objective-C category MKMapView(Extensions): @implementation MKMapView (Extensions) - (void)adjustToContainRect:(CGRect)rect usingReferenceView:(UIView *)referenceView animated:(BOOL)animated { // I just like this assert here NSParameterAssert(referenceView); CGRect visibleRect = [self convertRegion:self.region toRectToView:self]; // We convert our annotation from its own coordinate system to a coodinate system of a map's top view, so we can compare it with the bounds of the map itself CGRect annotationRect = [self convertRect:rect fromView:referenceView.superview]; // Fatten the area occupied by your annotation if you want to have a margin after adjustment CGFloat additionalMargin = 2; adjustedRect.origin.x -= additionalMargin; adjustedRect.origin.y -= additionalMargin; adjustedRect.size.width += additionalMargin * 2; adjustedRect.size.height += additionalMargin * 2; // This is the magic: if the map must expand its bounds to contain annotation, it will do this CGRect adjustedRect = CGRectTransformToContainRect(visibleRect, annotationRect); // Now we just convert adjusted rect to a coordinate region MKCoordinateRegion adjustedRegion = [self convertRect:adjustedRect toRegionFromView:self]; // Trivial regionThatFits: sugar and final setRegion:animated: call [self setRegion:[self regionThatFits:adjustedRegion] animated:animated]; } @end 

现在控制器和视图:

 @interface AnnotationView : MKAnnotationView @property AnnotationCalloutView *calloutView; @property (readonly) CGRect annotationViewWithCalloutViewFrame; @end @implementation AnnotationView - (void)showCalloutBubble { // This is a code where you create your custom annotation callout view // add add it using -[self addSubview:] // At the end of this method a callout view should be displayed. } - (CGRect)annotationViewWithCalloutViewFrame { // Here you should adjust your annotation frame so it match itself in the moment when annotation callout is displayed and ... return CGRectOfAdjustedAnnotation; // ... } @end 

在地图上选择AnnotationView分类注释时,它会将其calloutView添加为子视图,因此会显示自定义注释标注视图。 它是使用MKMapViewDelegate的方法完成的:

 - (void)mapView:(MapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view { // AnnotationPresenter is just a class that contains information to be displayed on callout annotation view if ([view.annotation isKindOfClass:[AnnotationPresenter class]]) { // Hide another annotation if it is shown if (mapView.selectedAnnotationView != nil && [mapView.selectedAnnotationView isKindOfClass:[AnnotationView class]] && mapView.selectedAnnotationView != view) { [mapView.selectedAnnotationView hideCalloutBubble]; } mapView.selectedAnnotationView = view; annotationView *annotationView = (annotationView *)view; // This just adds *calloutView* as a subview [annotationView showCalloutBubble]; [mapView adjustToContainRect:annotationView.annotationViewWithCalloutViewFrame usingReferenceView:annotationView animated:NO]; } } 

当然,你的实现可能与我在这里描述的不同(我的是!)。 上面代码中最重要的部分当然是[MKMapView adjustToContainRect:usingReferenceView:animated: method。 现在我对当前的解决方案和我对这个(以及一些相关的)问题的理解非常满意。 如果您对上述解决方案有任何意见,请随时与我联系(见个人资料)。

以下Apple文档对于理解像[MKMapView convertRect:fromView:]这样的方法中发生的事情非常有用:

http://developer.apple.com/library/ios/#documentation/MapKit/Reference/MKMapView_Class/MKMapView/MKMapView.html

http://developer.apple.com/library/ios/#documentation/MapKit/Reference/MapKitDataTypesReference/Reference/reference.html

http://developer.apple.com/library/ios/#documentation/MapKit/Reference/MapKitFunctionsReference/Reference/reference.html

此外,WWDC 2013会议的前10-15分钟“Map Kit中的新function”(#304)非常适合观看Apple工程师完成的整个“带注释的地图”设置的精彩快速演示。


初始解决方案(在iOS7中不起作用,不要使用它,请使用上面的解决方案)

不知怎的,我忘了一次回答我的问题。 这是我现在使用的完整解决方案(为了便于阅读,略微编辑):

首先,将一些映射逻辑封装在帮助器文件中的某个地方,如MapKit + Helpers.h

 typedef struct { CLLocationDegrees top; CLLocationDegrees bottom; } MKLatitudeEdgedSpan; typedef struct { CLLocationDegrees left; CLLocationDegrees right; } MKLongitudeEdgedSpan; typedef struct { MKLatitudeEdgedSpan latitude; MKLongitudeEdgedSpan longitude; } MKEdgedRegion; MKEdgedRegion MKEdgedRegionFromCoordinateRegion(MKCoordinateRegion region) { MKEdgedRegion edgedRegion; float latitude = region.center.latitude; float longitude = region.center.longitude; float latitudeDelta = region.span.latitudeDelta; float longitudeDelta = region.span.longitudeDelta; edgedRegion.longitude.left = longitude - longitudeDelta / 2; edgedRegion.longitude.right = longitude + longitudeDelta / 2; edgedRegion.latitude.top = latitude + latitudeDelta / 2; edgedRegion.latitude.bottom = latitude - latitudeDelta / 2; return edgedRegion; } 

与MKCoordinateRegion(中心坐标+跨度)一样,MKEdgedRegion只是一种定义区域但使用其边缘坐标的方法。

MKEdgedRegionFromCoordinateRegion()是一个不言自明的转换器方法。

假设我们的注释有以下类,包含其标注作为子视图。

 @interface AnnotationView : MKAnnotationView @property AnnotationCalloutView *calloutView; @end 

在地图上选择AnnotationView分类注释时,它会将其calloutView添加为子视图,因此会显示自定义注释标注视图。 它是使用MKMapViewDelegate的方法完成的:

 - (void)mapView:(MapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view { // AnnotationPresenter is just a class that contains information to be displayed on callout annotation view if ([view.annotation isKindOfClass:[AnnotationPresenter class]]) { // Hide another annotation if it is shown if (mapView.selectedAnnotationView != nil && [mapView.selectedAnnotationView isKindOfClass:[AnnotationView class]] && mapView.selectedAnnotationView != view) { [mapView.selectedAnnotationView hideCalloutBubble]; } mapView.selectedAnnotationView = view; annotationView *annotationView = (annotationView *)view; // This just adds *calloutView* as a subview [annotationView showCalloutBubble]; /* Here the trickiest piece of code goes */ /* 1. We capture _annotation's (not callout's)_ frame in its superview's (map's!) coordinate system resulting in something like (CGRect){4910547.000000, 2967852.000000, 23.000000, 28.000000} The .origin.x and .origin.y are especially important! */ CGRect annotationFrame = annotationView.frame; /* 2. Now we need to perform an adjustment, so our frame would correspond to the annotation view's _callout view subview_ that it holds. */ annotationFrame.origin.x = annotationFrame.origin.x + ANNOTATION_CALLOUT_TRIANLE_HALF; // Mine callout view has small x offset - you should choose yours! annotationFrame.origin.y = annotationFrame.origin.y - ANNOTATION_CALLOUT_HEIGHT / 2; // Again my custom offset. annotationFrame.size = placeAnnotationView.calloutView.frame.size; // We can grab calloutView size directly because in its case we don't care about the coordinate system. MKCoordinateRegion mapRegion = mapView.region; /* 3. This was a long run before I did stop to try to pass mapView.view as an argument to _toRegionFromView_. */ /* annotationView.superView is very important - it gives us the same coordinate system that annotationFrame.origin is based. */ MKCoordinateRegion annotationRegion = [mapView convertRect:annotationFrame toRegionFromView:annotationView.superview]; /* I hope that the following MKEdgedRegion magic is self-explanatory */ MKEdgedRegion mapEdgedRegion = MKEdgedRegionFromCoordinateRegion(mapRegion); MKEdgedRegion annotationEdgedRegion = MKEdgedRegionFromCoordinateRegion(annotationRegion); float diff; if ((diff = (annotationEdgedRegion.longitude.left - mapEdgedRegion.longitude.left)) < 0 || (diff = (annotationEdgedRegion.longitude.right - mapEdgedRegion.longitude.right)) > 0) mapRegion.center.longitude += diff; if ((diff = (annotationEdgedRegion.latitude.bottom - mapEdgedRegion.latitude.bottom)) < 0 || (diff = (annotationEdgedRegion.latitude.top - mapEdgedRegion.latitude.top)) > 0) mapRegion.center.latitude += diff; mapView.region = mapRegion; } } 

我正在寻找一个类似的解决方案,以适应屏幕可见矩形中的路线和标注。 我尝试了一些解决方案,但最终只是在setVisibleMapRect:edgePadding:animated:上设置了足够的填充setVisibleMapRect:edgePadding:animated: . 可能不那么复杂,但基本上做我需要的。

 MKMapRect routeMapRect = myRoute.polyline.boundingMapRect; CGFloat padding = myCallout.bounds.width / 2.0; [myMapView setVisibleMapRect: routeMapRect edgePadding:UIEdgeInsetsMake(padding, padding, padding, padding) animated:YES]; 

当然,这可以更加优化,例如,检测您实际需要填充的哪一侧,并在另一侧设置较小的填充。 但是你明白了。