0
回答
自定义callouts part 1

前段时间开发地图程序的时候,有自定义annotation的callouts的需求,看到这个需求的时候毫无头绪,在网上找资料找了很久,最后终于找到了方法,真是功夫不负有心人。网上已有人实现了这个功能,国外有人写了两篇博文专门介绍了,不过在国内这个网站好像已被屏蔽,需要翻墙才能浏览。这次在国外,我专门转到我的博客当中,以供国人观看。原文地址:part1, part2

原文是英文,我也没得时间翻译,不了解的地方直接看代码或给我留言。

 

---------------------------------------------------------------以下是转载-----------------------------------------------------------------------------

 

Part 1

 

 

The iPhone’s Map Annotation Callouts are very useful for displaying small amounts of information when a map pin (annotation) is selected. One problem with the standard callouts present in iOS is the inability to change the height of the callout.

For example, you may want to display a logo or other image that is taller than the default callout. Or you may want to display an address and phone number on separate lines under the title. Both of these scenarios are impossible using the standard iOS callouts. There are many steps to building a good replacement callout with the proper look and behavior, but it can be done.

 

 

 

Part 1 (explained here) will explain how to build a custom map callout.

Part 2 covers adding a button to the custom callout, which is not as simple as it sounds.

 

Put it on the map (and take it off)

For this example we will create two simple map annotations in the view controller – one will display the standard callout and the other will display the custom callout.

To place the “custom callout annotation” on the map we will add the custom annotation when the mapView calls the mapView:didSelectAnnotationView: method, and we will remove the callout on the corresponding deselect method, mapView:didDeselectAnnotationView:. InmapView:viewForAnnotation: we return an instance of our custom MKAnnotationView subclass. Also, we disable the standard callout on the “parent” annotation view, which we will show the custom callout for.

- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view {
    if (view.annotation == self.customAnnotation) {
        if (self.calloutAnnotation == nil) {
            self.calloutAnnotation = [[CalloutMapAnnotation alloc]
                initWithLatitude:view.annotation.coordinate.latitude
                andLongitude:view.annotation.coordinate.longitude];
        } else {
            self.calloutAnnotation.latitude = view.annotation.coordinate.latitude;
            self.calloutAnnotation.longitude = view.annotation.coordinate.longitude;
        }
        [self.mapView addAnnotation:self.calloutAnnotation];
        self.selectedAnnotationView = view;
    }
}
 
- (void)mapView:(MKMapView *)mapView didDeselectAnnotationView:(MKAnnotationView *)view {
    if (self.calloutAnnotation && view.annotation == self.customAnnotation) {
        [self.mapView removeAnnotation: self.calloutAnnotation];
    }
}
 
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation {
    if (annotation == self.calloutAnnotation) {
        CalloutMapAnnotationView *calloutMapAnnotationView = (CalloutMapAnnotationView *)[self.mapView dequeueReusableAnnotationViewWithIdentifier:@"CalloutAnnotation"];
        if (!calloutMapAnnotationView) {
            calloutMapAnnotationView = [[[CalloutMapAnnotationView alloc] initWithAnnotation:annotation
                reuseIdentifier:@"CalloutAnnotation"] autorelease];
        }
        calloutMapAnnotationView.parentAnnotationView = self.selectedAnnotationView;
        calloutMapAnnotationView.mapView = self.mapView;
        return calloutMapAnnotationView;
    } else if (annotation == self.customAnnotation) {
        MKPinAnnotationView *annotationView = [[[MKPinAnnotationView alloc] initWithAnnotation:annotation
                reuseIdentifier:@"CustomAnnotation"] autorelease];
        annotationView.canShowCallout = NO;
        annotationView.pinColor = MKPinAnnotationColorGreen;
        return annotationView;
    } else if (annotation == self.normalAnnotation) {
        MKPinAnnotationView *annotationView = [[[MKPinAnnotationView alloc] initWithAnnotation:annotation
                reuseIdentifier:@"NormalAnnotation"] autorelease];
        annotationView.canShowCallout = YES;
        annotationView.pinColor = MKPinAnnotationColorPurple;
        return annotationView;
    }
 
    return nil;
}
 

