像Instagram一样带有圆角的文本背景

8
我希望能够像Instagram一样创建带有背景颜色和圆角的文本。我已经实现了背景颜色,但无法创建圆角。
目前我所拥有的是:

enter image description here

以下是上面截图的源代码:
-(void)createBackgroundColor{
    [self.txtView.layoutManager enumerateLineFragmentsForGlyphRange:NSMakeRange(0, self.txtView.text.length) usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer *textContainer, NSRange glyphRange, BOOL *stop) {
        [textArray addObject:[NSNumber numberWithInteger:glyphRange.length]];
        if (glyphRange.length == 1){
            return ;
        }
        UIImageView *highlightBackView = [[UIImageView alloc] initWithFrame:CGRectMake(usedRect.origin.x, usedRect.origin.y  , usedRect.size.width, usedRect.size.height + 2)];
        highlightBackView.layer.borderWidth = 1;
        highlightBackView.backgroundColor = [UIColor orangeColor];
        highlightBackView.layer.borderColor = [[UIColor clearColor] CGColor];
        [self.txtView insertSubview:highlightBackView atIndex:0];
        highlightBackView.layer.cornerRadius = 5;
    }];
}

我在shouldChangeTextInRange代理中调用此函数。 我的要求:

enter image description here

请看用箭头标出的内半径,非常感谢您的帮助!


似乎你正在为每条线创建完整的圆角,而这是不必要的。你需要改变逻辑。有很多方法,有些更优化,但我建议你选择你熟悉的一种。关于角度的逻辑似乎是凹或凸。这可能是一个研究的线索。 - Larme
谢谢您的回复。 - charanjit singh
2
我建议您使用UIBezierPath自己绘制背景。 - Zuzana Paulis
2个回答

32

更新

我已经重写了这段代码的实现,并将其作为SwiftPM软件包RectangleContour软件包提供。该软件包包括如何使用其API的说明以及macOS和iOS的演示应用程序。

原始版本

所以,您想要这个:

demo

这是一篇我花了太长时间来回答的问题,而且你可能不会喜欢,因为你的问题标记为,但我用Swift写了这个答案。你可以在Objective-C中使用Swift代码,但并不是每个人都想要这样做。
你可以在这个github repo中找到我的整个测试项目,包括iOS和macOS测试应用程序。
无论如何,我们需要计算所有线矩形的联合轮廓。我找到了一篇1980年的论文描述了必要的算法:
Lipski,W.和F. Preparata. “Finding the Contour of a Union of Iso-Oriented Rectangles.” J. Algorithms 1(1980):235-246。doi:10.1016/0196-6774(80)90011-5
这个算法可能比你的问题实际需要的更加通用,因为它可以处理创建孔的矩形排列:

enter image description here

对于您来说可能有些过度,但它能完成工作。

无论如何,一旦我们有了轮廓线,我们就可以将其转换为具有圆角的CGPath以进行描边或填充。

该算法有点复杂,但我已经在CGPath的扩展方法中实现了它(使用Swift):

import CoreGraphics

extension CGPath {
    static func makeUnion(of rects: [CGRect], cornerRadius: CGFloat) -> CGPath {
        let phase2 = AlgorithmPhase2(cornerRadius: cornerRadius)
        _ = AlgorithmPhase1(rects: rects, phase2: phase2)
        return phase2.makePath()
    }
}

fileprivate func swapped<A, B>(_ pair: (A, B)) -> (B, A) { return (pair.1, pair.0) }

