如何在iPhone上绘制“讲话泡泡”?

时间:2010-12-14 17:26:43

标签: ios objective-c iphone uiview uibezierpath

当您右键单击Dock中的某些内容时,我正试图获得类似于Mac OS X中的“语音气泡”效果。这就是我现在所拥有的:

alt text

我需要得到下半部分的“三角形”部分。有什么方法可以画出类似的东西并围绕它画一个边框吗?这适用于 iPhone 应用。

提前致谢!

编辑:非常感谢Brad Larson,现在看来是这样的: alt text

12 个答案:

答案 0 :(得分:51)

我之前已经绘制了这个精确的形状(圆角矩形,底部有一个指向三角形)。我使用的Quartz绘图代码如下:

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);     

如果您不打算使用渐变或其他比简单颜色更复杂的填充,则可以省略末尾的剪切路径。

答案 1 :(得分:12)

也许一个更简单的问题是“是否有代码可以为我做这件事”,答案是“是”。

看哪MAAttachedWindow

alt text

当然,您可能不希望整个“附加窗口”行为,但至少绘图代码已经存在。 (Matt Gemmell的代码是高质量的东西)

答案 2 :(得分:12)

创建UIBezierPath的Swift 2代码:

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
}

现在你可以用这条路做任何你想做的事。例如,将它与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)

enter image description here

答案 3 :(得分:9)

我到这里寻找在现有视图中绘制“箭头”的解决方案 我很高兴与大家分享一些我希望有用的代码 - Swift 2.3兼容 -

public extension UIView {

  public enum PeakSide: Int {
        case Top
        case Left
        case Right
        case Bottom
    }

    public func addPikeOnView(side side: PeakSide, size: CGFloat = 10.0) {
        self.layoutIfNeeded()
        let peakLayer = CAShapeLayer()
        var path: CGPathRef?
        switch side {
        case .Top:
            path = self.makePeakPathWithRect(self.bounds, topSize: size, rightSize: 0.0, bottomSize: 0.0, leftSize: 0.0)
        case .Left:
            path = self.makePeakPathWithRect(self.bounds, topSize: 0.0, rightSize: 0.0, bottomSize: 0.0, leftSize: size)
        case .Right:
            path = self.makePeakPathWithRect(self.bounds, topSize: 0.0, rightSize: size, bottomSize: 0.0, leftSize: 0.0)
        case .Bottom:
            path = self.makePeakPathWithRect(self.bounds, topSize: 0.0, rightSize: 0.0, bottomSize: size, leftSize: 0.0)
        }
        peakLayer.path = path
        let color = (self.backgroundColor ?? .clearColor()).CGColor
        peakLayer.fillColor = color
        peakLayer.strokeColor = color
        peakLayer.lineWidth = 1
        peakLayer.position = CGPoint.zero
        self.layer.insertSublayer(peakLayer, atIndex: 0)
    }


