ios 带有按钮的自定义 MKAnnotation 标注气泡
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/17772108/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Custom MKAnnotation callout bubble with button
提问by Yanchi
I'm developing app, where user is localized by gps and then he is asked, whether he is located in specific place. To confirm this, callout bubble is presented to him straight away, asking him, if he is at specific place.
我正在开发应用程序,用户通过 gps 进行本地化,然后询问他是否位于特定位置。为了确认这一点,标注气泡会立即呈现给他,询问他是否在特定地点。
As there is alot of similar questions, I was able to do custom callout bubble:
由于有很多类似的问题,我能够自定义标注气泡:
My problem: button is not "clickable"
My guess: because this custom callout is higher than standard callout bubble, I had to place it in negative "frame", therefore button cannot be clicked. Here is my didSelectAnnotationView
method
我的问题:按钮不是“可点击” 我的猜测:因为这个自定义标注高于标准标注气泡,我不得不将它放在负“框架”中,因此无法点击按钮。这是我的didSelectAnnotationView
方法
- (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view {
if(![view.annotation isKindOfClass:[MKUserLocation class]]) {
CalloutView *calloutView = (CalloutView *)[[[NSBundle mainBundle] loadNibNamed:@"callOutView" owner:self options:nil] objectAtIndex:0];
CGRect calloutViewFrame = calloutView.frame;
calloutViewFrame.origin = CGPointMake(-calloutViewFrame.size.width/2 + 15, -calloutViewFrame.size.height);
calloutView.frame = calloutViewFrame;
[calloutView.calloutLabel setText:[(MyLocation*)[view annotation] title]];
[calloutView.btnYes addTarget:self
action:@selector(checkin)
forControlEvents:UIControlEventTouchUpInside];
calloutView.userInteractionEnabled = YES;
view.userInteractionEnabled = YES;
[view addSubview:calloutView];
}
}
CalloutView is just simple class with 2 properties(label that shows name of place and button) and with xib.
CalloutView 只是具有 2 个属性(显示地点和按钮名称的标签)和 xib 的简单类。
I have been doing this custom callout bubble for a few days. I tried using "asynchrony solutions" solutionbut I was unable to add any other kind of button then disclosure button.
几天来我一直在做这个自定义标注气泡。我尝试使用“异步解决方案”解决方案,但我无法添加任何其他类型的按钮,然后是披露按钮。
My next attempt, was to find something that was easier than asynchrony solutions and modify it to my use. Thats how I found tochi's custom callout.
我的下一个尝试是找到比异步解决方案更容易的东西,并根据我的使用情况进行修改。这就是我找到tochi 的自定义标注的方式。
Based on his work, I was able to customize his bubble and change info button for my custom button. My problem however remained the same. In order to place my custom callout view on top of the pin, I had to give it negative frame, so my button was "clickable" only in bottom 5 pixels. It seems, that I have to maybe dig deeper into ios default callout bubble, subclass it and change frame of callout in there. But I'm really hopeless now.
根据他的工作,我能够为我的自定义按钮自定义他的气泡和更改信息按钮。然而,我的问题保持不变。为了将我的自定义标注视图放在图钉的顶部,我必须给它负框架,所以我的按钮只能在底部 5 个像素中“可点击”。看来,我可能必须更深入地研究 ios 默认标注气泡,将其子类化并更改其中的标注框架。但我现在真的很绝望。
If you guys could show me the right way, or give me advice, I'll be glad.
如果你们能告诉我正确的方法,或者给我建议,我会很高兴。
回答by Rob
There are several approaches to customizing callouts:
有几种自定义标注的方法:
The easiest approach is to use the existing right and left callout accessories, and put your button in one of those. For example:
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation { static NSString *identifier = @"MyAnnotationView"; if ([annotation isKindOfClass:[MKUserLocation class]]) { return nil; } MKPinAnnotationView *view = (id)[mapView dequeueReusableAnnotationViewWithIdentifier:identifier]; if (view) { view.annotation = annotation; } else { view = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:identifier]; view.canShowCallout = true; view.animatesDrop = true; view.rightCalloutAccessoryView = [self yesButton]; } return view; } - (UIButton *)yesButton { UIImage *image = [self yesButtonImage]; UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; button.frame = CGRectMake(0, 0, image.size.width, image.size.height); // don't use auto layout [button setImage:image forState:UIControlStateNormal]; [button addTarget:self action:@selector(didTapButton:) forControlEvents:UIControlEventPrimaryActionTriggered]; return button; } - (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control { NSLog(@"%s", __PRETTY_FUNCTION__); }
That yields:
If you really don't like the button on the right, where accessories generally go, you can turn off that accessory, and iOS 9 offers the opportunity to specify the
detailCalloutAccessoryView
, which replaces the callout's subtitle with whatever view you want:- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation { static NSString *identifier = @"MyAnnotationView"; if ([annotation isKindOfClass:[MKUserLocation class]]) { return nil; } MKPinAnnotationView *view = (id)[mapView dequeueReusableAnnotationViewWithIdentifier:identifier]; if (view) { view.annotation = annotation; } else { view = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:identifier]; view.canShowCallout = true; view.animatesDrop = true; } view.detailCalloutAccessoryView = [self detailViewForAnnotation:annotation]; return view; } - (UIView *)detailViewForAnnotation:(PlacemarkAnnotation *)annotation { UIView *view = [[UIView alloc] init]; view.translatesAutoresizingMaskIntoConstraints = false; UILabel *label = [[UILabel alloc] init]; label.text = annotation.placemark.name; label.font = [UIFont systemFontOfSize:20]; label.translatesAutoresizingMaskIntoConstraints = false; label.numberOfLines = 0; [view addSubview:label]; UIButton *button = [self yesButton]; [view addSubview:button]; NSDictionary *views = NSDictionaryOfVariableBindings(label, button); [view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[label]|" options:0 metrics:nil views:views]]; [view addConstraint:[NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeCenterX multiplier:1 constant:0]]; [view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[label]-[button]|" options:0 metrics:nil views:views]]; return view; } - (UIButton *)yesButton { UIImage *image = [self yesButtonImage]; UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; button.translatesAutoresizingMaskIntoConstraints = false; // use auto layout in this case [button setImage:image forState:UIControlStateNormal]; [button addTarget:self action:@selector(didTapButton:) forControlEvents:UIControlEventPrimaryActionTriggered]; return button; }
This yields:
If you really want to develop a custom callout yourself, the Location and Maps Programming Guideoutlines the steps involved:
In an iOS app, it's good practice to use the
mapView:annotationView:calloutAccessoryControlTapped:
delegate method to respond when users tap a callout view's control (as long as the control is a descendant ofUIControl
). In your implementation of this method you can discover the identity of the callout view's annotation view so that you know which annotation the user tapped. In a Mac app, the callout view's view controller can implement an action method that responds when a user clicks the control in a callout view.When you use a custom view instead of a standard callout, you need to do extra work to make sure your callout shows and hides appropriately when users interact with it. The steps below outline the process for creating a custom callout that contains a button:
Design an
NSView
orUIView
subclass that represents the custom callout. It's likely that the subclass needs to implement thedrawRect:
method to draw your custom content.Create a view controller that initializes the callout view and performs the action related to the button.
In the annotation view, implement
hitTest:
to respond to hits that are outside the annotation view's bounds but inside the callout view's bounds, as shown in Listing 6-7.In the annotation view, implement
setSelected:animated:
to add your callout view as a subview of the annotation view when the user clicks or taps it.If the callout view is already visible when the user selects it, the
setSelected:
method should remove the callout subview from the annotation view (see Listing 6-8).In the annotation view's
initWithAnnotation:
method, set thecanShowCallout
property toNO
to prevent the map from displaying the standard callout when the user selects the annotation.
While it's in Swift, https://github.com/robertmryan/CustomMapViewAnnotationCalloutSwiftillustrates an example of how you can do this complete customization of the callout (e.g. change shape of callout bubble, change background color, etc.).
That previous point outlines a pretty complicated scenarios (i.e. you have to write your own code to detecting taps outside the view in order to dismiss the it). If you're supporting iOS 9, you might just use a popover view controller, e.g.:
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation { static NSString *identifier = @"MyAnnotationView"; if ([annotation isKindOfClass:[MKUserLocation class]]) { return nil; } MKPinAnnotationView *view = (id)[mapView dequeueReusableAnnotationViewWithIdentifier:identifier]; if (view) { view.annotation = annotation; } else { view = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:identifier]; view.canShowCallout = false; // note, we're not going to use the system callout view.animatesDrop = true; } return view; } - (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view { PopoverController *controller = [self.storyboard instantiateViewControllerWithIdentifier:@"AnnotationPopover"]; controller.modalPresentationStyle = UIModalPresentationPopover; controller.popoverPresentationController.sourceView = view; // adjust sourceRect so it's centered over the annotation CGRect sourceRect = CGRectZero; sourceRect.origin.x += [mapView convertCoordinate:view.annotation.coordinate toPointToView:mapView].x - view.frame.origin.x; sourceRect.size.height = view.frame.size.height; controller.popoverPresentationController.sourceRect = sourceRect; controller.annotation = view.annotation; [self presentViewController:controller animated:TRUE completion:nil]; [mapView deselectAnnotation:view.annotation animated:true]; // deselect the annotation so that when we dismiss the popover, the annotation won't still be selected }
最简单的方法是使用现有的左右标注附件,并将您的按钮放在其中之一。例如:
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation { static NSString *identifier = @"MyAnnotationView"; if ([annotation isKindOfClass:[MKUserLocation class]]) { return nil; } MKPinAnnotationView *view = (id)[mapView dequeueReusableAnnotationViewWithIdentifier:identifier]; if (view) { view.annotation = annotation; } else { view = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:identifier]; view.canShowCallout = true; view.animatesDrop = true; view.rightCalloutAccessoryView = [self yesButton]; } return view; } - (UIButton *)yesButton { UIImage *image = [self yesButtonImage]; UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; button.frame = CGRectMake(0, 0, image.size.width, image.size.height); // don't use auto layout [button setImage:image forState:UIControlStateNormal]; [button addTarget:self action:@selector(didTapButton:) forControlEvents:UIControlEventPrimaryActionTriggered]; return button; } - (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control { NSLog(@"%s", __PRETTY_FUNCTION__); }
这产生:
如果你真的不喜欢右边的按钮,附件通常放在那里,你可以关闭那个附件,iOS 9 提供了指定 的机会
detailCalloutAccessoryView
,它用你想要的任何视图替换标注的副标题:- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation { static NSString *identifier = @"MyAnnotationView"; if ([annotation isKindOfClass:[MKUserLocation class]]) { return nil; } MKPinAnnotationView *view = (id)[mapView dequeueReusableAnnotationViewWithIdentifier:identifier]; if (view) { view.annotation = annotation; } else { view = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:identifier]; view.canShowCallout = true; view.animatesDrop = true; } view.detailCalloutAccessoryView = [self detailViewForAnnotation:annotation]; return view; } - (UIView *)detailViewForAnnotation:(PlacemarkAnnotation *)annotation { UIView *view = [[UIView alloc] init]; view.translatesAutoresizingMaskIntoConstraints = false; UILabel *label = [[UILabel alloc] init]; label.text = annotation.placemark.name; label.font = [UIFont systemFontOfSize:20]; label.translatesAutoresizingMaskIntoConstraints = false; label.numberOfLines = 0; [view addSubview:label]; UIButton *button = [self yesButton]; [view addSubview:button]; NSDictionary *views = NSDictionaryOfVariableBindings(label, button); [view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[label]|" options:0 metrics:nil views:views]]; [view addConstraint:[NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeCenterX multiplier:1 constant:0]]; [view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[label]-[button]|" options:0 metrics:nil views:views]]; return view; } - (UIButton *)yesButton { UIImage *image = [self yesButtonImage]; UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; button.translatesAutoresizingMaskIntoConstraints = false; // use auto layout in this case [button setImage:image forState:UIControlStateNormal]; [button addTarget:self action:@selector(didTapButton:) forControlEvents:UIControlEventPrimaryActionTriggered]; return button; }
这产生:
如果您真的想自己开发自定义标注,位置和地图编程指南概述了所涉及的步骤:
在 iOS 应用程序中,
mapView:annotationView:calloutAccessoryControlTapped:
当用户点击标注视图的控件时,最好使用委托方法进行响应(只要该控件是 的后代UIControl
)。在此方法的实现中,您可以发现标注视图的注释视图的标识,以便您知道用户点击了哪个注释。在 Mac 应用程序中,标注视图的视图控制器可以实现一个动作方法,当用户单击标注视图中的控件时做出响应。当您使用自定义视图而不是标准标注时,您需要做额外的工作以确保您的标注在用户与其交互时正确显示和隐藏。以下步骤概述了创建包含按钮的自定义标注的过程:
设计一个代表自定义标注的
NSView
或UIView
子类。很可能子类需要实现drawRect:
绘制自定义内容的方法。创建一个视图控制器,用于初始化标注视图并执行与按钮相关的操作。
在注释视图中,实现
hitTest:
响应超出注释视图边界但在标注视图边界内的点击,如清单 6-7 所示。在注释视图中,实现
setSelected:animated:
在用户单击或点击时将标注视图添加为注释视图的子视图。如果标注视图在用户选择时已经可见,则该
setSelected:
方法应该从注释视图中删除标注子视图(参见清单 6-8)。在注解视图的
initWithAnnotation:
方法中,将canShowCallout
属性设置NO
为防止地图在用户选择注解时显示标准标注。
虽然它在 Swift 中,但https://github.com/robertmryan/CustomMapViewAnnotationCalloutSwift举例说明了如何完全自定义标注(例如更改标注气泡的形状、更改背景颜色等)。
前一点概述了一个非常复杂的场景(即,您必须编写自己的代码来检测视图外的点击以消除它)。如果您支持 iOS 9,您可能只使用弹出视图控制器,例如:
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation { static NSString *identifier = @"MyAnnotationView"; if ([annotation isKindOfClass:[MKUserLocation class]]) { return nil; } MKPinAnnotationView *view = (id)[mapView dequeueReusableAnnotationViewWithIdentifier:identifier]; if (view) { view.annotation = annotation; } else { view = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:identifier]; view.canShowCallout = false; // note, we're not going to use the system callout view.animatesDrop = true; } return view; } - (void)mapView:(MKMapView *)mapView didSelectAnnotationView:(MKAnnotationView *)view { PopoverController *controller = [self.storyboard instantiateViewControllerWithIdentifier:@"AnnotationPopover"]; controller.modalPresentationStyle = UIModalPresentationPopover; controller.popoverPresentationController.sourceView = view; // adjust sourceRect so it's centered over the annotation CGRect sourceRect = CGRectZero; sourceRect.origin.x += [mapView convertCoordinate:view.annotation.coordinate toPointToView:mapView].x - view.frame.origin.x; sourceRect.size.height = view.frame.size.height; controller.popoverPresentationController.sourceRect = sourceRect; controller.annotation = view.annotation; [self presentViewController:controller animated:TRUE completion:nil]; [mapView deselectAnnotation:view.annotation animated:true]; // deselect the annotation so that when we dismiss the popover, the annotation won't still be selected }
回答by psy
I wound up taking a different approach. I tried the others but they seemed bloated and I didn't want to add more classes or rely on the MKMapViewDelegate to handle the interaction.
我最终采取了不同的方法。我尝试了其他的,但它们看起来很臃肿,我不想添加更多的类或依赖 MKMapViewDelegate 来处理交互。
I instead override setSelected:animated of my MKAnnotationView subclass. The trick is to expand the bounds of the annotationView after it it selected to fully encompass the call out view, and then return them back to normal after it is deselected. This will allow your custom call outs to accept touches and interactions outside the original bounds of the MKAnnotationView.
我改为覆盖我的 MKAnnotationView 子类的 setSelected:animated 。诀窍是在它选择后扩展 annotationView 的边界以完全包含调用视图,然后在取消选择后将它们恢复正常。这将允许您的自定义调用接受 MKAnnotationView 原始边界之外的触摸和交互。
Here is a trimmed down code sample to get anyone started:
这是一个精简的代码示例,可以让任何人开始:
#define kAnnotationCalloutBoxTag 787801
#define kAnnotationCalloutArrowTag 787802
#define kAnnotationTempImageViewTag 787803
-(void)setSelected:(BOOL)selected animated:(BOOL)animated
{
if (selected == self.selected)
{
NSLog(@"annotation already selected, abort!");
return;
}
if (selected)
{
self.image = nil; //hide default image otherwise it takes the shape of the entire bounds
UIView* calloutBox = [self newCustomCallout];
float imgW = [self unselectedSize].width;
float imgH = [self unselectedSize].height;
float arrowW = 20;
float arrowH = 12;
//Annotation frames wrap a center coordinate, in this instance we want the call out box to fall under the
//central coordinate, so we need to adjust the height to be double what the callout box height would be
//making the height *2, this is to make sure the callout view is inside of it.
self.bounds = CGRectMake(0, 0, calloutBox.frame.size.width, calloutBox.frame.size.height*2 + arrowH*2 + imgH);
CGPoint center = CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2);
UIView* imgView = [[UIImageView alloc] initWithImage:icon];
[imgView setFrame:CGRectMake(center.x - imgW/2, center.y-imgH/2, imgW, imgH)];
imgView.tag = kAnnotationTempImageViewTag;
[self addSubview:imgView];
UIView* triangle = [self newTriangleViewWithFrame:CGRectMake(center.x-arrowW/2, center.y+imgH/2, arrowW, arrowH)];
triangle.tag = kAnnotationCalloutArrowTag;
[self addSubview:triangle];
[calloutBox setFrame:CGRectMake(0, center.y+imgH/2+arrowH, calloutBox.width, calloutBox.height)];
calloutBox.tag = kAnnotationCalloutBoxTag;
[self addSubview:calloutBox];
}
else
{
//return things back to normal
UIView* v = [self viewWithTag:kAnnotationCalloutBoxTag];
[v removeFromSuperview];
v = [self viewWithTag:kAnnotationCalloutArrowTag];
[v removeFromSuperview];
v = [self viewWithTag:kAnnotationTempImageViewTag];
[v removeFromSuperview];
self.image = icon;
self.bounds = CGRectMake(0, 0, [self unselectedSize].width, [self unselectedSize].height);
}
[super setSelected:selected animated:animated];
}
-(CGSize)unselectedSize
{
return CGSizeMake(20,20);
}
-(UIView*)newCustomCallout
{
//create your own custom call out view
UIView* v = [[UIView alloc] initWithFrame:CGRectMake(0,0,250,250)];
v.backgroundColor = [UIColor greenColor];
return v;
}
-(UIView*)newTriangleWithFrame:(CGRect)frame
{
//create your own triangle
UIImageView* v = [[UIImageView alloc] initWithFrame:frame];
[v setImage:[UIImage imageNamed:@"trianglePointedUp.png"]];
return v;
}
回答by Abo3atef
(void)mapView:(MKMapView *)mapViewIn didSelectAnnotationView:(MKAnnotationView *)view {
if(![view.annotation isKindOfClass:[MKUserLocation class]])
{
CustomeCalloutViewController *calloutView = [[CustomeCalloutViewController alloc]initWithNibName:@"CustomeCalloutViewController" bundle:nil];
[calloutView setPopinTransitionStyle:BKTPopinTransitionStyleSlide];
[calloutView setPopinTransitionDirection:BKTPopinTransitionDirectionTop];
[self presentPopinController:calloutView animated:YES completion:^{
NSLog(@"Popin presented !");
}];
[mapView deselectAnnotation:view.annotation animated:true];
}
}