fileprivate class AlgorithmPhase1 {

    init(rects: [CGRect], phase2: AlgorithmPhase2) {
        self.phase2 = phase2
        xs = Array(Set(rects.map({ $0.origin.x})).union(rects.map({ $0.origin.x + $0.size.width }))).sorted()
        indexOfX = [CGFloat:Int](uniqueKeysWithValues: xs.enumerated().map(swapped))
        ys = Array(Set(rects.map({ $0.origin.y})).union(rects.map({ $0.origin.y + $0.size.height }))).sorted()
        indexOfY = [CGFloat:Int](uniqueKeysWithValues: ys.enumerated().map(swapped))
        segments.reserveCapacity(2 * ys.count)
        _ = makeSegment(y0: 0, y1: ys.count - 1)

        let sides = (rects.map({ makeSide(direction: .down, rect: $0) }) + rects.map({ makeSide(direction: .up, rect: $0)})).sorted()
        var priorX = 0
        var priorDirection = VerticalDirection.down
        for side in sides {
            if side.x != priorX || side.direction != priorDirection {
                convertStackToPhase2Sides(atX: priorX, direction: priorDirection)
                priorX = side.x
                priorDirection = side.direction
            }
            switch priorDirection {
            case .down:
                pushEmptySegmentsOfSegmentTree(atIndex: 0, thatOverlap: side)
                adjustInsertionCountsOfSegmentTree(atIndex: 0, by: 1, for: side)
            case .up:
                adjustInsertionCountsOfSegmentTree(atIndex: 0, by: -1, for: side)
                pushEmptySegmentsOfSegmentTree(atIndex: 0, thatOverlap: side)
            }
        }
        convertStackToPhase2Sides(atX: priorX, direction: priorDirection)

    }

    private let phase2: AlgorithmPhase2
    private let xs: [CGFloat]
    private let indexOfX: [CGFloat: Int]
    private let ys: [CGFloat]
    private let indexOfY: [CGFloat: Int]
    private var segments: [Segment] = []
    private var stack: [(Int, Int)] = []

    private struct Segment {
        var y0: Int
        var y1: Int
        var insertions = 0
        var status  = Status.empty
        var leftChildIndex: Int?
        var rightChildIndex: Int?

        var mid: Int { return (y0 + y1 + 1) / 2 }

        func withChildrenThatOverlap(_ side: Side, do body: (_ childIndex: Int) -> ()) {
            if side.y0 < mid, let l = leftChildIndex { body(l) }
            if mid < side.y1, let r = rightChildIndex { body(r) }
        }

        init(y0: Int, y1: Int) {
            self.y0 = y0
            self.y1 = y1
        }

        enum Status {
            case empty
            case partial
            case full
        }
    }

    private struct /*Vertical*/Side: Comparable {
        var x: Int
        var direction: VerticalDirection
        var y0: Int
        var y1: Int

        func fullyContains(_ segment: Segment) -> Bool {
            return y0 <= segment.y0 && segment.y1 <= y1
        }

        static func ==(lhs: Side, rhs: Side) -> Bool {
            return lhs.x == rhs.x && lhs.direction == rhs.direction && lhs.y0 == rhs.y0 && lhs.y1 == rhs.y1
        }

        static func <(lhs: Side, rhs: Side) -> Bool {
            if lhs.x < rhs.x { return true }
            if lhs.x > rhs.x { return false }
            if lhs.direction.rawValue < rhs.direction.rawValue { return true }
            if lhs.direction.rawValue > rhs.direction.rawValue { return false }
            if lhs.y0 < rhs.y0 { return true }
            if lhs.y0 > rhs.y0 { return false }
            return lhs.y1 < rhs.y1
        }
    }

    private func makeSegment(y0: Int, y1: Int) -> Int {
        let index = segments.count
        let segment: Segment = Segment(y0: y0, y1: y1)
        segments.append(segment)
        if y1 - y0 > 1 {
            let mid = segment.mid
            segments[index].leftChildIndex = makeSegment(y0: y0, y1: mid)
            segments[index].rightChildIndex = makeSegment(y0: mid, y1: y1)
        }
        return index
    }

    private func adjustInsertionCountsOfSegmentTree(atIndex i: Int, by delta: Int, for side: Side) {
        var segment = segments[i]
        if side.fullyContains(segment) {
            segment.insertions += delta
        } else {
            segment.withChildrenThatOverlap(side) { adjustInsertionCountsOfSegmentTree(atIndex: $0, by: delta, for: side) }
        }

        segment.status = uncachedStatus(of: segment)
        segments[i] = segment
    }

    private func uncachedStatus(of segment: Segment) -> Segment.Status {
        if segment.insertions > 0 { return .full }
        if let l = segment.leftChildIndex, let r = segment.rightChildIndex {
            return segments[l].status == .empty && segments[r].status == .empty ? .empty : .partial
        }
        return .empty
    }

    private func pushEmptySegmentsOfSegmentTree(atIndex i: Int, thatOverlap side: Side) {
        let segment = segments[i]
        switch segment.status {
        case .empty where side.fullyContains(segment):
            if let top = stack.last, segment.y0 == top.1 {
                // segment.y0 == prior segment.y1, so merge.
                stack[stack.count - 1] = (top.0, segment.y1)
            } else {
                stack.append((segment.y0, segment.y1))
            }
        case .partial, .empty:
            segment.withChildrenThatOverlap(side) { pushEmptySegmentsOfSegmentTree(atIndex: $0, thatOverlap: side) }
        case .full: break
        }
    }

    private func makeSide(direction: VerticalDirection, rect: CGRect) -> Side {
        let x: Int
        switch direction {
        case .down: x = indexOfX[rect.minX]!
        case .up: x = indexOfX[rect.maxX]!
        }
        return Side(x: x, direction: direction, y0: indexOfY[rect.minY]!, y1: indexOfY[rect.maxY]!)
    }

    private func convertStackToPhase2Sides(atX x: Int, direction: VerticalDirection) {
        guard stack.count > 0 else { return }
        let gx = xs[x]
        switch direction {
        case .up:
            for (y0, y1) in stack {
                phase2.addVerticalSide(atX: gx, fromY: ys[y0], toY: ys[y1])
            }
        case .down:
            for (y0, y1) in stack {
                phase2.addVerticalSide(atX: gx, fromY: ys[y1], toY: ys[y0])
            }
        }
        stack.removeAll(keepingCapacity: true)
    }

}

