【问题标题】:How to adjust region to fit custom annotation callout that have just appeared?如何调整区域以适应刚刚出现的自定义注释标注?
【发布时间】:2012-10-27 00:00:24
【问题描述】:

我使用 MKAnnotationView 的自定义子类。在我的 Map 委托的 mapView:didSelectAnnotationView: 方法中,我调用了此类的方法,该方法将带有图像的 UIImageView 添加为子视图 - 它用作我的自定义注释标注。

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

【问题讨论】:

    标签: ios mkmapview mkannotationview mapkit


    【解决方案1】:

    当前解决方案

    我制作了演示项目,实现了以下讨论的内容:请参阅 AdjustRegionToFitAnnotationCallout 项目。

    iOS7 中 Map Kit 的 MKMapView 如何呈现地图注释的最新变化让我重新审视了这个问题。我对此进行了更准确的思考,并提出了非常非常好的解决方案。我将把之前的解决方案留在这个答案的底部,但请记住 - 当我这样做时我错了。

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

    注意:它的行为与CGRectUnion() 的行为不同——CGRectUnion() 只返回包含CGRects 的最小CGRect,而下面的助手允许平行移动,即CGRectTransformToContainRect(CGRectMake(0, 0, 100, 100), CGRectMake(50, 50, 100, 100)) 等于(CGRect){50, 50, 100, 100} 而不是@ 987654334@ 就像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: 方法。现在我对当前的解决方案和我对这个(以及一些相关的)问题的理解非常满意。如果您需要有关上述解决方案的任何 cmets,请随时与我联系(请参阅个人资料)。

    以下 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 分钟“地图工具包的新功能”(#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;
        }
    }
    

    【讨论】:

    • 你能帮我吗,我无法理解你的代码。
    • 您好,如果这对您来说仍然是真实的,请随时使用 Skype 与我联系。我可以制作一个演示项目并展示给你看。
    • 请查看我的答案顶部 - 我刚刚更新了它,并提供了一个指向新创建的演示项目的链接。我希望一切都清楚。
    • 但是你不能点击calout视图的子视图
    【解决方案2】:

    我一直在寻找类似的解决方案,以在屏幕的可见矩形中适应路线和标注。我尝试了一些解决方案,但最终只是在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];
    

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-11-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-12-11
      • 1970-01-01
      相关资源
      最近更新 更多