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

2024-02-27

我想像 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代表。

我想要的是:

请参阅标有箭头的内半径,任何帮助将不胜感激!


UPDATE

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

ORIGINAL

所以,你想要这个:

这是我花了一个答案way太长了,您可能甚至不喜欢,因为您的问题已被标记目标-c /questions/tagged/objective-c但我用 Swift 写了这个答案。您可以使用 Objective-C 中的 Swift 代码,但并不是每个人都愿意这样做。

你可以找到我的整个测试项目,包括iOS和macOS测试应用程序,在这个 github 仓库中 https://github.com/mayoff/contour.

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

Lipski,W. 和 F. Preparata。“求等向矩形并集的轮廓。” https://sci-hub.se/10.1016/0196-6774(80)90011-5 J. 算法1(1980):235-246。号码: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
}

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

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)
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

随机推荐

  • Lambda/Linq 包含多个关键字的条件

    我必须列出 带有评论字段的主列表 要搜索的关键字列表 我想搜索每条记录的每个注释字段中的关键字 在 SQL 中如下所示 select from MainList where Comment like keyword1 or Comment
  • 在Slave redis db上写入

    我有一个主 Redis 服务器 S1 还有 6 个其他服务器 我希望他们有本地redis从属服务器 因此redis主服务器上的任何更改都将复制到每个本地服务器上的从属服务器 我可以在一个从站上写入并让该从站更新主站 然后主站将更新其他从站吗
  • 从另一个浏览器打开一个浏览器[重复]

    这个问题在这里已经有答案了 我正在 Firefox 上运行我的 Web 应用程序 我在其中一个页面上有超链接 当我单击它时 它会打开另一个应用程序作为新的弹出窗口 但 Firefox 不支持该应用程序 所以我想在 Internet Expl
  • 将 Android CheckBox 设置为不同的图像...然后返回原始图像

    我正在使用以下 非常常见 代码来更改 Android 应用程序中的复选框图像 mCheck CheckBox findViewById R id chkMine mCheck setButtonDrawable R drawable my
  • 如何通过属性名称和/或值查找锚标记? [复制]

    这个问题在这里已经有答案了 这是我正在尝试自动化的网络 https www supremenewyork com shop sweatshirts xi9sboa21 u2te1fdw8 https www supremenewyork c
  • 为 Databricks 生成数据库架构图

    我正在创建一个 Databricks 应用程序 并且数据库架构变得非常重要 有没有办法为 Databricks 数据库生成架构图 类似于可以从 mysql 生成的架构图 有两种可能的变体 使用 Spark SQL 与show databas
  • 将 C/C++ 回调传递到 matlab 引擎

    我有一个 C 文件 启动 matlab 引擎 calls matlab optimize 一个编译后的 m 文件 在内部运行 matlab 优化器之一 打印结果 停止发动机并退出 这很好用 我现在想将第二行更改为 calls matlab
  • Xcode 文档图标未更新

    我创建了一个 icns并在我的项目的 文档类型 部分中进行设置 但 Finder 中的文档图标仍然是通用的 我注意到 如果更改文档类型的文件扩展名 则会显示该图标 是否有我需要清除的缓存或其他方式来更新图标而不更改文件扩展名 要强制刷新图标
  • Sequelize 同步与迁移

    我正在学习 Sequelize 我想了解一些有关同步与迁移的说明 我知道同步将根据我的模型架构创建丢失的表 但我还读到同步用于初始化数据库 而迁移用于生产 如果是这种情况 express example 显示调用同步来自bin www ht
  • 在哪里可以获得 MediaWiki 的模板?

    我注意到很多基于 mediawiki 的网站都使用 Robelbox Ambox 等模板 我在哪里可以获得它们以及如何安装它们 It s a PITA http www urbandictionary com define php term
  • 为什么 Python 的 lambda 表达式中不允许赋值?

    这不是重复的Python 中 lambda 表达式内的赋值 https stackoverflow com questions 6282042 assignment inside lambda expression in python 即我
  • 如何在 JavaFX2 中的任务之间重置进度指示器?

    我的主屏幕 UI 上有一个进度指示器 由各种选项卡和服务共享 每个 TabController 都有自己的 Service 实例 在我的 MainController 类中 对于每个选项卡 我已将每个服务的进度属性绑定到 ProgressI
  • 通过 Kivy 在 Android 上运行 Tensorflow

    I found 这个答案 https stackoverflow com a 34495029 828184这让我想到了一个想法 你可以在 Android 手机上使用 kivy 而不是使用编译后的张量流图 这样你就可以使用直接与张量流图对话
  • 将数组中的相同值分组

    我有一个数组 里面有一些值 我希望返回另一个数组 该数组的值分组到自己的数组中 所以我想要达到的结果是这样的 var arr 1 1 2 2 2 3 3 4 4 4 4 5 6 var groupedArr 1 1 2 2 2 3 3 4
  • Xamarin.Android 中的测试活动

    我想对活动生命周期的一些交互进行单元测试 我正在开发一个数据绑定库 没有它就无法进行单元测试 安卓也有一些不错的文档 http developer android com tools testing activity testing htm
  • “代码生成器已取消优化 [某些文件] 的样式,因为它超过了最大值“100KB””是什么意思?

    我向我的项目添加了一个新的 npm 包 并在我的一个模块中需要它 现在我从 webpack 收到这条消息 build modulesNote The code generator has deoptimised the styling of
  • SSRS 选择性分页符

    我正在处理的有关财产记录的当前报告在 SSRS 中遇到一些格式问题 我的报告由 6 个在横向视图中对齐的 tablixes 构建而成 旨在保留特定的布局以供潜在的报告打印 第一个 Tablix 包含有关相关财产和所有者的一般信息 该信息将是
  • 如何以编程方式选择 MKUserLocation?

    标题和副标题可以添加到 iOS 显示的用户位置MK用户位置 https stackoverflow com questions 6400880 on mkuserlocation how do i show my own custom me
  • 如何重置向南迁移以捕获 django 模型的当前状态

    我有一个应用程序 目前有 35 次向南迁移 在设置新部署时 我们经常创建新部署 这些需要一段时间才能完成 并且应用程序正在不断发展 添加更多迁移 此外 迁移还包括一些潜在的复杂数据迁移和一些破坏 SQLite3 的自定义迁移 目前这不是一个
  • 像 Instagram 一样带有圆角的文本背景

    我想像 Instagram 一样创建具有背景颜色和圆角的文本 我能够实现背景颜色 但无法创建圆角 到目前为止我所拥有的 下面是上面截图的源代码 void createBackgroundColor self txtView layoutMa