fileprivate class AlgorithmPhase2 {

    init(cornerRadius: CGFloat) {
        self.cornerRadius = cornerRadius
    }

    let cornerRadius: CGFloat

    func addVerticalSide(atX x: CGFloat, fromY y0: CGFloat, toY y1: CGFloat) {
        verticalSides.append(VerticalSide(x: x, y0: y0, y1: y1))
    }

    func makePath() -> CGPath {
        verticalSides.sort(by: { (a, b) in
            if a.x < b.x { return true }
            if a.x > b.x { return false }
            return a.y0 < b.y0
        })


        var vertexes: [Vertex] = []
        for (i, side) in verticalSides.enumerated() {
            vertexes.append(Vertex(x: side.x, y0: side.y0, y1: side.y1, sideIndex: i, representsEnd: false))
            vertexes.append(Vertex(x: side.x, y0: side.y1, y1: side.y0, sideIndex: i, representsEnd: true))
        }
        vertexes.sort(by: { (a, b) in
            if a.y0 < b.y0 { return true }
            if a.y0 > b.y0 { return false }
            return a.x < b.x
        })

        for i in stride(from: 0, to: vertexes.count, by: 2) {
            let v0 = vertexes[i]
            let v1 = vertexes[i+1]
            let startSideIndex: Int
            let endSideIndex: Int
            if v0.representsEnd {
                startSideIndex = v0.sideIndex
                endSideIndex = v1.sideIndex
            } else {
                startSideIndex = v1.sideIndex
                endSideIndex = v0.sideIndex
            }
            precondition(verticalSides[startSideIndex].nextIndex == -1)
            verticalSides[startSideIndex].nextIndex = endSideIndex
        }

        let path = CGMutablePath()
        for i in verticalSides.indices where !verticalSides[i].emitted {
            addLoop(startingAtSideIndex: i, to: path)
        }
        return path.copy()!
    }

    private var verticalSides: [VerticalSide] = []

    private struct VerticalSide {
        var x: CGFloat
        var y0: CGFloat
        var y1: CGFloat
        var nextIndex = -1
        var emitted = false

        var isDown: Bool { return y1 < y0 }

        var startPoint: CGPoint { return CGPoint(x: x, y: y0) }
        var midPoint: CGPoint { return CGPoint(x: x, y: 0.5 * (y0 + y1)) }
        var endPoint: CGPoint { return CGPoint(x: x, y: y1) }

        init(x: CGFloat, y0: CGFloat, y1: CGFloat) {
            self.x = x
            self.y0 = y0
            self.y1 = y1
        }
    }

    private struct Vertex {
        var x: CGFloat
        var y0: CGFloat
        var y1: CGFloat
        var sideIndex: Int
        var representsEnd: Bool
    }

    private func addLoop(startingAtSideIndex startIndex: Int, to path: CGMutablePath) {
        var point = verticalSides[startIndex].midPoint
        path.move(to: point)
        var fromIndex = startIndex
        repeat {
            let toIndex = verticalSides[fromIndex].nextIndex
            let horizontalMidpoint = CGPoint(x: 0.5 * (verticalSides[fromIndex].x + verticalSides[toIndex].x), y: verticalSides[fromIndex].y1)
            path.addCorner(from: point, toward: verticalSides[fromIndex].endPoint, to: horizontalMidpoint, maxRadius: cornerRadius)
            let nextPoint = verticalSides[toIndex].midPoint
            path.addCorner(from: horizontalMidpoint, toward: verticalSides[toIndex].startPoint, to: nextPoint, maxRadius: cornerRadius)
            verticalSides[fromIndex].emitted = true
            fromIndex = toIndex
            point = nextPoint
        } while fromIndex != startIndex
        path.closeSubpath()
    }

}

