像Instagram一样带有圆角的文本背景 [英] Text background with round corner like Instagram does

查看:74
本文介绍了像Instagram一样带有圆角的文本背景的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想像Instagram一样使用背景颜色和圆角创建文本.我可以实现背景颜色,但无法创建圆角.

I want to create text with background color and round corners like Instagram does. I am able to achieve the background color but could not create the round corners.

我到目前为止所拥有的:

以下是上述屏幕截图的源代码:

-(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委托中调用此函数.

I call this function in shouldChangeTextInRange delegate.

我想要的东西:

查看带有箭头标记的内半径,将不胜感激!

See the inner radius marked with arrows, Any help would be appreciated!

推荐答案

因此,您需要这样做:

以下是我在方式上花了很长时间的答案,您可能甚至不喜欢,因为您的问题被标记为,但是我在Swift中写了这个答案.您可以使用Objective-C中的Swift代码,但并非所有人都愿意.

Here's an answer that I spent way too long on, and that you probably won't even like, because your question is tagged objective-c but I wrote this answer in Swift. You can use Swift code from Objective-C, but not everyone wants to.

您可以在此github存储库中找到我的整个测试项目,包括iOS和macOS测试应用程序 .

You can find my entire test project, including iOS and macOS test apps, in this github repo.

无论如何,我们需要做的是计算所有线矩形的并集轮廓. Lipski和Preparata于1979年发布了一种用于计算该轮廓的算法.

Anyway, what we need to do is compute the contour of the union of all of the line rects. Lipski and Preparata published an algorithm for computing this contour in 1979.

此算法可能比您的问题实际需要的更通用,因为它可以处理产生孔的矩形排列:

This algorithm is probably more general than actually required for your problem, since it can handle rectangle arrangements that create holes:

所以这对您来说可能算是过高了,但它可以完成工作.

So it might be overkill for you, but it gets the job done.

无论如何,一旦有了轮廓,就可以将其转换为带有圆角的CGPath以便进行描边或填充.

Anyway, once we have the contour, we can convert it to a CGPath with rounded corners for stroking or filling.

该算法多少有些涉及,但是我(在Swift中)将其实现为CGPath上的扩展方法:

The algorithm is somewhat involved, but I implemented it (in Swift) as an extension method on CGPath:

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
}

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

With this, I can paint the rounded background you want in my view controller:

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

这篇关于像Instagram一样带有圆角的文本背景的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