    func makePeakPathWithRect(rect: CGRect, topSize ts: CGFloat, rightSize rs: CGFloat, bottomSize bs: CGFloat, leftSize ls: CGFloat) -> CGPathRef {
        //                      P3
        //                    /    \
        //      P1 -------- P2     P4 -------- P5
        //      |                               |
        //      |                               |
        //      P16                            P6
        //     /                                 \
        //  P15                                   P7
        //     \                                 /
        //      P14                            P8
        //      |                               |
        //      |                               |
        //      P13 ------ P12    P10 -------- P9
        //                    \   /
        //                     P11

        let centerX = rect.width / 2
        let centerY = rect.height / 2
        var h: CGFloat = 0
        let path = CGPathCreateMutable()
        var points: [CGPoint] = []
        // P1
        points.append(CGPointMake(rect.origin.x, rect.origin.y))
        // Points for top side
        if ts > 0 {
            h = ts * sqrt(3.0) / 2
            let x = rect.origin.x + centerX
            let y = rect.origin.y
            points.append(CGPointMake(x - ts, y))
            points.append(CGPointMake(x, y - h))
            points.append(CGPointMake(x + ts, y))
        }

        // P5
        points.append(CGPointMake(rect.origin.x + rect.width, rect.origin.y))
        // Points for right side
        if rs > 0 {
            h = rs * sqrt(3.0) / 2
            let x = rect.origin.x + rect.width
            let y = rect.origin.y + centerY
            points.append(CGPointMake(x, y - rs))
            points.append(CGPointMake(x + h, y))
            points.append(CGPointMake(x, y + rs))
        }

        // P9
        points.append(CGPointMake(rect.origin.x + rect.width, rect.origin.y + rect.height))
        // Point for bottom side
        if bs > 0 {
            h = bs * sqrt(3.0) / 2
            let x = rect.origin.x + centerX
            let y = rect.origin.y + rect.height
            points.append(CGPointMake(x + bs, y))
            points.append(CGPointMake(x, y + h))
            points.append(CGPointMake(x - bs, y))
        }

        // P13
        points.append(CGPointMake(rect.origin.x, rect.origin.y + rect.height))
        // Point for left side
        if ls > 0 {
            h = ls * sqrt(3.0) / 2
            let x = rect.origin.x
            let y = rect.origin.y + centerY
            points.append(CGPointMake(x, y + ls))
            points.append(CGPointMake(x - h, y))
            points.append(CGPointMake(x, y - ls))
        }

        let startPoint = points.removeFirst()
        self.startPath(path: path, onPoint: startPoint)
        for point in points {
            self.addPoint(point, toPath: path)
        }
        self.addPoint(startPoint, toPath: path)
        return path
    }

    private func startPath(path path: CGMutablePath, onPoint point: CGPoint) {
        CGPathMoveToPoint(path, nil, point.x, point.y)
    }

    private func addPoint(point: CGPoint, toPath path: CGMutablePath) {
        CGPathAddLineToPoint(path, nil, point.x, point.y)
    }

}

通过这种方式,你可以为每种视图调用它:

let view = UIView(frame: frame)
view.addPikeOnView(side: .Top)

将来我会增加派克位置的偏移量。

  • 是的,名字肯定是可以改进的!

SWIFT 3版本

public extension UIView {

    public enum PeakSide: Int {
        case Top
        case Left
        case Right
        case Bottom
    }

    public func addPikeOnView( side: PeakSide, size: CGFloat = 10.0) {
        self.layoutIfNeeded()
        let peakLayer = CAShapeLayer()
        var path: CGPath?
        switch side {
        case .Top:
            path = self.makePeakPathWithRect(rect: self.bounds, topSize: size, rightSize: 0.0, bottomSize: 0.0, leftSize: 0.0)
        case .Left:
            path = self.makePeakPathWithRect(rect: self.bounds, topSize: 0.0, rightSize: 0.0, bottomSize: 0.0, leftSize: size)
        case .Right:
            path = self.makePeakPathWithRect(rect: self.bounds, topSize: 0.0, rightSize: size, bottomSize: 0.0, leftSize: 0.0)
        case .Bottom:
            path = self.makePeakPathWithRect(rect: self.bounds, topSize: 0.0, rightSize: 0.0, bottomSize: size, leftSize: 0.0)
        }
        peakLayer.path = path
        let color = (self.backgroundColor?.cgColor)
        peakLayer.fillColor = color
        peakLayer.strokeColor = color
        peakLayer.lineWidth = 1
        peakLayer.position = CGPoint.zero
        self.layer.insertSublayer(peakLayer, at: 0)
    }