fileprivate extension CGMutablePath {
    func addCorner(from start: CGPoint, toward corner: CGPoint, to end: CGPoint, maxRadius: CGFloat) {
        let radius = min(maxRadius, min(abs(start.x - end.x), abs(start.y - end.y)))
        addArc(tangent1End: corner, tangent2End: end, radius: radius)
    }
}

fileprivate enum VerticalDirection: Int {
    case down = 0
    case up = 1
}

有了这个,我可以在我的视图控制器中绘制你想要的圆角背景:

private func setHighlightPath() {
    let textLayer = textView.layer
    let textContainerInset = textView.textContainerInset
    let uiInset = CGFloat(insetSlider.value)
    let radius = CGFloat(radiusSlider.value)
    let highlightLayer = self.highlightLayer
    let layout = textView.layoutManager
    let range = NSMakeRange(0, layout.numberOfGlyphs)
    var rects = [CGRect]()
    layout.enumerateLineFragments(forGlyphRange: range) { (_, usedRect, _, _, _) in
        if usedRect.width > 0 && usedRect.height > 0 {
            var rect = usedRect
            rect.origin.x += textContainerInset.left
            rect.origin.y += textContainerInset.top
            rect = highlightLayer.convert(rect, from: textLayer)
            rect = rect.insetBy(dx: uiInset, dy: uiInset)
            rects.append(rect)
        }
    }
    highlightLayer.path = CGPath.makeUnion(of: rects, cornerRadius: radius)
}

你好 @rob,安卓平台也有同样的问题,参考链接:https://dev59.com/RVYN5IYBdhLWcg3wAUH-。有什么参考资料吗? - karanatwal.github.io
1
您可以使用技术报告和我的Swift代码作为在Android上实现该算法的基础。我没有Android设备,也从未安装过Android SDK,所以无法再为您提供更多帮助。 - rob mayoff
@robmayoff 谢谢你的代码,它运行得非常好。我只遇到了一个问题,当缩放/旋转textView时,它会改变图层的填充... 你能帮忙修复一下吗? - TheTiger

1

我正在粘贴我为此制作的包装器类。感谢算法rob mayoff,它非常有效。

