如何自定义 MKAnnotationView 的标注气泡?

我现在正在使用地图工具包,我被卡住了。

我有一个自定义的注释视图,我正在使用,我想使用图像属性显示点在地图上与我自己的图标。我把它修好了。但是我还想覆盖默认的调出视图(当触摸注释图标时,标题/字幕显示的泡泡)。我希望能够控制标注本身: mapkit 只提供对左侧和右侧辅助标注视图的访问,但是没有办法为标注气泡提供自定义视图,或者给它零大小,或者其他任何东西。

我的想法是在我的 MKMapViewDelegate中覆盖 selectAnnotation/deselectAnnotation,然后通过调用我的自定义注释视图来绘制我自己的自定义视图。这可以工作,但只有在我的自定义注释视图类中将 canShowCallout设置为 YES时才可以。如果我将这些方法设置为 NO,则不会调用这些方法(这正是我想要的,因此不会绘制默认的调出气泡)。因此,如果没有默认的标注泡泡视图显示出来,我就无法知道用户是触摸了我在地图上的点(选中了它)还是触摸了一个不属于我的注释视图的点(删除了它)。

我试着走一条不同的道路,只是处理所有触摸事件自己在地图上,我似乎不能得到这个工作。我读过其他一些关于在地图视图中捕捉触摸事件的帖子,但它们并不完全是我想要的。是否有一种方法可以在绘制之前在地图视图中删除标注气泡?我不知所措。

有什么建议吗? 我是不是漏掉了什么明显的东西?

63006 次浏览

I had the same problem. There is a serious of blog posts about this topic on this blog http://spitzkoff.com/craig/?p=81.

Just using the MKMapViewDelegate doesn't help you here and subclassing MKMapView and trying to extend the existing functionality also didn't work for me.

What I ended up doing is to create my own CustomCalloutView that I am having on top of my MKMapView. You can style this view in any way you want.

My CustomCalloutView has a method similar to this one:


- (void) openForAnnotation: (id)anAnnotation
{
self.annotation = anAnnotation;
// remove from view
[self removeFromSuperview];


titleLabel.text = self.annotation.title;


[self updateSubviews];
[self updateSpeechBubble];


[self.mapView addSubview: self];
}

It takes an MKAnnotation object and sets its own title, afterward it calls two other methods which are quite ugly which adjust the width and size of the callout contents and afterward draw the speech bubble around it at the correct position.

Finally the view is added as a subview to the mapView. The problem with this solution is that it is hard to keep the callout at the correct position when the map view is scrolled. I am just hiding the callout in the map views delegate method on a region change to solve this problem.

It took some time to solve all those problems, but now the callout almost behaves like the official one, but I have it in my own style.

Basically to solve this, one needs to: a) Prevent the default callout bubble from coming up. b) Figure out which annotation was clicked.

I was able to achieve these by: a) setting canShowCallout to NO b) subclassing, MKPinAnnotationView and overriding the touchesBegan and touchesEnd methods.

Note: You need to handle the touch events for the MKAnnotationView and not MKMapView

You can use leftCalloutView, setting annotation.text to @" "

Please find below the example code:

pinView = (MKPinAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:defaultPinID];
if(pinView == nil){
pinView = [[[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:defaultPinID] autorelease];
}
CGSize sizeText = [annotation.title sizeWithFont:[UIFont fontWithName:@"HelveticaNeue" size:12] constrainedToSize:CGSizeMake(150, CGRectGetHeight(pinView.frame))                                 lineBreakMode:UILineBreakModeTailTruncation];
pinView.canShowCallout = YES;
UILabel *lblTitolo = [[UILabel alloc] initWithFrame:CGRectMake(2,2,150,sizeText.height)];
lblTitolo.text = [NSString stringWithString:ann.title];
lblTitolo.font = [UIFont fontWithName:@"HelveticaNeue" size:12];
lblTitolo.lineBreakMode = UILineBreakModeTailTruncation;
lblTitolo.numberOfLines = 0;
pinView.leftCalloutAccessoryView = lblTitolo;
[lblTitolo release];
annotation.title = @" ";

There is an even easier solution.

Create a custom UIView (for your callout).

Then create a subclass of MKAnnotationView and override setSelected as follows:

- (void)setSelected:(BOOL)selected animated:(BOOL)animated
{
[super setSelected:selected animated:animated];


if(selected)
{
//Add your custom view to self...
}
else
{
//Remove your custom view...
}
}

Boom, job done.

Found this to be the best solution for me. You'll have to use some creativity to do your own customizations

In your MKAnnotationView subclass, you can use

- (void)didAddSubview:(UIView *)subview{
int image = 0;
int labelcount = 0;
if ([[[subview class] description] isEqualToString:@"UICalloutView"]) {
for (UIView *subsubView in subview.subviews) {
if ([subsubView class] == [UIImageView class]) {
UIImageView *imageView = ((UIImageView *)subsubView);
switch (image) {
case 0:
[imageView setImage:[UIImage imageNamed:@"map_left"]];
break;
case 1:
[imageView setImage:[UIImage imageNamed:@"map_right"]];
break;
case 3:
[imageView setImage:[UIImage imageNamed:@"map_arrow"]];
break;
default:
[imageView setImage:[UIImage imageNamed:@"map_mid"]];
break;
}
image++;
}else if ([subsubView class] == [UILabel class]) {
UILabel *labelView = ((UILabel *)subsubView);
switch (labelcount) {
case 0:
labelView.textColor = [UIColor blackColor];
break;
case 1:
labelView.textColor = [UIColor lightGrayColor];
break;


default:
break;
}
labelView.shadowOffset = CGSizeMake(0, 0);
[labelView sizeToFit];
labelcount++;
}
}
}
}

And if the subview is a UICalloutView, then you can screw around with it, and what's inside it.

Continuing on from @TappCandy's brilliantly simple answer, if you want to animate your bubble in the same way as the default one, I've produced this animation method:

- (void)animateIn
{
float myBubbleWidth = 247;
float myBubbleHeight = 59;


calloutView.frame = CGRectMake(-myBubbleWidth*0.005+8, -myBubbleHeight*0.01-2, myBubbleWidth*0.01, myBubbleHeight*0.01);
[self addSubview:calloutView];


[UIView animateWithDuration:0.12 delay:0.0 options:UIViewAnimationOptionCurveEaseOut animations:^(void) {
calloutView.frame = CGRectMake(-myBubbleWidth*0.55+8, -myBubbleHeight*1.1-2, myBubbleWidth*1.1, myBubbleHeight*1.1);
} completion:^(BOOL finished) {
[UIView animateWithDuration:0.1 animations:^(void) {
calloutView.frame = CGRectMake(-myBubbleWidth*0.475+8, -myBubbleHeight*0.95-2, myBubbleWidth*0.95, myBubbleHeight*0.95);
} completion:^(BOOL finished) {
[UIView animateWithDuration:0.075 animations:^(void) {
calloutView.frame = CGRectMake(-round(myBubbleWidth/2-8), -myBubbleHeight-2, myBubbleWidth, myBubbleHeight);
}];
}];
}];
}

It looks fairly complicated, but as long as the point of your callout bubble is designed to be centre-bottom, you should just be able to replace myBubbleWidth and myBubbleHeight with your own size for it to work. And remember to make sure your subviews have their autoResizeMask property set to 63 (i.e. "all") so that they scale correctly in the animation.

:-Joe

I just come up with an approach, the idea here is

  // Detect the touch point of the AnnotationView ( i mean the red or green pin )
// Based on that draw a UIView and add it to subview.
- (void)mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated
{
CGPoint newPoint = [self.mapView convertCoordinate:selectedCoordinate toPointToView:self.view];
//    NSLog(@"regionWillChangeAnimated newPoint %f,%f",newPoint.x,newPoint.y);
[testview  setCenter:CGPointMake(newPoint.x+5,newPoint.y-((testview.frame.size.height/2)+35))];
[testview setHidden:YES];
}


- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated
{
CGPoint newPoint = [self.mapView convertCoordinate:selectedCoordinate toPointToView:self.view];
//    NSLog(@"regionDidChangeAnimated newPoint %f,%f",newPoint.x,newPoint.y);
[testview  setCenter:CGPointMake(newPoint.x,newPoint.y-((testview.frame.size.height/2)+35))];
[testview setHidden:NO];
}


- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view
{
NSLog(@"Select");
showCallout = YES;
CGPoint point = [self.mapView convertPoint:view.frame.origin fromView:view.superview];
[testview setHidden:NO];
[testview  setCenter:CGPointMake(point.x+5,point.y-(testview.frame.size.height/2))];
selectedCoordinate = view.annotation.coordinate;
[self animateIn];
}


- (void)mapView:(MKMapView *)mapView didDeselectAnnotationView:(MKAnnotationView *)view
{
NSLog(@"deSelect");
if(!showCallout)
{
[testview setHidden:YES];
}
}

Here - testview is a UIView of size 320x100 - showCallout is BOOL - [self animateIn]; is the function that does view animation like UIAlertView.

detailCalloutAccessoryView

In the olden days this was a pain, but Apple has solved it, just check the docs on MKAnnotationView

view = MKPinAnnotationView(annotation: annotation, reuseIdentifier: identifier)
view.canShowCallout = true
view.detailCalloutAccessoryView = UIImageView(image: UIImage(named: "zebra"))

Really, that's it. Takes any UIView.

I've pushed out my fork of the excellent SMCalloutView that solves the issue with providing a custom view for callouts and allowing flexible widths/heights pretty painlessly. Still some quirks to work out, but it's pretty functional so far:

https://github.com/u10int/calloutview