    func makePeakPathWithRect(rect: CGRect, topSize ts: CGFloat, rightSize rs: CGFloat, bottomSize bs: CGFloat, leftSize ls: CGFloat) -> CGPath {
        //                      P3
        //                    /    \
        //      P1 -------- P2     P4 -------- P5
        //      |                               |
        //      |                               |
        //      P16                            P6
        //     /                                 \
        //  P15                                   P7
        //     \                                 /
        //      P14                            P8
        //      |                               |
        //      |                               |
        //      P13 ------ P12    P10 -------- P9
        //                    \   /
        //                     P11

        let centerX = rect.width / 2
        let centerY = rect.height / 2
        var h: CGFloat = 0
        let path = CGMutablePath()
        var points: [CGPoint] = []
        // P1
        points.append(CGPoint(x:rect.origin.x,y: rect.origin.y))
        // Points for top side
        if ts > 0 {
            h = ts * sqrt(3.0) / 2
            let x = rect.origin.x + centerX
            let y = rect.origin.y
            points.append(CGPoint(x:x - ts,y: y))
            points.append(CGPoint(x:x,y: y - h))
            points.append(CGPoint(x:x + ts,y: y))
       }

        // P5
        points.append(CGPoint(x:rect.origin.x + rect.width,y: rect.origin.y))
        // Points for right side
        if rs > 0 {
            h = rs * sqrt(3.0) / 2
            let x = rect.origin.x + rect.width
           let y = rect.origin.y + centerY
           points.append(CGPoint(x:x,y: y - rs))
           points.append(CGPoint(x:x + h,y: y))
           points.append(CGPoint(x:x,y: y + rs))
        }

        // P9
        points.append(CGPoint(x:rect.origin.x + rect.width,y: rect.origin.y + rect.height))
        // Point for bottom side
        if bs > 0 {
            h = bs * sqrt(3.0) / 2
            let x = rect.origin.x + centerX
            let y = rect.origin.y + rect.height
            points.append(CGPoint(x:x + bs,y: y))
            points.append(CGPoint(x:x,y: y + h))
            points.append(CGPoint(x:x - bs,y: y))
        }

        // P13
        points.append(CGPoint(x:rect.origin.x, y: rect.origin.y + rect.height))
        // Point for left sidey:
        if ls > 0 {
            h = ls * sqrt(3.0) / 2
            let x = rect.origin.x
            let y = rect.origin.y + centerY
            points.append(CGPoint(x:x,y: y + ls))
            points.append(CGPoint(x:x - h,y: y))
            points.append(CGPoint(x:x,y: y - ls))
        }

        let startPoint = points.removeFirst()
        self.startPath(path: path, onPoint: startPoint)
        for point in points {
            self.addPoint(point: point, toPath: path)
        }
        self.addPoint(point: startPoint, toPath: path)
        return path
    }

    private func startPath( path: CGMutablePath, onPoint point: CGPoint) {
        path.move(to: CGPoint(x: point.x, y: point.y))
    }

    private func addPoint(point: CGPoint, toPath path: CGMutablePath) {
       path.addLine(to: CGPoint(x: point.x, y: point.y))
    }
}

答案 4 :(得分:7)

有两种方法可以实现这一目标:

  1. 在正确的位置添加带有三角形图像的UIImageView。确保图像的其余部分是透明的,以免阻挡背景。
  2. 覆盖UIView上的drawRect:方法以自定义绘制视图。然后,您可以为三角形添加线性路径组件,根据需要填充和边界路径。
  3. 要使用drawRect:绘制一个简单的三角形,您可能会这样做。此代码段会在视图底部绘制一个指向下方的三角形。

    // 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);
    

    有关详细信息,请参阅CGContext reference

答案 5 :(得分:3)

对于那些使用swift 2.0的人,基于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)
}

答案 6 :(得分:1)

请参阅下图中弹出菜单上的三角形,这些三角形是使用Core Graphics funcs绘制的,并且是完全可扩展的。

alt text

做这样做等边三角形(老派功能名称,对不起):

#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);

}

注意,triH(x)func是用于计算等边三角形高度的优化公式,例如h = 1/2 * sqrt(3)* x。由于1/2 * sqrt(3)永远不会改变,我将其优化为定义。

答案 7 :(得分:1)

Swift 4更新

