How to draw a “speech bubble” on an iPhone?

后端 未结 11 1132
礼貌的吻别
礼貌的吻别 2020-11-29 16:34

I\'m trying to get a \"speech bubble\" effect similar to the one in Mac OS X when you right click on something in the dock. Here\'s what I have now:

相关标签:
11条回答
  • 2020-11-29 17:03

    Swift 2 code that creates UIBezierPath:

    var borderWidth : CGFloat = 4 // Should be less or equal to the `radius` property
    var radius : CGFloat = 10
    var triangleHeight : CGFloat = 15
    
    private func bubblePathForContentSize(contentSize: CGSize) -> UIBezierPath {
        let rect = CGRectMake(0, 0, contentSize.width, contentSize.height).offsetBy(dx: radius, dy: radius + triangleHeight)
        let path = UIBezierPath();
        let radius2 = radius - borderWidth / 2 // Radius adjasted for the border width
    
        path.moveToPoint(CGPointMake(rect.maxX - triangleHeight * 2, rect.minY - radius2))
        path.addLineToPoint(CGPointMake(rect.maxX - triangleHeight, rect.minY - radius2 - triangleHeight))
        path.addArcWithCenter(CGPointMake(rect.maxX, rect.minY), radius: radius2, startAngle: CGFloat(-M_PI_2), endAngle: 0, clockwise: true)
        path.addArcWithCenter(CGPointMake(rect.maxX, rect.maxY), radius: radius2, startAngle: 0, endAngle: CGFloat(M_PI_2), clockwise: true)
        path.addArcWithCenter(CGPointMake(rect.minX, rect.maxY), radius: radius2, startAngle: CGFloat(M_PI_2), endAngle: CGFloat(M_PI), clockwise: true)
        path.addArcWithCenter(CGPointMake(rect.minX, rect.minY), radius: radius2, startAngle: CGFloat(M_PI), endAngle: CGFloat(-M_PI_2), clockwise: true)
        path.closePath()
        return path
    }
    

    Now you could do whatever you want with this path. For example use it with CAShapeLayer:

    let bubbleLayer = CAShapeLayer()
    bubbleLayer.path = bubblePathForContentSize(contentView.bounds.size).CGPath
    bubbleLayer.fillColor = fillColor.CGColor
    bubbleLayer.strokeColor = borderColor.CGColor
    bubbleLayer.lineWidth = borderWidth
    bubbleLayer.position = CGPoint.zero
    myView.layer.addSublayer(bubbleLayer)
    

    0 讨论(0)
  • 2020-11-29 17:04

    See the triangle on the pop up menu in the image below, thats drawn with Core Graphics funcs and is completely scalable.

    alt text

    Done like this to do an equilateral triangle (old-school function names, sorry):

    #define triH(v) (v * 0.866)    
    
    func(CGContextRef inContext, CGRect arrowRect, CustomPushButtonData* controlData) {
    // Draw the triangle
    float   arrowXstart, arrowYstart;
    float   arrowXpos, arrowYpos, arrowHpos; 
    
    if (controlData->controlEnabled && controlData->controlActive) {
    
        CGContextSetRGBFillColor(inContext, 0., 0., 0., 1.);
    
    } else {
    
        CGContextSetRGBFillColor(inContext, 0., 0., 0., 0.5);
    
    }
    
    arrowHpos = triH(arrowRect.size.height);
    
    // Point C
    
    CGContextBeginPath(inContext);
    
    arrowXstart = arrowXpos = (arrowRect.origin.x + ((float)(arrowRect.size.width / 2.) - (arrowSize / 2.)));
    
    arrowYstart = arrowYpos = (arrowRect.origin.y + (float)((arrowRect.size.height / 2.) - (float)(arrowHpos / 2.)));
    
    CGContextMoveToPoint(inContext, arrowXpos, arrowYpos);
    
    // Point A
    
    arrowXpos += arrowSize;
    
    CGContextAddLineToPoint(inContext, arrowXpos, arrowYpos);
    
    // Point B
    
    arrowYpos += arrowHpos;
    
    arrowXpos -= (float)(arrowSize / 2.0);
    
    CGContextAddLineToPoint(inContext, arrowXpos, arrowYpos);
    
    // Point C
    CGContextAddLineToPoint(inContext, arrowXstart, arrowYstart);
    
    CGContextClosePath(inContext);
    
    CGContextFillPath(inContext);
    

    }

    Note that the triH(x) func is an optimized formula for calculating the height of an equitlateral triangle e.g. h = 1/2 * sqrt(3) * x . Since 1/2 * sqrt(3) never changes, I optimized it into that define.

    0 讨论(0)
  • 2020-11-29 17:08

    I've actually drawn this exact shape before (rounded rectangle with a pointing triangle at the bottom). The Quartz drawing code that I used is as follows:

    CGRect currentFrame = self.bounds;
    
    CGContextSetLineJoin(context, kCGLineJoinRound);
    CGContextSetLineWidth(context, strokeWidth);
    CGContextSetStrokeColorWithColor(context, [MyPopupLayer popupBorderColor]); 
    CGContextSetFillColorWithColor(context, [MyPopupLayer popupBackgroundColor]);
    
    // Draw and fill the bubble
    CGContextBeginPath(context);
    CGContextMoveToPoint(context, borderRadius + strokeWidth + 0.5f, strokeWidth + HEIGHTOFPOPUPTRIANGLE + 0.5f);
    CGContextAddLineToPoint(context, round(currentFrame.size.width / 2.0f - WIDTHOFPOPUPTRIANGLE / 2.0f) + 0.5f, HEIGHTOFPOPUPTRIANGLE + strokeWidth + 0.5f);
    CGContextAddLineToPoint(context, round(currentFrame.size.width / 2.0f) + 0.5f, strokeWidth + 0.5f);
    CGContextAddLineToPoint(context, round(currentFrame.size.width / 2.0f + WIDTHOFPOPUPTRIANGLE / 2.0f) + 0.5f, HEIGHTOFPOPUPTRIANGLE + strokeWidth + 0.5f);
    CGContextAddArcToPoint(context, currentFrame.size.width - strokeWidth - 0.5f, strokeWidth + HEIGHTOFPOPUPTRIANGLE + 0.5f, currentFrame.size.width - strokeWidth - 0.5f, currentFrame.size.height - strokeWidth - 0.5f, borderRadius - strokeWidth);
    CGContextAddArcToPoint(context, currentFrame.size.width - strokeWidth - 0.5f, currentFrame.size.height - strokeWidth - 0.5f, round(currentFrame.size.width / 2.0f + WIDTHOFPOPUPTRIANGLE / 2.0f) - strokeWidth + 0.5f, currentFrame.size.height - strokeWidth - 0.5f, borderRadius - strokeWidth);
    CGContextAddArcToPoint(context, strokeWidth + 0.5f, currentFrame.size.height - strokeWidth - 0.5f, strokeWidth + 0.5f, HEIGHTOFPOPUPTRIANGLE + strokeWidth + 0.5f, borderRadius - strokeWidth);
    CGContextAddArcToPoint(context, strokeWidth + 0.5f, strokeWidth + HEIGHTOFPOPUPTRIANGLE + 0.5f, currentFrame.size.width - strokeWidth - 0.5f, HEIGHTOFPOPUPTRIANGLE + strokeWidth + 0.5f, borderRadius - strokeWidth);
    CGContextClosePath(context);
    CGContextDrawPath(context, kCGPathFillStroke);
    
    // Draw a clipping path for the fill
    CGContextBeginPath(context);
    CGContextMoveToPoint(context, borderRadius + strokeWidth + 0.5f, round((currentFrame.size.height + HEIGHTOFPOPUPTRIANGLE) * 0.50f) + 0.5f);
    CGContextAddArcToPoint(context, currentFrame.size.width - strokeWidth - 0.5f, round((currentFrame.size.height + HEIGHTOFPOPUPTRIANGLE) * 0.50f) + 0.5f, currentFrame.size.width - strokeWidth - 0.5f, currentFrame.size.height - strokeWidth - 0.5f, borderRadius - strokeWidth);
    CGContextAddArcToPoint(context, currentFrame.size.width - strokeWidth - 0.5f, currentFrame.size.height - strokeWidth - 0.5f, round(currentFrame.size.width / 2.0f + WIDTHOFPOPUPTRIANGLE / 2.0f) - strokeWidth + 0.5f, currentFrame.size.height - strokeWidth - 0.5f, borderRadius - strokeWidth);
    CGContextAddArcToPoint(context, strokeWidth + 0.5f, currentFrame.size.height - strokeWidth - 0.5f, strokeWidth + 0.5f, HEIGHTOFPOPUPTRIANGLE + strokeWidth + 0.5f, borderRadius - strokeWidth);
    CGContextAddArcToPoint(context, strokeWidth + 0.5f, round((currentFrame.size.height + HEIGHTOFPOPUPTRIANGLE) * 0.50f) + 0.5f, currentFrame.size.width - strokeWidth - 0.5f, round((currentFrame.size.height + HEIGHTOFPOPUPTRIANGLE) * 0.50f) + 0.5f, borderRadius - strokeWidth);
    CGContextClosePath(context);
    CGContextClip(context);     
    

    The clipping path at the end can be left out if you're not going to use a gradient or some other more fill that's more complex than a simple color.

    0 讨论(0)
  • 2020-11-29 17:09

    For those using swift 2.0 based on the answer by Brad Larson

    override func drawRect(rect: CGRect) {
        super.drawRect(rect) // optional if a direct UIView-subclass, should be called otherwise.
    
        let HEIGHTOFPOPUPTRIANGLE:CGFloat = 20.0
        let WIDTHOFPOPUPTRIANGLE:CGFloat = 40.0
        let borderRadius:CGFloat = 8.0
        let strokeWidth:CGFloat = 3.0
    
        // Get the context
        let context: CGContextRef = UIGraphicsGetCurrentContext()!
        CGContextTranslateCTM(context, 0.0, self.bounds.size.height)
        CGContextScaleCTM(context, 1.0, -1.0)
        //
        let currentFrame: CGRect = self.bounds
        CGContextSetLineJoin(context, CGLineJoin.Round)
        CGContextSetLineWidth(context, strokeWidth)
        CGContextSetStrokeColorWithColor(context, UIColor.whiteColor().CGColor)
        CGContextSetFillColorWithColor(context, UIColor.blackColor().CGColor)
        // Draw and fill the bubble
        CGContextBeginPath(context)
        CGContextMoveToPoint(context, borderRadius + strokeWidth + 0.5, strokeWidth + HEIGHTOFPOPUPTRIANGLE + 0.5)
        CGContextAddLineToPoint(context, round(currentFrame.size.width / 2.0 - WIDTHOFPOPUPTRIANGLE / 2.0) + 0.5, HEIGHTOFPOPUPTRIANGLE + strokeWidth + 0.5)
        CGContextAddLineToPoint(context, round(currentFrame.size.width / 2.0) + 0.5, strokeWidth + 0.5)
        CGContextAddLineToPoint(context, round(currentFrame.size.width / 2.0 + WIDTHOFPOPUPTRIANGLE / 2.0) + 0.5, HEIGHTOFPOPUPTRIANGLE + strokeWidth + 0.5)
        CGContextAddArcToPoint(context, currentFrame.size.width - strokeWidth - 0.5, strokeWidth + HEIGHTOFPOPUPTRIANGLE + 0.5, currentFrame.size.width - strokeWidth - 0.5, currentFrame.size.height - strokeWidth - 0.5, borderRadius - strokeWidth)
        CGContextAddArcToPoint(context, currentFrame.size.width - strokeWidth - 0.5, currentFrame.size.height - strokeWidth - 0.5, round(currentFrame.size.width / 2.0 + WIDTHOFPOPUPTRIANGLE / 2.0) - strokeWidth + 0.5, currentFrame.size.height - strokeWidth - 0.5, borderRadius - strokeWidth)
        CGContextAddArcToPoint(context, strokeWidth + 0.5, currentFrame.size.height - strokeWidth - 0.5, strokeWidth + 0.5, HEIGHTOFPOPUPTRIANGLE + strokeWidth + 0.5, borderRadius - strokeWidth)
        CGContextAddArcToPoint(context, strokeWidth + 0.5, strokeWidth + HEIGHTOFPOPUPTRIANGLE + 0.5, currentFrame.size.width - strokeWidth - 0.5, HEIGHTOFPOPUPTRIANGLE + strokeWidth + 0.5, borderRadius - strokeWidth)
        CGContextClosePath(context)
        CGContextDrawPath(context, CGPathDrawingMode.FillStroke)
    
        // Draw a clipping path for the fill
        CGContextBeginPath(context)
        CGContextMoveToPoint(context, borderRadius + strokeWidth + 0.5, round((currentFrame.size.height + HEIGHTOFPOPUPTRIANGLE) * 0.50) + 0.5)
        CGContextAddArcToPoint(context, currentFrame.size.width - strokeWidth - 0.5, round((currentFrame.size.height + HEIGHTOFPOPUPTRIANGLE) * 0.50) + 0.5, currentFrame.size.width - strokeWidth - 0.5, currentFrame.size.height - strokeWidth - 0.5, borderRadius - strokeWidth)
        CGContextAddArcToPoint(context, currentFrame.size.width - strokeWidth - 0.5, currentFrame.size.height - strokeWidth - 0.5, round(currentFrame.size.width / 2.0 + WIDTHOFPOPUPTRIANGLE / 2.0) - strokeWidth + 0.5, currentFrame.size.height - strokeWidth - 0.5, borderRadius - strokeWidth)
        CGContextAddArcToPoint(context, strokeWidth + 0.5, currentFrame.size.height - strokeWidth - 0.5, strokeWidth + 0.5, HEIGHTOFPOPUPTRIANGLE + strokeWidth + 0.5, borderRadius - strokeWidth)
        CGContextAddArcToPoint(context, strokeWidth + 0.5, round((currentFrame.size.height + HEIGHTOFPOPUPTRIANGLE) * 0.50) + 0.5, currentFrame.size.width - strokeWidth - 0.5, round((currentFrame.size.height + HEIGHTOFPOPUPTRIANGLE) * 0.50) + 0.5, borderRadius - strokeWidth)
        CGContextClosePath(context)
        CGContextClip(context)
    }
    
    0 讨论(0)
  • 2020-11-29 17:13

    There are two ways you might be able to accomplish this:

    1. Add a UIImageView with a triangle image in the right place. Make sure the rest of the image is transparent so as not to block your background.
    2. Override the drawRect: method on your UIView to custom-draw the view. You can then add linear path components for your triangle, filling and bordering the path as necessary.

    To draw a simple triangle using drawRect:, you might do something like this. This snippet will draw a triangle pointing downwards at the bottom of your view.

    // Get the context
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    // Pick colors
    CGContextSetStrokeColorWithColor(context, [[UIColor blackColor] CGColor]);
    CGContextSetFillColorWithColor(context, [[UIColor redColor] CGColor]);
    
    // Define triangle dimensions
    CGFloat baseWidth = 30.0;
    CGFloat height = 20.0;
    
    // Define path
    CGContextMoveToPoint(context, self.bounds.size.width / 2.0 - baseWidth / 2.0, 
                                  self.bounds.size.height - height);
    CGContextAddLineToPoint(context, self.bounds.size.width / 2.0 + baseWidth / 2.0, 
                                     self.bounds.size.height - height);
    CGContextAddLineToPoint(context, self.bounds.size.width / 2.0, 
                                     self.bounds.size.height);
    
    // Finalize and draw using path
    CGContextClosePath(context);
    CGContextStrokePath(context);
    

    For more info, see the CGContext reference.

    0 讨论(0)
  • 2020-11-29 17:14

    Perhaps a simpler question is "Is there code that does this for me already", to which the answer is "Yes".

    Behold MAAttachedWindow:

    alt text

    Granted, you may not want the whole "Attached window" behavior, but at least the drawing code is already there. (And Matt Gemmell's code is high quality stuff)

    0 讨论(0)
提交回复
热议问题