我知道的限制:

  1. 如果使用动态颜色,则此类不会意识到traitcollection和用户界面样式更改。高亮显示是cgColor,因此不会采用。因此,在接口样式更改后,您的高亮标签颜色将保持为.white或.black等。在外部处理并进行更新。
  2. 只是快速编写,没有测试太多场景。插图和半径自动计算以适应字体大小,但不了解或行距和其他变量。
  3. 通常没有经过严格测试,请检查是否符合您的目的。
  4. 这是一个View和其上的textView,您需要使用frame或约束对其进行布局以设置宽度和高度。没有内在内容大小。如果您想调整它,请执行intrinsicContentSize函数并发布。
  5. 禁用了textView的编辑和选择。textView是公开的;如果需要,请打开编辑。

方便的init / update签名:

init(text:String? = nil,
     font:UIFont? = nil,
     textColor:UIColor? = nil,
     highlightColor:UIColor? = nil,
     inset:CGFloat? = nil,
     radius:CGFloat? = nil)

使用示例:

let stamp = Stamp()
let stamp = Stamp(text: "Whatever\nneeds to be\nstamped.")
let stamp = Stamp(text: "Placeholder that has no line breaks but wraps anyway.")
stamp.update(text: "Smaller Version", 
             font: UIFont.systemFont(ofSize: 15, weight: .regular),
             textColor: .label, 
             highlightColor:.purple)

只需创建一个新文件并粘贴此类。按照说明使用。

import UIKit
import CoreGraphics

class Stamp: UIView, UITextViewDelegate {

var textView = UITextView()

private var text:String = "Place holder\nline\nbroken Stamp."

private var highlightLayer = CAShapeLayer()
private var highlightColor:CGColor = UIColor.systemOrange.cgColor

private var textColor:UIColor = UIColor.label
private var font:UIFont = UIFont.systemFont(ofSize: 35, weight: .bold)

private var inset:CGFloat = 1
private var radius:CGFloat = 1

override init(frame: CGRect) {
    super.init(frame: frame)
    
    textView.delegate = self
    textView.isEditable = false
    textView.isSelectable = false
    textView.font = self.font
    
    self.inset = -font.pointSize / 5
    self.radius = font.pointSize / 4
    
    self.textView.text = self.text
    self.textView.textAlignment = .center
    self.textView.backgroundColor = .clear
    
    highlightLayer.backgroundColor = nil
    highlightLayer.strokeColor = nil
    self.layer.insertSublayer(highlightLayer, at: 0)
    
    highlightLayer.fillColor = self.highlightColor
    
    addSubview(textView)
    textView.fillSuperview()
}

convenience init(text:String? = nil,
                 font:UIFont? = nil,
                 textColor:UIColor? = nil,
                 highlightColor:UIColor? = nil,
                 inset:CGFloat? = nil,
                 radius:CGFloat? = nil) {
    
    self.init(frame: .zero)
    
    self.update(text: text,
                font: font,
                textColor: textColor,
                highlightColor: highlightColor,
                inset: inset,
                radius: radius)
}

func update(text:String? = nil,
            font:UIFont? = nil,
            textColor:UIColor? = nil,
            highlightColor:UIColor? = nil,
            inset:CGFloat? = nil,
            radius:CGFloat? = nil){
    
    if let text = text { self.text = text }
    if let font = font { self.font = font }
    if let textColor = textColor { self.textColor = textColor }
    if let highlightColor = highlightColor { self.highlightColor = highlightColor.cgColor }
    
    self.inset = inset ?? -self.font.pointSize / 5
    self.radius = radius ?? self.font.pointSize / 4
    
    self.textView.text = text
    self.textView.textColor = self.textColor
    self.textView.font = self.font
    
    highlightLayer.fillColor = self.highlightColor
    
    // this will re-draw the highlight
    setHighlightPath()
}

override func layoutSubviews() {
    super.layoutSubviews()
    highlightLayer.frame = self.bounds
    self.setHighlightPath()
}

func textViewDidChange(_ textView: UITextView) {
    setHighlightPath()
}

private func setHighlightPath() {
    let textLayer = textView.layer
    let textContainerInset = textView.textContainerInset
    let uiInset = CGFloat(inset)
    let radius = CGFloat(radius)
    let highlightLayer = self.highlightLayer
    let layout = textView.layoutManager
    let range = NSMakeRange(0, layout.numberOfGlyphs)
    var rects = [CGRect]()
    layout.enumerateLineFragments(forGlyphRange: range) { (_, usedRect, _, _, _) in
        if usedRect.width > 0 && usedRect.height > 0 {
            var rect = usedRect
            rect.origin.x += textContainerInset.left
            rect.origin.y += textContainerInset.top
            rect = highlightLayer.convert(rect, from: textLayer)
            rect = rect.insetBy(dx: uiInset, dy: uiInset)
            rects.append(rect)
        }
    }
    highlightLayer.path = CGPath.makeUnion(of: rects, cornerRadius: radius)
}

// Bojler
required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}
}