这是AVT's original code的Swift 4版本。

 private func bubblePathForContentSize(contentSize: CGSize) -> UIBezierPath {
    let rect = CGRect(origin: .zero, size: CGSize(width: contentSize.width, height: contentSize.height)).offsetBy(dx: radius, dy: radius + triangleHeight)
    let path = UIBezierPath();
    let radius2 = radius - borderWidth / 2 // Radius adjasted for the border width

    path.move(to: CGPoint(x: rect.maxX - triangleHeight * 2, y: rect.minY - radius2))
    path.addLine(to: CGPoint(x: rect.maxX - triangleHeight, y: rect.minY - radius2 - triangleHeight))
    path.addArc(withCenter: CGPoint(x: rect.maxX, y: rect.minY),
                radius: radius2,
                startAngle: CGFloat(-(Double.pi/2)), endAngle: 0, clockwise: true)
    path.addArc(withCenter: CGPoint(x: rect.maxX, y: rect.maxY),
                radius: radius2,
                startAngle: 0, endAngle: CGFloat(Double.pi/2), clockwise: true)
    path.addArc(withCenter: CGPoint(x: rect.minX, y: rect.maxY),
                radius: radius2,
                startAngle: CGFloat(Double.pi/2),endAngle: CGFloat(Double.pi), clockwise: true)
    path.addArc(withCenter: CGPoint(x: rect.minX, y: rect.minY),
                radius: radius2,
                startAngle: CGFloat(Double.pi), endAngle: CGFloat(-(Double.pi/2)), clockwise: true)
    path.close()
    return path
}

//Example usage:
 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)

答案 8 :(得分:1)

这是一个 swift 5 @IBDesignable UIView 版本

@IBDesignable
class SpeechBubble: UIView {

@IBInspectable var lineWidth: CGFloat = 4 { didSet { setNeedsDisplay() } }
@IBInspectable var cornerRadius: CGFloat = 8 { didSet { setNeedsDisplay() } }

@IBInspectable var strokeColor: UIColor = .red { didSet { setNeedsDisplay() } }
@IBInspectable var fillColor: UIColor = .gray { didSet { setNeedsDisplay() } }

@IBInspectable var peakWidth: CGFloat  = 10 { didSet { setNeedsDisplay() } }
@IBInspectable var peakHeight: CGFloat = 10 { didSet { setNeedsDisplay() } }
@IBInspectable var peakOffset: CGFloat = 0 { didSet { setNeedsDisplay() } }

override func draw(_ rectangle: CGRect) {
    
    //Add a bounding area so we can fit the peak in the view
    let rect = bounds.insetBy(dx: peakHeight, dy: peakHeight)
    
    let centerX = rect.width / 2
    //let centerY = rect.height / 2
    var h: CGFloat = 0
    
    //create the path
    let path = UIBezierPath()
    path.lineWidth = lineWidth
    
    // Start of bubble (Top Left)
    path.move(to: CGPoint(x: rect.minX, y: rect.minY + cornerRadius))
    path.addQuadCurve(to: CGPoint(x: rect.minX + cornerRadius, y: rect.minY),
                      controlPoint: CGPoint(x: rect.minX, y: rect.minY))
    
    //Add the peak
    h = peakHeight * sqrt(3.0) / 2

    let x = rect.origin.x + centerX
    let y = rect.origin.y
    path.addLine(to: CGPoint(x: (x + peakOffset) - peakWidth, y: y))
    path.addLine(to: CGPoint(x: (x + peakOffset), y: y - h))
    path.addLine(to: CGPoint(x: (x + peakOffset) + peakWidth, y: y))
    
    // Top Right
    path.addLine(to: CGPoint(x: rect.maxX - cornerRadius, y: rect.minY))
    path.addQuadCurve(to: CGPoint(x: rect.maxX, y: rect.minY + cornerRadius),
                      controlPoint: CGPoint(x: rect.maxX, y: rect.minY))
    
    // Bottom Right
    path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - cornerRadius))
    path.addQuadCurve(to: CGPoint(x: rect.maxX - cornerRadius, y: rect.maxY),
                      controlPoint: CGPoint(x: rect.maxX, y: rect.maxY))
    //Bottom Left
    path.addLine(to: CGPoint(x: rect.minX + cornerRadius, y: rect.maxY))
    path.addQuadCurve(to: CGPoint(x: rect.minX, y: rect.maxY - cornerRadius), controlPoint: CGPoint(x: rect.minX, y: rect.maxY))
    // Back to start
    path.addLine(to: CGPoint(x: rect.origin.x, y: rect.minY + cornerRadius))
    
    //set and draw stroke color
    strokeColor.setStroke()
    path.stroke()
    
    //set and draw fill color
    fillColor.setFill()
    path.fill()
    
  }
}

答案 9 :(得分:0)

