目前的解决方案
我已经制作了演示项目,实现了下面讨论的内容:请参阅那里调整区域以适合注释标注 https://github.com/stanislaw/Examples/tree/20140114-adjust-region-to-fit-annotation-callout项目。
最新的iOS7对Map Kit的MKMapView渲染地图标注方式的改变让我重新审视这个问题。我对此进行了更准确的思考,并提出了更好的解决方案。我将把之前的解决方案留在这个答案的底部,但请记住 - 当我这样做时我错了。
首先我们需要一个帮手CGRectTransformToContainRect()
扩展给定的CGRect
包含另一个CGRect
.
注意:它的行为与CGRectUnion()
does - CGRectUnion()
仅返回最小的CGRect
包含两者CGRects
,而以下助手允许平行移动,即CGRectTransformToContainRect(CGRectMake(0, 0, 100, 100), CGRectMake(50, 50, 100, 100))
equals (CGRect){50, 50, 100, 100}
并不是(CGRect){0, 0, 150, 150}
like 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 类注释时,它会添加其标注视图作为子视图,因此显示自定义注释标注视图。这是使用 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:
方法。现在我对当前的解决方案以及我对这个(和一些相关)问题的理解非常满意。如果您对上述解决方案有任何意见,请随时与我联系(请参阅简介)。
以下 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/MKMapView_Class/MKMapView/MKMapView.html
http://developer.apple.com/library/ios/#documentation/MapKit/Reference/MapKit 数据类型参考/Reference/reference.html http://developer.apple.com/library/ios/#documentation/MapKit/Reference/MapKitDataTypesReference/Reference/reference.html
http://developer.apple.com/library/ios/#documentation/MapKit/Reference/MapKit 函数参考/Reference/reference.html http://developer.apple.com/library/ios/#documentation/MapKit/Reference/MapKitFunctionsReference/Reference/reference.html
此外,WWDC 2013 会议“Map Kit 的新增功能”(#304) 的前 10-15 分钟也非常值得观看,因为可以快速演示由 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 只是定义区域的一种方法,但使用其边缘的坐标。
MKEdgedRegionFromCooperativeRegion() 是一个不言自明的转换器方法。
假设我们有以下注释类,其中包含其标注作为子视图。
@interface AnnotationView : MKAnnotationView
@property AnnotationCalloutView *calloutView;
@end
当在地图上选择 AnnotationView 类注释时,它会添加其标注视图作为子视图,因此显示自定义注释标注视图。这是使用 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;
}
}