extension CGPath {
    static func makeUnion(of rects: [CGRect], cornerRadius: CGFloat) -> CGPath {
        let phase2 = AlgorithmPhase2(cornerRadius: cornerRadius)
        _ = AlgorithmPhase1(rects: rects, phase2: phase2)
        return phase2.makePath()
    }
}

fileprivate func swapped<A, B>(_ pair: (A, B)) -> (B, A) { return (pair.1, pair.0) }

fileprivate class AlgorithmPhase1 {

init(rects: [CGRect], phase2: AlgorithmPhase2) {
    self.phase2 = phase2
    xs = Array(Set(rects.map({ $0.origin.x})).union(rects.map({ $0.origin.x + $0.size.width }))).sorted()
    indexOfX = [CGFloat:Int](uniqueKeysWithValues: xs.enumerated().map(swapped))
    ys = Array(Set(rects.map({ $0.origin.y})).union(rects.map({ $0.origin.y + $0.size.height }))).sorted()
    indexOfY = [CGFloat:Int](uniqueKeysWithValues: ys.enumerated().map(swapped))
    segments.reserveCapacity(2 * ys.count)
    _ = makeSegment(y0: 0, y1: ys.count - 1)

    let sides = (rects.map({ makeSide(direction: .down, rect: $0) }) + rects.map({ makeSide(direction: .up, rect: $0)})).sorted()
    var priorX = 0
    var priorDirection = VerticalDirection.down
    for side in sides {
        if side.x != priorX || side.direction != priorDirection {
            convertStackToPhase2Sides(atX: priorX, direction: priorDirection)
            priorX = side.x
            priorDirection = side.direction
        }
        switch priorDirection {
        case .down:
            pushEmptySegmentsOfSegmentTree(atIndex: 0, thatOverlap: side)
            adjustInsertionCountsOfSegmentTree(atIndex: 0, by: 1, for: side)
        case .up:
            adjustInsertionCountsOfSegmentTree(atIndex: 0, by: -1, for: side)
            pushEmptySegmentsOfSegmentTree(atIndex: 0, thatOverlap: side)
        }
    }
    convertStackToPhase2Sides(atX: priorX, direction: priorDirection)

}

private let phase2: AlgorithmPhase2
private let xs: [CGFloat]
private let indexOfX: [CGFloat: Int]
private let ys: [CGFloat]
private let indexOfY: [CGFloat: Int]
private var segments: [Segment] = []
private var stack: [(Int, Int)] = []

private struct Segment {
    var y0: Int
    var y1: Int
    var insertions = 0
    var status  = Status.empty
    var leftChildIndex: Int?
    var rightChildIndex: Int?

    var mid: Int { return (y0 + y1 + 1) / 2 }

    func withChildrenThatOverlap(_ side: Side, do body: (_ childIndex: Int) -> ()) {
        if side.y0 < mid, let l = leftChildIndex { body(l) }
        if mid < side.y1, let r = rightChildIndex { body(r) }
    }

    init(y0: Int, y1: Int) {
        self.y0 = y0
        self.y1 = y1
    }

    enum Status {
        case empty
        case partial
        case full
    }
}

private struct /*Vertical*/Side: Comparable {
    var x: Int
    var direction: VerticalDirection
    var y0: Int
    var y1: Int

    func fullyContains(_ segment: Segment) -> Bool {
        return y0 <= segment.y0 && segment.y1 <= y1
    }

    static func ==(lhs: Side, rhs: Side) -> Bool {
        return lhs.x == rhs.x && lhs.direction == rhs.direction && lhs.y0 == rhs.y0 && lhs.y1 == rhs.y1
    }

    static func <(lhs: Side, rhs: Side) -> Bool {
        if lhs.x < rhs.x { return true }
        if lhs.x > rhs.x { return false }
        if lhs.direction.rawValue < rhs.direction.rawValue { return true }
        if lhs.direction.rawValue > rhs.direction.rawValue { return false }
        if lhs.y0 < rhs.y0 { return true }
        if lhs.y0 > rhs.y0 { return false }
        return lhs.y1 < rhs.y1
    }
}

private func makeSegment(y0: Int, y1: Int) -> Int {
    let index = segments.count
    let segment: Segment = Segment(y0: y0, y1: y1)
    segments.append(segment)
    if y1 - y0 > 1 {
        let mid = segment.mid
        segments[index].leftChildIndex = makeSegment(y0: y0, y1: mid)
        segments[index].rightChildIndex = makeSegment(y0: mid, y1: y1)
    }
    return index
}

private func adjustInsertionCountsOfSegmentTree(atIndex i: Int, by delta: Int, for side: Side) {
    var segment = segments[i]
    if side.fullyContains(segment) {
        segment.insertions += delta
    } else {
        segment.withChildrenThatOverlap(side) { adjustInsertionCountsOfSegmentTree(atIndex: $0, by: delta, for: side) }
    }

    segment.status = uncachedStatus(of: segment)
    segments[i] = segment
}

private func uncachedStatus(of segment: Segment) -> Segment.Status {
    if segment.insertions > 0 { return .full }
    if let l = segment.leftChildIndex, let r = segment.rightChildIndex {
        return segments[l].status == .empty && segments[r].status == .empty ? .empty : .partial
    }
    return .empty
}

private func pushEmptySegmentsOfSegmentTree(atIndex i: Int, thatOverlap side: Side) {
    let segment = segments[i]
    switch segment.status {
    case .empty where side.fullyContains(segment):
        if let top = stack.last, segment.y0 == top.1 {
            // segment.y0 == prior segment.y1, so merge.
            stack[stack.count - 1] = (top.0, segment.y1)
        } else {
            stack.append((segment.y0, segment.y1))
        }
    case .partial, .empty:
        segment.withChildrenThatOverlap(side) { pushEmptySegmentsOfSegmentTree(atIndex: $0, thatOverlap: side) }
    case .full: break
    }
}

private func makeSide(direction: VerticalDirection, rect: CGRect) -> Side {
    let x: Int
    switch direction {
    case .down: x = indexOfX[rect.minX]!
    case .up: x = indexOfX[rect.maxX]!
    }
    return Side(x: x, direction: direction, y0: indexOfY[rect.minY]!, y1: indexOfY[rect.maxY]!)
}

private func convertStackToPhase2Sides(atX x: Int, direction: VerticalDirection) {
    guard stack.count > 0 else { return }
    let gx = xs[x]
    switch direction {
    case .up:
        for (y0, y1) in stack {
            phase2.addVerticalSide(atX: gx, fromY: ys[y0], toY: ys[y1])
        }
    case .down:
        for (y0, y1) in stack {
            phase2.addVerticalSide(atX: gx, fromY: ys[y1], toY: ys[y0])
        }
    }
    stack.removeAll(keepingCapacity: true)
}

}