如果有人来寻找Swift 3的答案,这就行了!感谢那些在我做过之前做出贡献的人,可爱的一段代码!

    let rRect = CGRect(x: start.x, y: start.y, width: defaultHeightWidth.0, height: defaultHeightWidth.1)


    context?.translateBy(x: 0, y: rRect.size.height - 3)
    context?.scaleBy(x: 1.0, y: -1.0)


    context?.setLineJoin(.bevel)
    context?.setLineWidth(strokeWidth)
    context?.setStrokeColor(UIColor.black.cgColor)
    context?.setFillColor(UIColor.white.cgColor)

    // draw and fill the bubble
    context?.beginPath()
    context?.move(to: CGPoint(x: borderRadius + strokeWidth + 0.5, y: strokeWidth + triangleHeight + 0.5))
    context?.addLine(to: CGPoint(x: round(rRect.size.width / 2.0 - triangleWidth / 2.0) + 0.5, y: triangleHeight + strokeWidth + 0.5))
    context?.addLine(to: CGPoint(x: round(rRect.size.width / 2.0) + 0.5, y: strokeWidth + 0.5))
    context?.addLine(to: CGPoint(x: round(rRect.size.width / 2.0 + triangleWidth / 2.0), y: triangleHeight + strokeWidth + 0.5))
    context?.addArc(tangent1End: CGPoint(x: rRect.size.width - strokeWidth - 0.5, y: strokeWidth + triangleHeight + 0.5), tangent2End: CGPoint(x: rRect.size.width - strokeWidth - 0.5, y: rRect.size.height - strokeWidth - 0.5), radius: borderRadius - strokeWidth)
    context?.addArc(tangent1End: CGPoint(x: rRect.size.width - strokeWidth - 0.5, y: rRect.size.height - strokeWidth - 0.5), tangent2End: CGPoint(x: round(rRect.size.width / 2.0 + triangleWidth / 2.0) - strokeWidth + 0.5, y: rRect.size.height - strokeWidth - 0.5), radius: borderRadius - strokeWidth)
    context?.addArc(tangent1End: CGPoint(x: strokeWidth + 0.5, y: rRect.size.height - strokeWidth - 0.5), tangent2End: CGPoint(x: strokeWidth + 0.5, y: triangleHeight + strokeWidth + 0.5), radius: borderRadius - strokeWidth)
    context?.addArc(tangent1End: CGPoint(x: strokeWidth + 0.5, y: strokeWidth + triangleHeight + 0.5), tangent2End: CGPoint(x: rRect.size.width - strokeWidth - 0.5, y: triangleHeight + strokeWidth + 0.5), radius: borderRadius - strokeWidth)
    context?.closePath()
    context?.drawPath(using: .fillStroke)

在我的案例triangleWidth = 10triangleHeight = 5中,视图比OPs版本的视图小得多。

答案 10 :(得分:0)

以下是Brad Larson

