【问题标题】:How to draw a "speech bubble" on an iPhone?如何在 iPhone 上画出“讲话泡泡”?
【发布时间】:2026-02-14 11:40:01
【问题描述】:

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

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

提前致谢!

编辑:非常感谢 Brad Larson,这就是现在的样子:

【问题讨论】:

    标签: ios objective-c iphone uiview uibezierpath


    【解决方案1】:

    我之前实际上已经画过这个精确的形状(底部有一个指向三角形的圆角矩形)。我使用的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);     
    

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

    【讨论】:

    • 看起来很棒!我确定我在这里遗漏了很多关键信息,因为我尝试为HEIGHTOFPOPUPTRIANGLEWIDTHOFPOPUPTRIANGLEborderRadiusstrokeWidth 定义一些常量,但它给了我大约 25 个错误。有人说“参数太少,无法运行CGContextMoveToPoint”有什么想法吗?这对你有用吗?
    • @sudo - 在我的实现中,strokeWidthborderRadius 是实例变量,HEIGHTOFPOPUPTRIANGLEHEIGHTOFPOPUPTRIANGLE 是定义的常量。上下文当然需要从 NSView、UIView 或 CALayer 的覆盖绘制方法中使用的内容中绘制。 “参数太少”错误听起来像是在您的编译器常量定义中可能有一个杂散的分号,或者类似的东西。
    • 没关系,我只是用theView.transform = CGAffineTransformMakeRotation(3.14);
    • 对于所有将来使用 Brad 代码的人,为了实现我上面帖子中的外观,我制作了HEIGHTOFPOPUPTRIANGLE = 20WIDTHOFPOPUPTRIANGLE = 40、borderRadius = 8strokeWidth = 3。我将视图 alpha 设置为 0.75
    • @sudo - 是的,倒置坐标空间的原因是我重新调整了图层的方向以匹配 Mac(和 Quartz)的默认坐标空间。你也可以在上面的绘图代码之前使用CGContextTranslateCTM(context, 0.0f, self.bounds.size.height); CGContextScaleCTM(context, 1.0f, -1.0f);实现这种不旋转的翻转。
    【解决方案2】:

    创建 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)
    

    【讨论】:

      【解决方案3】:

      也许一个更简单的问题是“是否有代码已经为我做到了这一点”,答案是“是”。

      MAAttachedWindow:

      当然,您可能不想要整个“附加窗口”行为,但至少绘图代码已经存在。 (而且 Matt Gemmell 的代码质量很高)

      【讨论】:

      • 虽然 Matt 的 MAAttachedWindow 对于 Mac 来说是一个很棒的组件,但我相信他正在寻找也适用于 iPhone 的东西(基于标签)。
      • 是的,这看起来很棒,但是是的......我正在寻找一种 iPhone 的方式来做到这一点。我会更新我的标题...
      • @sudo @Brad 确实适用于 Mac,但他使用的代码很容易适用于 iPhone;只需将NSMakePointCGPointMakeNSBezierPathUIBezierPath 交换,你就完成了。
      【解决方案4】:

      我来这里是为了寻找在现有视图中绘制“箭头”的解决方案。
      我很高兴与您分享一些我希望有用的代码 - 兼容 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))
          }
      }
      

      【讨论】:

      • 谢谢!有没有办法添加一些方法来让 UIView 上有这个峰值和 clipToBounds?
      • 嗨,我需要下图中的 P15 侧箭头。请帮我解决这个问题
      • @Luca Davanzo 它有效,但有一个问题,你如何绕过角落?如果我设置cornerRadius = 7,然后masksToBounds = true,我会失去派克。但是,如果我不设置 maskToBounds,我会保留长矛,但会丢失圆角。
      【解决方案5】:

      您可能有两种方法可以做到这一点:

      1. 在正确的位置添加带有三角形图像的UIImageView。确保图片的其余部分是透明的,以免挡住您的背景。
      2. 覆盖 UIView 上的 drawRect: 方法以自定义绘制视图。然后,您可以为三角形添加线性路径组件,并根据需要填充路径并设置边界。

      要使用drawRect: 绘制一个简单的三角形,您可以这样做。这个 sn-p 将在您的视图底部绘制一个向下的三角形。

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

      【讨论】:

      • 尝试将最后一行更改为CGContextDrawPath(context, kCGPathFillStroke); 以填充和描边。
      • 我在 Xcode 中运行了这个,看到在我的视图中绘制了一个黑色三角形,所以我不确定你出了什么问题 - 你能描述一下你正在使用什么样的视图层次结构和子类吗?
      • @Tim 好的,我让它显示。但是,我无法将其填满。我尝试使用CGContextFillPath(context)CGContextDrawPath(context, kCGPathFillStroke),但我仍然得到这个:i.imgur.com/y99nt.png 我正在做的是创建一个使用drawRect:(CGRect)rect 绘制的新视图,以绘制一个位于我正在绘制的另一个矩形下方的三角形那个观点。
      • 你在打电话给CGContextFillPath()之前确定你的填充颜色了吗?
      • 我按照你说的做了:` CGContextSetFillColorWithColor(context, [[UIColor redColor] CGColor]);` 我想我应该为这个小三角形创建一个全新的视图看看我能不能让它在那里工作。
      【解决方案6】:

      对于那些根据Brad Larson 的回答使用 swift 2.0 的人

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

      【讨论】:

        【解决方案7】:

        见下图中弹出菜单上的三角形,它是用 Core Graphics funcs 绘制的,并且是完全可扩展的。

        这样做是为了做一个等边三角形(老派函数名称,抱歉):

        #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) 函数是计算等边三角形高度的优化公式,例如h = 1/2 * sqrt(3) * x 。由于 1/2 * sqrt(3) 永远不会改变,因此我将其优化为该定义。

        【讨论】:

        • 也许您正在考虑 Cocoa,但您提供的代码在 iPhone 上不起作用。它给了我很多错误。
        【解决方案8】:

        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)
        

        【讨论】:

          【解决方案9】:

          这是一个 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()
              
            }
          }
          

          【讨论】:

            【解决方案10】:

            如果有人来寻找 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 版本中的视图小得多。

            【讨论】:

              【解决方案11】:

              这里是Brad Larson的swift 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()
                  }
              

              【讨论】:

              • 如果我想在绘制的形状上添加阴影,我该怎么做? @BradLarson
              【解决方案12】:

              我可能会在 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];
              

              【讨论】:

              • 由于分辨率独立的灵活性,我想避免使用图像。