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

后端 未结 11 1133
礼貌的吻别
礼貌的吻别 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:17

    I would probably make the whole image (including the triangle) in Photoshop, and then display it on the screen at the appropriate time using the:

    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];
    
    0 讨论(0)
  • 2020-11-29 17:20

    If anyone comes along looking for the Swift 3 answer, this does the trick! Thanks to those who contributed before I did, lovely piece of code!

        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)
    

    In my case triangleWidth = 10 and triangleHeight = 5 for a much smaller view than what's in OPs version.

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

    Here is the swift 3 solution of Brad Larson

    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()
        }
    
    0 讨论(0)
  • 2020-11-29 17:23

    I get here looking for a solution to draw "arrows" in an existing view.
    I'm pleased to share you some code that I hope usefull - Swift 2.3 compatible -

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

    In this way you can call this for every kind of view:

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

    In a future I'll add offset for pike position.

    • yes, names are definitely improvable!

    SWIFT 3 Version

    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))
        }
    }
    
    0 讨论(0)
  • 2020-11-29 17:27

    Swift 4 Update

    Here's a Swift 4 version of AVT's original code.

     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)
    
    0 讨论(0)
提交回复
热议问题