的快速3解决方案
override func draw(_ rect: CGRect) {
        super.draw(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: CGContext = UIGraphicsGetCurrentContext()!
        context.translateBy(x: 0.0, y: self.bounds.size.height)
        context.scaleBy(x: 1.0, y: -1.0)
        //
        let currentFrame: CGRect = self.bounds
        context.setLineJoin(CGLineJoin.round)
        context.setLineWidth(strokeWidth)
        context.setStrokeColor(UIColor.white.cgColor)
        context.setFillColor(UIColor.black.cgColor)
        // Draw and fill the bubble
        context.beginPath()

        context.move(to: CGPoint(x: borderRadius + strokeWidth + 0.5, y: strokeWidth + HEIGHTOFPOPUPTRIANGLE + 0.5))

            context.addLine(to: CGPoint(x: round(currentFrame.size.width / 2.0 - WIDTHOFPOPUPTRIANGLE / 2.0) + 0.5, y: HEIGHTOFPOPUPTRIANGLE + strokeWidth + 0.5))
        context.addLine(to: CGPoint(x: round(currentFrame.size.width / 2.0) + 0.5, y: strokeWidth + 0.5))
        context.addLine(to: CGPoint(x: round(currentFrame.size.width / 2.0 + WIDTHOFPOPUPTRIANGLE / 2.0) + 0.5, y: HEIGHTOFPOPUPTRIANGLE + strokeWidth + 0.5))

        context.addArc(tangent1End: CGPoint(x: currentFrame.size.width - strokeWidth - 0.5, y: strokeWidth + HEIGHTOFPOPUPTRIANGLE + 0.5), tangent2End: CGPoint(x: currentFrame.size.width - strokeWidth - 0.5, y: currentFrame.size.height - strokeWidth - 0.5), radius: borderRadius - strokeWidth)

        context.addArc(tangent1End: CGPoint(x: currentFrame.size.width - strokeWidth - 0.5, y: currentFrame.size.height - strokeWidth - 0.5) , tangent2End: CGPoint(x: round(currentFrame.size.width / 2.0 + WIDTHOFPOPUPTRIANGLE / 2.0) - strokeWidth + 0.5, y: currentFrame.size.height - strokeWidth - 0.5) , radius: borderRadius - strokeWidth)

        context.addArc(tangent1End: CGPoint(x: strokeWidth + 0.5, y: currentFrame.size.height - strokeWidth - 0.5), tangent2End: CGPoint(x: strokeWidth + 0.5, y: HEIGHTOFPOPUPTRIANGLE + strokeWidth + 0.5), radius: borderRadius - strokeWidth)

        context.addArc(tangent1End: CGPoint(x: strokeWidth + 0.5, y :strokeWidth + HEIGHTOFPOPUPTRIANGLE + 0.5), tangent2End: CGPoint(x: currentFrame.size.width - strokeWidth - 0.5 ,y: HEIGHTOFPOPUPTRIANGLE + strokeWidth + 0.5), radius: borderRadius - strokeWidth)

        context.closePath()
        context.drawPath(using: CGPathDrawingMode.fillStroke)

        // Draw a clipping path for the fill
        context.beginPath()

        context.move(to: CGPoint(x: borderRadius + strokeWidth + 0.5, y: round((currentFrame.size.height + HEIGHTOFPOPUPTRIANGLE) * 0.50) + 0.5))
        context.addArc(tangent1End: CGPoint(x: currentFrame.size.width - strokeWidth - 0.5, y: round((currentFrame.size.height + HEIGHTOFPOPUPTRIANGLE) * 0.50) + 0.5), tangent2End: CGPoint(x: currentFrame.size.width - strokeWidth - 0.5, y: currentFrame.size.height - strokeWidth - 0.5), radius: borderRadius - strokeWidth)

        context.addArc(tangent1End: CGPoint(x: currentFrame.size.width - strokeWidth - 0.5, y: currentFrame.size.height - strokeWidth - 0.5) , tangent2End: CGPoint(x: round(currentFrame.size.width / 2.0 + WIDTHOFPOPUPTRIANGLE / 2.0) - strokeWidth + 0.5, y: currentFrame.size.height - strokeWidth - 0.5), radius: borderRadius - strokeWidth)
        context.addArc(tangent1End: CGPoint(x: strokeWidth + 0.5, y: currentFrame.size.height - strokeWidth - 0.5), tangent2End: CGPoint(x: strokeWidth + 0.5, y: HEIGHTOFPOPUPTRIANGLE + strokeWidth + 0.5), radius: borderRadius - strokeWidth)
        context.addArc(tangent1End: CGPoint(x: strokeWidth + 0.5, y: round((currentFrame.size.height + HEIGHTOFPOPUPTRIANGLE) * 0.50) + 0.5), tangent2End: CGPoint(x: currentFrame.size.width - strokeWidth - 0.5, y: round((currentFrame.size.height + HEIGHTOFPOPUPTRIANGLE) * 0.50) + 0.5), radius: borderRadius - strokeWidth)

        context.closePath()
        context.clip()
    }

答案 11 :(得分:-2)

我可能会在Photoshop中制作整个图像(包括三角形),然后使用以下方法在适当的时间在屏幕上显示:

CGRect myRect = CGRectMake(10.0f, 0.0f, 300.0f, 420.0f);
UIImageView *myImage = [[UIImageView alloc] initWithFrame:myRect];
[myImage setImage:[UIImage imageNamed:@"ThisIsMyImageName.png"]];
myImage.opaque = YES;
[self.view addSubview:myImage];
[myImage release];