我想像 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
这个算法可能比您的问题实际需要的更通用,因为它可以处理产生孔洞的矩形排列: 所以这对你来说可能有点过头了,但它完成了工作。 无论如何,一旦我们有了轮廓,我们就可以将它转换为带有圆角的 该算法有些复杂,但我(在 Swift 中)将其实现为 有了这个,我可以在我的 View Controller 中绘制你想要的圆形背景:CGPath 以进行描边或填充。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)
}
关于ios - 像 Instagram 一样带有圆角的文本背景,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48088956/
我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h
所以我在关注Railscast,我注意到在html.erb文件中,ruby代码有一个微弱的背景高亮效果,以区别于其他代码HTML文档。我知道Ryan使用TextMate。我正在使用SublimeText3。我怎样才能达到同样的效果?谢谢! 最佳答案 为SublimeText安装ERB包。假设您安装了SublimeText包管理器*,只需点击cmd+shift+P即可获得命令菜单,然后键入installpackage并选择PackageControl:InstallPackage获取包管理器菜单。在该菜单中,键入ERB并在看到包时选择
我有一张背景图片,我想在其中添加一个文本框。我想弄清楚如何将标题放置在其顶部的正确位置。(我使用标题是因为我需要自动换行功能)。现在,我只能让文本显示在左上角,但我需要能够手动定位它的开始位置。require'RMagick'require'Pry'includeMagicktext="Loremipsumdolorsitamet"img=ImageList.new('template001.jpg')img 最佳答案 这是使用convert的ImageMagick命令行的答案。如果你想在Rmagick中使用这个方法,你必须自己移植
这里有一个很好的答案解释了如何在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返回它复制的字节数,但是当我还没有下
我正在尝试解析一个文本文件,该文件每行包含可变数量的单词和数字,如下所示:foo4.500bar3.001.33foobar如何读取由空格而不是换行符分隔的文件?有什么方法可以设置File("file.txt").foreach方法以使用空格而不是换行符作为分隔符? 最佳答案 接受的答案将slurp文件,这可能是大文本文件的问题。更好的解决方案是IO.foreach.它是惯用的,将按字符流式传输文件:File.foreach(filename,""){|string|putsstring}包含“thisisanexample”结果的
1.错误信息:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:requestcanceledwhilewaitingforconnection(Client.Timeoutexceededwhileawaitingheaders)或者:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:TLShandshaketimeout2.报错原因:docker使用的镜像网址默认为国外,下载容易超时,需要修改成国内镜像地址(首先阿里
使用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中做
假设我有一个类A,里面有一些方法。假设stringmethodName是这些方法之一,我已经知道我想给它什么参数。它们在散列中{'param1'=>value1,'param2'=>value2}所以我有:params={'param1'=>value1,'param2'=>value2}a=A.new()a.send(methodName,value1,value2)#callmethodnamewithbothparams我希望能够通过传递我的哈希以某种方式调用该方法。这可能吗? 最佳答案 确保methodName是一个符号,而
当我进入Rails控制台时,我已将pry设置为加载代替irb。我找不到该页面或不记得如何将其恢复为默认行为,因为它似乎干扰了我的Rubymine调试器。有什么建议吗? 最佳答案 我刚发现问题,pry-railsgem。忘记了它的目的是让“railsconsole”打开pry。 关于ruby-on-rails-带有Pry的Rails控制台,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/question
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上