Note: If building for iOS 3.x you will need to determine annotation selection another way (KVO, notifications, etc.).

Draw the callout (in the right place)

Now that we have the callout annotation placed on the map at the same coordinate as the parent annotation, we need to adjust the width and height of the callout view and adjust the center offset so that the view spans the entire width of the map and sits above the parent annotation. These calculations will be done during setAnnotation: because ourcontentHeightoffsetFromParent, and mapView properties should have been set by then.setNeedsDisplay will also be called in setAnnotation: so that the callout is redrawn to match up with the annotation.

- (void)setAnnotation:(id <MKAnnotation>)annotation {
    [super setAnnotation:annotation];
    [self prepareFrameSize];
    [self prepareOffset];
    [self setNeedsDisplay];
}
 
- (void)prepareFrameSize {
    CGRect frame = self.frame;
    CGFloat height =    self.contentHeight +
    CalloutMapAnnotationViewContentHeightBuffer +
    CalloutMapAnnotationViewBottomShadowBufferSize -
    self.offsetFromParent.y;
 
    frame.size = CGSizeMake(self.mapView.frame.size.width, height);
    self.frame = frame;
}
 
- (void)prepareOffset {
    CGPoint parentOrigin = [self.mapView
                convertPoint:self.parentAnnotationView.frame.origin
                fromView:self.parentAnnotationView.superview];
 
    CGFloat xOffset =   (self.mapView.frame.size.width / 2) -
                        (parentOrigin.x + self.offsetFromParent.x);
 
    //Add half our height plus half of the height of the annotation we are tied to so that our bottom lines up to its top
    //Then take into account its offset and the extra space needed for our drop shadow
    CGFloat yOffset = -(self.frame.size.height / 2 +
                        self.parentAnnotationView.frame.size.height / 2) +
                        self.offsetFromParent.y +
                        CalloutMapAnnotationViewBottomShadowBufferSize;
 
    self.centerOffset = CGPointMake(xOffset, yOffset);
}
 

The shape of the callout bubble is basically a round-rectangle with a triangle that points to the parent annotation. Determining where that point should be is a matter of finding the x-coordinate of the parent relative to it and adding the offsetFromParent.x property. LuckilyUIView contains the handy convertPoint:fromView: method to handle the conversion between coordinate systems.

The steps to draw something similar to the standard callout are as follows:

  • Create the shape (path) of the callout bubble with the point in the right position to match up with the parent
  • Fill the path and add the shadow (adding the shadow here and then restoring the context prevents the shadow from being redrawn with each subsequent step)
  • Apply a stroke to the path (more opaque than the fill)
  • Create a round rectangle path to appear as the “gloss”
  • Fill the gloss path with a gradient
  • Convert the glass path to a “stroked path” (this will allow us to apply a gradient to the stroke)
  • Apply a gradient (light to transparent) to the stroked path

In code:

- (void)drawRect:(CGRect)rect {
    CGFloat stroke = 1.0;
    CGFloat radius = 7.0;
    CGMutablePathRef path = CGPathCreateMutable();
    UIColor *color;
    CGColorSpaceRef space = CGColorSpaceCreateDeviceRGB();
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGFloat parentX = [self relativeParentXPosition];
 
    //Determine Size
    rect = self.bounds;
    rect.size.width -= stroke + 14;
    rect.size.height -= stroke + CalloutMapAnnotationViewHeightAboveParent - self.offsetFromParent.y + CalloutMapAnnotationViewBottomShadowBufferSize;
    rect.origin.x += stroke / 2.0 + 7;
    rect.origin.y += stroke / 2.0;
 
    //Create Path For Callout Bubble
    CGPathMoveToPoint(path, NULL, rect.origin.x, rect.origin.y + radius);
    CGPathAddLineToPoint(path, NULL, rect.origin.x, rect.origin.y + rect.size.height - radius);
    CGPathAddArc(path, NULL, rect.origin.x + radius, rect.origin.y + rect.size.height - radius,
                 radius, M_PI, M_PI / 2, 1);
    CGPathAddLineToPoint(path, NULL, parentX - 15,
                         rect.origin.y + rect.size.height);
    CGPathAddLineToPoint(path, NULL, parentX,
                         rect.origin.y + rect.size.height + 15);
    CGPathAddLineToPoint(path, NULL, parentX + 15,
                         rect.origin.y + rect.size.height);
    CGPathAddLineToPoint(path, NULL, rect.origin.x + rect.size.width - radius,
                         rect.origin.y + rect.size.height);
    CGPathAddArc(path, NULL, rect.origin.x + rect.size.width - radius,
                 rect.origin.y + rect.size.height - radius, radius, M_PI / 2, 0.0f, 1);
    CGPathAddLineToPoint(path, NULL, rect.origin.x + rect.size.width, rect.origin.y + radius);
    CGPathAddArc(path, NULL, rect.origin.x + rect.size.width - radius, rect.origin.y + radius,
                 radius, 0.0f, -M_PI / 2, 1);
    CGPathAddLineToPoint(path, NULL, rect.origin.x + radius, rect.origin.y);
    CGPathAddArc(path, NULL, rect.origin.x + radius, rect.origin.y + radius, radius,
                 -M_PI / 2, M_PI, 1);
    CGPathCloseSubpath(path);
 
    //Fill Callout Bubble & Add Shadow
    color = [[UIColor blackColor] colorWithAlphaComponent:.6];
    [color setFill];
    CGContextAddPath(context, path);
    CGContextSaveGState(context);
    CGContextSetShadowWithColor(context, CGSizeMake (0, self.yShadowOffset), 6, [UIColor colorWithWhite:0 alpha:.5].CGColor);
    CGContextFillPath(context);
    CGContextRestoreGState(context);
 
    //Stroke Callout Bubble
    color = [[UIColor darkGrayColor] colorWithAlphaComponent:.9];
    [color setStroke];
    CGContextSetLineWidth(context, stroke);
    CGContextSetLineCap(context, kCGLineCapSquare);
    CGContextAddPath(context, path);
    CGContextStrokePath(context);
 
    //Determine Size for Gloss
    CGRect glossRect = self.bounds;
    glossRect.size.width = rect.size.width - stroke;
    glossRect.size.height = (rect.size.height - stroke) / 2;
    glossRect.origin.x = rect.origin.x + stroke / 2;
    glossRect.origin.y += rect.origin.y + stroke / 2;
 
    CGFloat glossTopRadius = radius - stroke / 2;
    CGFloat glossBottomRadius = radius / 1.5;
 
    //Create Path For Gloss
    CGMutablePathRef glossPath = CGPathCreateMutable();
    CGPathMoveToPoint(glossPath, NULL, glossRect.origin.x, glossRect.origin.y + glossTopRadius);
    CGPathAddLineToPoint(glossPath, NULL, glossRect.origin.x, glossRect.origin.y + glossRect.size.height - glossBottomRadius);
    CGPathAddArc(glossPath, NULL, glossRect.origin.x + glossBottomRadius, glossRect.origin.y + glossRect.size.height - glossBottomRadius,
                 glossBottomRadius, M_PI, M_PI / 2, 1);
    CGPathAddLineToPoint(glossPath, NULL, glossRect.origin.x + glossRect.size.width - glossBottomRadius,
                         glossRect.origin.y + glossRect.size.height);
    CGPathAddArc(glossPath, NULL, glossRect.origin.x + glossRect.size.width - glossBottomRadius,
                 glossRect.origin.y + glossRect.size.height - glossBottomRadius, glossBottomRadius, M_PI / 2, 0.0f, 1);
    CGPathAddLineToPoint(glossPath, NULL, glossRect.origin.x + glossRect.size.width, glossRect.origin.y + glossTopRadius);
    CGPathAddArc(glossPath, NULL, glossRect.origin.x + glossRect.size.width - glossTopRadius, glossRect.origin.y + glossTopRadius,
                 glossTopRadius, 0.0f, -M_PI / 2, 1);
    CGPathAddLineToPoint(glossPath, NULL, glossRect.origin.x + glossTopRadius, glossRect.origin.y);
    CGPathAddArc(glossPath, NULL, glossRect.origin.x + glossTopRadius, glossRect.origin.y + glossTopRadius, glossTopRadius,
                 -M_PI / 2, M_PI, 1);
    CGPathCloseSubpath(glossPath);
 
    //Fill Gloss Path
    CGContextAddPath(context, glossPath);
    CGContextClip(context);
    CGFloat colors[] =
    {
        1, 1, 1, .3,
        1, 1, 1, .1,
    };
    CGFloat locations[] = { 0, 1.0 };
    CGGradientRef gradient = CGGradientCreateWithColorComponents(space, colors, locations, 2);
    CGPoint startPoint = glossRect.origin;
    CGPoint endPoint = CGPointMake(glossRect.origin.x, glossRect.origin.y + glossRect.size.height);
    CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0);
 
    //Gradient Stroke Gloss Path
    CGContextAddPath(context, glossPath);
    CGContextSetLineWidth(context, 2);
    CGContextReplacePathWithStrokedPath(context);
    CGContextClip(context);
    CGFloat colors2[] =
    {
        1, 1, 1, .3,
        1, 1, 1, .1,
        1, 1, 1, .0,
    };
    CGFloat locations2[] = { 0, .1, 1.0 };
    CGGradientRef gradient2 = CGGradientCreateWithColorComponents(space, colors2, locations2, 3);
    CGPoint startPoint2 = glossRect.origin;
    CGPoint endPoint2 = CGPointMake(glossRect.origin.x, glossRect.origin.y + glossRect.size.height);
    CGContextDrawLinearGradient(context, gradient2, startPoint2, endPoint2, 0);
 
    //Cleanup
    CGPathRelease(path);
    CGPathRelease(glossPath);
    CGColorSpaceRelease(space);
    CGGradientRelease(gradient);
    CGGradientRelease(gradient2);
}
 
