草庐IT

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

coder 2023-09-24 原文

我想像 Instagram 那样创建带有背景色和圆角的文本。我能够实现背景颜色,但无法创建圆角。

我目前拥有的:

以上截图的源代码如下:

-(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 委托(delegate)中调用这个函数。

我想要的:

查看标有箭头的内半径,如有任何帮助,我们将不胜感激!

最佳答案

更新

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

原创

所以,你想要这个:

这是一个我花了方式太久的答案,你可能甚至不会喜欢,因为你的问题被标记为 objective-c但我用 Swift 写了这个答案。您可以使用来自 Objective-C 的 Swift 代码,但并不是每个人都想这样做。

您可以找到我的整个测试项目,包括 iOS 和 macOS 测试应用程序,in this github repo .

无论如何,我们需要做的是计算所有直线矩形的并集的轮廓。我找到了一篇 1980 年的论文,描述了必要的算法:

Lipski, W. 和 F. Preparata。 “Finding the Contour of a Union of Iso-Oriented Rectangles.” J。算法 1 (1980):235-246。 doi:10.1016/0196-6774(80)90011-5

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

所以这对你来说可能有点过头了,但它完成了工作。

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

该算法有些复杂,但我(在 Swift 中)将其实现为 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
}

有了这个,我可以在我的 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)
}

关于ios - 像 Instagram 一样带有圆角的文本背景,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48088956/

有关ios - 像 Instagram 一样带有圆角的文本背景的更多相关文章

  1. ruby - 使用 ruby​​ 将 HTML 转换为纯文本并维护结构/格式 - 2

    我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h

  2. ruby-on-rails - 使用 Sublime Text 3 突出显示 HTML 背景语法中的 ERB? - 2

    所以我在关注Railscast,我注意到在html.erb文件中,ruby代码有一个微弱的背景高亮效果,以区别于其他代码HTML文档。我知道Ryan使用TextMate。我正在使用SublimeText3。我怎样才能达到同样的效果?谢谢! 最佳答案 为SublimeText安装ERB包。假设您安装了SublimeText包管理器*,只需点击cmd+shift+P即可获得命令菜单,然后键入installpackage并选择PackageControl:InstallPackage获取包管理器菜单。在该菜单中,键入ERB并在看到包时选择

  3. ruby-on-rails - 使用 Rmagick 或 ImageMagick 在背景上放置标题 - 2

    我有一张背景图片,我想在其中添加一个文本框。我想弄清楚如何将标题放置在其顶部的正确位置。(我使用标题是因为我需要自动换行功能)。现在,我只能让文本显示在左上角,但我需要能够手动定位它的开始位置。require'RMagick'require'Pry'includeMagicktext="Loremipsumdolorsitamet"img=ImageList.new('template001.jpg')img 最佳答案 这是使用convert的ImageMagick命令行的答案。如果你想在Rmagick中使用这个方法,你必须自己移植

  4. ruby - 如何验证 IO.copy_stream 是否成功 - 2

    这里有一个很好的答案解释了如何在Ruby中下载文件而不将其加载到内存中:https://stackoverflow.com/a/29743394/4852737require'open-uri'download=open('http://example.com/image.png')IO.copy_stream(download,'~/image.png')我如何验证下载文件的IO.copy_stream调用是否真的成功——这意味着下载的文件与我打算下载的文件完全相同,而不是下载一半的损坏文件?documentation说IO.copy_stream返回它复制的字节数,但是当我还没有下

  5. Ruby 文件 IO 定界符? - 2

    我正在尝试解析一个文本文件,该文件每行包含可变数量的单词和数字,如下所示:foo4.500bar3.001.33foobar如何读取由空格而不是换行符分隔的文件?有什么方法可以设置File("file.txt").foreach方法以使用空格而不是换行符作为分隔符? 最佳答案 接受的答案将slurp文件,这可能是大文本文件的问题。更好的解决方案是IO.foreach.它是惯用的,将按字符流式传输文件:File.foreach(filename,""){|string|putsstring}包含“thisisanexample”结果的

  6. Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting - 2

    1.错误信息:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:requestcanceledwhilewaitingforconnection(Client.Timeoutexceededwhileawaitingheaders)或者:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:TLShandshaketimeout2.报错原因:docker使用的镜像网址默认为国外,下载容易超时,需要修改成国内镜像地址(首先阿里

  7. ruby-on-rails - 带有 Zeus 的 RSpec 3.1,我应该在 spec_helper 中要求 'rspec/rails' 吗? - 2

    使用rspec-rails3.0+,测试设置分为spec_helper和rails_helper我注意到生成的spec_helper不需要'rspec/rails'。这会导致zeus崩溃:spec_helper.rb:5:in`':undefinedmethod`configure'forRSpec:Module(NoMethodError)对thisissue最常见的回应是需要'rspec/rails'。但这是否会破坏仅使用spec_helper拆分rails规范和PORO规范的全部目的?或者这无关紧要,因为Zeus无论如何都会预加载Rails?我应该在我的spec_helper中做

  8. Ruby:如何使用带有散列的 'send' 方法调用方法? - 2

    假设我有一个类A,里面有一些方法。假设stringmethodName是这些方法之一,我已经知道我想给它什么参数。它们在散列中{'param1'=>value1,'param2'=>value2}所以我有:params={'param1'=>value1,'param2'=>value2}a=A.new()a.send(methodName,value1,value2)#callmethodnamewithbothparams我希望能够通过传递我的哈希以某种方式调用该方法。这可能吗? 最佳答案 确保methodName是一个符号,而

  9. ruby-on-rails - 带有 Pry 的 Rails 控制台 - 2

    当我进入Rails控制台时,我已将pry设置为加载代替irb。我找不到该页面或不记得如何将其恢复为默认行为,因为它似乎干扰了我的Rubymine调试器。有什么建议吗? 最佳答案 我刚发现问题,pry-railsgem。忘记了它的目的是让“railsconsole”打开pry。 关于ruby-on-rails-带有Pry的Rails控制台,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/question

  10. ruby - 为什么不能使用类IO的实例方法noecho? - 2

    print"Enteryourpassword:"pass=STDIN.noecho(&:gets)puts"Yourpasswordis#{pass}!"输出:Enteryourpassword:input.rb:2:in`':undefinedmethod`noecho'for#>(NoMethodError) 最佳答案 一开始require'io/console'后来的Ruby1.9.3 关于ruby-为什么不能使用类IO的实例方法noecho?,我们在StackOverflow上

随机推荐