26

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.

As there is alot of similar questions, I was able to do custom callout bubble: 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

- (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.

I have been doing this custom callout bubble for a few days. I tried using "asynchrony solutions" solution but 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's custom callout

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.

If you guys could show me the right way, or give me advice, I'll be glad.

4

3 回答 3

82

有几种自定义标注的方法:

  1. 最简单的方法是使用现有的左右标注配件,并将您的按钮放入其中之一。例如:

    - (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__);
    }
    

    这会产生:

    在此处输入图像描述

  2. 如果你真的不喜欢右边的按钮,附件通常放在哪里,你可以关闭那个附件,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;
    }
    

    这产生:

    在此处输入图像描述

  3. 如果您真的想自己开发自定义标注,位置和地图编程指南概述了所涉及的步骤:

    在 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举例说明了如何对标注进行完全自定义(例如更改标注气泡的形状、更改背景颜色等)。

  4. 前一点概述了一个相当复杂的场景(即,您必须编写自己的代码来检测视图外的点击以消除它)。如果您支持 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
    }
    
于 2013-07-21T12:44:51.977 回答
2

我最终采取了不同的方法。我尝试了其他的,但它们看起来很臃肿,我不想添加更多的类或依赖 MKMapViewDelegate 来处理交互。

我改为覆盖我的 MKAnnotationView 子类的 setSelected:animated。诀窍是在 annotationView 选择后扩展其边界以完全包含调用视图,然后在取消选择后将其恢复正常。这将允许您的自定义调用在 MKAnnotationView 的原始边界之外接受触摸和交互。

这是一个精简的代码示例,可以让任何人开始:

#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;
}
于 2013-12-27T04:32:26.060 回答
0
(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];
    }
}
于 2016-02-10T11:51:03.447 回答