- (CGFloat)yShadowOffset {
    if (!_yShadowOffset) {
        float osVersion = [[[UIDevice currentDevice] systemVersion] floatValue];
        if (osVersion >= 3.2) {
            _yShadowOffset = 6;
        } else {
            _yShadowOffset = -6;
        }
 
    }
    return _yShadowOffset;
}
 
- (CGFloat)relativeParentXPosition {
    CGPoint parentOrigin = [self.mapView convertPoint:self.parentAnnotationView.frame.origin
                                             fromView:self.parentAnnotationView.superview];
    return parentOrigin.x + self.offsetFromParent.x;
}
 

Note: in iOS 3.2 CGContextSetShadowWithColor reversed the direction of the y-axis offset, thus requiring theyShadowOffset method seen above.

Let’s Add Some Content

To allow the addition of content we will create a content view as a read-only property, which will allow our consumers to access it. An additional method, prepareContentFrame will be added and invoked from setAnnotation: to set the content frame.

- (void)setAnnotation:(id <MKAnnotation>)annotation {
    [super setAnnotation:annotation];
    [self prepareFrameSize];
    [self prepareOffset];
    [self prepareContentFrame];
    [self setNeedsDisplay];
}
 
- (void)prepareContentFrame {
    CGRect contentFrame = CGRectMake(self.bounds.origin.x + 10,
                                     self.bounds.origin.y + 3,
                                     self.bounds.size.width - 20,
                                     self.contentHeight);
 
    self.contentView.frame = contentFrame;
}
 