fileprivate class AlgorithmPhase2 {

init(cornerRadius: CGFloat) {
    self.cornerRadius = cornerRadius
}

let cornerRadius: CGFloat

func addVerticalSide(atX x: CGFloat, fromY y0: CGFloat, toY y1: CGFloat) {
    verticalSides.append(VerticalSide(x: x, y0: y0, y1: y1))
}

func makePath() -> CGPath {
    verticalSides.sort(by: { (a, b) in
        if a.x < b.x { return true }
        if a.x > b.x { return false }
        return a.y0 < b.y0
    })


    var vertexes: [Vertex] = []
    for (i, side) in verticalSides.enumerated() {
        vertexes.append(Vertex(x: side.x, y0: side.y0, y1: side.y1, sideIndex: i, representsEnd: false))
        vertexes.append(Vertex(x: side.x, y0: side.y1, y1: side.y0, sideIndex: i, representsEnd: true))
    }
    vertexes.sort(by: { (a, b) in
        if a.y0 < b.y0 { return true }
        if a.y0 > b.y0 { return false }
        return a.x < b.x
    })

    for i in stride(from: 0, to: vertexes.count, by: 2) {
        let v0 = vertexes[i]
        let v1 = vertexes[i+1]
        let startSideIndex: Int
        let endSideIndex: Int
        if v0.representsEnd {
            startSideIndex = v0.sideIndex
            endSideIndex = v1.sideIndex
        } else {
            startSideIndex = v1.sideIndex
            endSideIndex = v0.sideIndex
        }
        precondition(verticalSides[startSideIndex].nextIndex == -1)
        verticalSides[startSideIndex].nextIndex = endSideIndex
    }

    let path = CGMutablePath()
    for i in verticalSides.indices where !verticalSides[i].emitted {
        addLoop(startingAtSideIndex: i, to: path)
    }
    return path.copy()!
}

private var verticalSides: [VerticalSide] = []

private struct VerticalSide {
    var x: CGFloat
    var y0: CGFloat
    var y1: CGFloat
    var nextIndex = -1
    var emitted = false

    var isDown: Bool { return y1 < y0 }

    var startPoint: CGPoint { return CGPoint(x: x, y: y0) }
    var midPoint: CGPoint { return CGPoint(x: x, y: 0.5 * (y0 + y1)) }
    var endPoint: CGPoint { return CGPoint(x: x, y: y1) }

    init(x: CGFloat, y0: CGFloat, y1: CGFloat) {
        self.x = x
        self.y0 = y0
        self.y1 = y1
    }
}

private struct Vertex {
    var x: CGFloat
    var y0: CGFloat
    var y1: CGFloat
    var sideIndex: Int
    var representsEnd: Bool
}

private func addLoop(startingAtSideIndex startIndex: Int, to path: CGMutablePath) {
    var point = verticalSides[startIndex].midPoint
    path.move(to: point)
    var fromIndex = startIndex
    repeat {
        let toIndex = verticalSides[fromIndex].nextIndex
        let horizontalMidpoint = CGPoint(x: 0.5 * (verticalSides[fromIndex].x + verticalSides[toIndex].x), y: verticalSides[fromIndex].y1)
        path.addCorner(from: point, toward: verticalSides[fromIndex].endPoint, to: horizontalMidpoint, maxRadius: cornerRadius)
        let nextPoint = verticalSides[toIndex].midPoint
        path.addCorner(from: horizontalMidpoint, toward: verticalSides[toIndex].startPoint, to: nextPoint, maxRadius: cornerRadius)
        verticalSides[fromIndex].emitted = true
        fromIndex = toIndex
        point = nextPoint
    } while fromIndex != startIndex
    path.closeSubpath()
}

}
    fileprivate extension CGMutablePath {
        func addCorner(from start: CGPoint, toward corner: CGPoint, to end: CGPoint, maxRadius: CGFloat) {
            let radius = min(maxRadius, min(abs(start.x - end.x), abs(start.y - end.y)))
            addArc(tangent1End: corner, tangent2End: end, radius: radius)
        }
    }
    
    fileprivate enum VerticalDirection: Int {
        case down = 0
        case up = 1
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接