- (UIView *)contentView {
    if (!_contentView) {
        _contentView = [[UIView alloc] init];
        self.contentView.backgroundColor = [UIColor clearColor];
        self.contentView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
        [self addSubview:self.contentView];
    }
    return _contentView;
}
 

In our map view controller we will add the following code in mapView:viewForAnnotation to place an image in the callout and set the proper content height.

calloutMapAnnotationView.contentHeight = 78.0f;
UIImage *asynchronyLogo = [UIImage imageNamed:@"asynchrony-logo-small.png"];
UIImageView *asynchronyLogoView = [[[UIImageView alloc] initWithImage:asynchronyLogo] autorelease];
asynchronyLogoView.frame = CGRectMake(5, 2, asynchronyLogoView.frame.size.width, asynchronyLogoView.frame.size.height);
[calloutMapAnnotationView.contentView addSubview:asynchronyLogoView];
 

Animation

So far the callout looks similar to the native callout, but it is still lacking some of the behavior of the original. The callout needs to animate out from the parent annotation. Also, when the parent annotation is near the edge of the map view, the map should be adjusted to move the parent annotation in from the edge of the view.

The animation would be fairly simple if we could just adjust the frame of the callout view, however that will not scale the contents of the callout. Thus, we must use aCGAffineTransform. Apple has a good introducton to affine transforms. The transform will need to both scale the view and translate the view to make it appear to grow out of the parent annotation. Scaling is simple – a value of 1 is normal size and other values act as a multiplier, so smaller values shrink the view and larger values expand the view. If the parent is off-center on the x-axis the callout needs to be translated to keep the point fixed directly over the parent annotation. Likewise the y-axis must be translated so that it appears that the callout grows upward from parent. We need to hold on to the frame for these calculations becauseself.frame cannot be trusted during the animations. The calculations are done in the following two methods:

- (CGFloat)xTransformForScale:(CGFloat)scale {
    CGFloat xDistanceFromCenterToParent = self.endFrame.size.width / 2 - [self relativeParentXPosition];
    return (xDistanceFromCenterToParent * scale) - xDistanceFromCenterToParent;
}
 
- (CGFloat)yTransformForScale:(CGFloat)scale {
    CGFloat yDistanceFromCenterToParent = (((self.endFrame.size.height) / 2) + self.offsetFromParent.y + CalloutMapAnnotationViewBottomShadowBufferSize + CalloutMapAnnotationViewHeightAboveParent);
    return yDistanceFromCenterToParent - yDistanceFromCenterToParent * scale;
}
 

There will be three steps to the animation to create the bounce-like effect of the standard callout. We cannot begin the animation with a scale of 0 because a transformation matrix with a scale of 0 cannot be inverted.

  1. Grow from very small to slightly larger than the final size
  2. Shrink to slightly smaller than the final size
  3. Grow to the final size

These three steps will be separate animations chained together using UIView’ssetAnimationDidStopSelector: and setAnimationDelegate: methods.

- (void)animateIn {
    self.endFrame = self.frame;
    CGFloat scale = 0.001f;
    self.transform = CGAffineTransformMake(scale, 0.0f, 0.0f, scale, [self xTransformForScale:scale], [self yTransformForScale:scale]);
    [UIView beginAnimations:@"animateIn" context:nil];
    [UIView setAnimationCurve:UIViewAnimationCurveEaseOut];
    [UIView setAnimationDuration:0.075];
    [UIView setAnimationDidStopSelector:@selector(animateInStepTwo)];
    [UIView setAnimationDelegate:self];
 
    scale = 1.1;
    self.transform = CGAffineTransformMake(scale, 0.0f, 0.0f, scale, [self xTransformForScale:scale], [self yTransformForScale:scale]);
 
    [UIView commitAnimations];
}
 
- (void)animateInStepTwo {
    [UIView beginAnimations:@"animateInStepTwo" context:nil];
    [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
    [UIView setAnimationDuration:0.1];
    [UIView setAnimationDidStopSelector:@selector(animateInStepThree)];
    [UIView setAnimationDelegate:self];
 
    CGFloat scale = 0.95;
    self.transform = CGAffineTransformMake(scale, 0.0f, 0.0f, scale, [self xTransformForScale:scale], [self yTransformForScale:scale]);
 
    [UIView commitAnimations];
}
 
- (void)animateInStepThree {
    [UIView beginAnimations:@"animateInStepThree" context:nil];
    [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
    [UIView setAnimationDuration:0.075];
 
    CGFloat scale = 1.0;
    self.transform = CGAffineTransformMake(scale, 0.0f, 0.0f, scale, [self xTransformForScale:scale], [self yTransformForScale:scale]);
 
    [UIView commitAnimations];
}
 

Shifting the Map

When the parent annotation is near the edge of the map, the map needs to be shifted so that the parent annotation and the callout remain a certain distance away from the edge of the view. To do this we need to calculate the distance to the edge of the view, the number of degrees latitude and longitude per pixel, and then set the new center point for the map. This adjustment should be made when didMoveToSuperview is called.

- (void)adjustMapRegionIfNeeded {
    //Longitude
    CGFloat xPixelShift = 0;
    if ([self relativeParentXPosition] < 38) {
        xPixelShift = 38 - [self relativeParentXPosition];
    } else if ([self relativeParentXPosition] > self.frame.size.width - 38) {
        xPixelShift = (self.frame.size.width - 38) - [self relativeParentXPosition];
    }
 
    //Latitude
    CGPoint mapViewOriginRelativeToParent = [self.mapView convertPoint:self.mapView.frame.origin toView:self.parentAnnotationView];
    CGFloat yPixelShift = 0;
    CGFloat pixelsFromTopOfMapView = -(mapViewOriginRelativeToParent.y + self.frame.size.height - CalloutMapAnnotationViewBottomShadowBufferSize);
    CGFloat pixelsFromBottomOfMapView = self.mapView.frame.size.height + mapViewOriginRelativeToParent.y - self.parentAnnotationView.frame.size.height;
    if (pixelsFromTopOfMapView < 7) {
        yPixelShift = 7 - pixelsFromTopOfMapView;
    } else if (pixelsFromBottomOfMapView < 10) {
        yPixelShift = -(10 - pixelsFromBottomOfMapView);
    }
 
    //Calculate new center point, if needed
    if (xPixelShift || yPixelShift) {
        CGFloat pixelsPerDegreeLongitude = self.mapView.frame.size.width / self.mapView.region.span.longitudeDelta;
        CGFloat pixelsPerDegreeLatitude = self.mapView.frame.size.height / self.mapView.region.span.latitudeDelta;
 
        CLLocationDegrees longitudinalShift = -(xPixelShift / pixelsPerDegreeLongitude);
        CLLocationDegrees latitudinalShift = yPixelShift / pixelsPerDegreeLatitude;
 
        CLLocationCoordinate2D newCenterCoordinate = {self.mapView.region.center.latitude + latitudinalShift,
            self.mapView.region.center.longitude + longitudinalShift};
 
        [self.mapView setCenterCoordinate:newCenterCoordinate animated:YES];
 
        //fix for now
        self.frame = CGRectMake(self.frame.origin.x - xPixelShift,
                                self.frame.origin.y - yPixelShift,
                                self.frame.size.width,
                                self.frame.size.height);
        //fix for later (after zoom or other action that resets the frame)
        self.centerOffset = CGPointMake(self.centerOffset.x - xPixelShift, self.centerOffset.y);
    }
}
 

Conclusion

It takes a bit of work to replicate the iOS map annotation callout, but it is worth the effort if you need a larger space for content. You may download the full source code to see a working example.

 

这是part1, part2马上奉上。

 

 

 

 

 

 

 

 


原文链接:http://blog.csdn.net/favormm/article/details/6183238
<无标签>
举报
长平狐
发帖于6年前 0回/214阅
顶部