如何教UITextView精美脱颖而出

问题陈述


给定:多行文字。
发现:设计精美的背景。

是的,这是一个小时,我想。 “您只需要将backgroundColor放在attributedText中。” 但这还不够。 事实是标准选择是一个填充的矩形。 丑 解决方案-您需要自定义! 它创建的棘手道路促使我描述了这一过程,这样子孙后代就不必遭受那么多的痛苦。 有兴趣的,我要猫。

验收


第一步是进行经典的Internet通话,并提出适当实施的问题。 对此,几乎没有建议。 最合适的是,一切都覆盖了fillBackgroundRectArray方法。 在正常生活中,他负责使用上述的attributedText属性为文本着色。 为了了解它是哪种动物,我尝试了一种现成的解决方案,希望任务仍然持续几个小时。 无法解决。 这个决定非常糟糕。

文档就是一切。


决定不再执行此操作,我转向了文档。 由此可以明显看出,UITextView是UIScrollView的简单后代,对于三个同伴驱动的文本而言:

  • NSTextStorage-本质上是NSMutableAtributedString的包装,用于存储文本及其属性;
  • NSTextContainer-NSObject,负责显示文本的几何形状。 默认情况下,它是一个矩形,您可以自定义任何内容。
  • NSLayoutManager-管理前两个:获取文本,缩进,分成段落,还负责我们需要的填充。

算法很酷


结果,该任务归结为创建自定义NSLayoutManadger并覆盖其中的所需方法。

class SelectionLayoutManager: NSLayoutManager { override func fillBackgroundRectArray(_ rectArray: UnsafePointer<CGRect>, count rectCount: Int, forCharacterRange charRange: NSRange, color: UIColor) { } 

在主要实现中,fillBackgroundRectArray获取单词的矩形并将其绘制。 通常,由于提供的图形是一条线的单独部分,且在任何地方都有交界处,因此必须将其废弃。 因此,时间的子任务是:确定正确的矩形,这些矩形从行的开头开始,并且一直到行尾都密不可分。

解决此问题的方法是以下算法:它根据段落循环进入,并检查是否在下一行中添加了该行,该行是否适合一行? 如果不是,则这是一个单独的完整行,请转到下一个。 如果是这样,请输入下一个单词并再次检查条件。 是的,实现非常简单,曾经有一个方法可以创建递归方法,但是还没有动手。 在评论中,我要求您优化的选项。

 private func detectLines(from string: String, width: CGFloat, font: UIFont) -> [String] { var strings: [String] = [] var cumulativeString = "" let words = string.components(separatedBy: CharacterSet.whitespaces) for word in words { let checkingString = cumulativeString + word if checkingString.size(withFont: font).width < width { cumulativeString.append(word) } else { if cumulativeString.isNotEmpty { strings.append(cumulativeString) } if word.size(withFont: font).width < width { cumulativeString = word } else { var stringsFromWord: [String] = [] var handlingWord = word while handlingWord.isNotEmpty { let fullFillString = detectFullFillString(from: handlingWord, width: width, font: font) stringsFromWord.append(fullFillString) handlingWord = word.replacingOccurrences(of: stringsFromWord.reduce("") { $0 + $1 }, with: "") } stringsFromWord.removeLast() strings.append(contentsOf: stringsFromWord) let remainString = word.replacingOccurrences(of: stringsFromWord.reduce("") { $0 + $1 }, with: "") cumulativeString = remainString } } } if cumulativeString.isNotEmpty { strings.append(cumulativeString) } return strings } 

值得一提的是一种特殊情况,即单词本身不适合一行。 UITextView非常简单地与它们一起工作-转移到未包含的零件的下一行。 我们使用相同的顺序传递方法,在一个单独的方法中复制此逻辑,但在这种情况下使用符号。 它包含了什么:一个完整​​的行,其余的-在一个很长的单词的情况下是一个完整的行,或者在一个新行上只是一个新单词。

 private func detectFullFillString(from word: String, width: CGFloat, font: UIFont) -> String { var string = "" for character in word { let checkingString = string.appending(String(character)) if checkingString.size(withFont: font).width > width { break } else { string.append(contentsOf: String(character)) } } return string } 

作为detectLines(from:width:font :)方法的结果,我们得到了沿线正确分割的字符串数组。 接下来,从帧(对于线条:宽度:字体)方法中,我们获得一个坐标和线条尺寸的数组。

 private func frames(for lines: [String], width: CGFloat, font: UIFont) -> [CGRect] { var rects: [CGRect] = [] let stringsSizes = lines.map { $0.size(withFont: font) } stringsSizes.forEach { let rect = CGRect(origin: CGPoint(x: (width - $0.width) / 2, y: $0.height * CGFloat(rects.count)), size: $0) rects.append(rect) } return rects } 

带来美丽


第二个子任务:在矩形上绘制。 在“美丽”的概念下,应该用圆角填充矩形的选定颜色。 解决方案:使用UIBezierPath在上下文中的指定坐标处绘制一个附加层。 为了使关节看起来更好,我们将不对宽度较小的矩形的边缘进行圆角处理。 方法很简单:我们遵循每个矩形的坐标并绘制轮廓。

 private func path(from rects: [CGRect], cornerRadius: CGFloat, horizontalInset: CGFloat) -> CGPath { let path = CGMutablePath() rects.enumerated().forEach { (index, rect) in let hasPrevious = index > 0 let isPreviousWider = hasPrevious ? rects[index - 1].width >= rect.width || abs(rects[index - 1].width - rect.width) < 5 : false let hasNext = index != rects.count - 1 let isNextWider = hasNext ? rects[index + 1].width >= rect.width || abs(rects[index + 1].width - rect.width) < 5 : false path.move(to: CGPoint(x: rect.minX - horizontalInset + (isPreviousWider ? 0 : cornerRadius), y: rect.minY)) // top path.addLine(to: CGPoint(x: rect.maxX + horizontalInset - (isPreviousWider ? 0 : cornerRadius), y: rect.minY)) if isPreviousWider == false { path.addQuadCurve(to: CGPoint(x: rect.maxX + horizontalInset, y: rect.minY + cornerRadius), control: CGPoint(x: rect.maxX + horizontalInset, y: rect.minY)) } // right path.addLine(to: CGPoint(x: rect.maxX + horizontalInset, y: rect.maxY - (isNextWider ? 0 : cornerRadius))) if isNextWider == false { path.addQuadCurve(to: CGPoint(x: rect.maxX + horizontalInset - cornerRadius, y: rect.maxY), control: CGPoint(x: rect.maxX + horizontalInset, y: rect.maxY)) } // bottom path.addLine(to: CGPoint(x: rect.minX - horizontalInset + (isNextWider ? 0 : cornerRadius), y: rect.maxY)) if isNextWider == false { path.addQuadCurve(to: CGPoint(x: rect.minX - horizontalInset, y: rect.maxY - cornerRadius), control: CGPoint(x: rect.minX - horizontalInset, y: rect.maxY)) } // left path.addLine(to: CGPoint(x: rect.minX - horizontalInset, y: rect.minY + (isPreviousWider ? 0 : cornerRadius))) if isPreviousWider == false { path.addQuadCurve(to: CGPoint(x: rect.minX - horizontalInset + cornerRadius, y: rect.minY), control: CGPoint(x: rect.minX - horizontalInset, y: rect.minY)) } path.closeSubpath() } return path } 

接下来,在draw(_:color :)方法中,填充路径。

 private func draw(_ path: CGPath, color: UIColor) { color.set() if let ctx = UIGraphicsGetCurrentContext() { ctx.setAllowsAntialiasing(true) ctx.setShouldAntialias(true) ctx.addPath(path) ctx.drawPath(using: .fillStroke) } } 

fillBackgroundRectArray方法的完整代码(_:count:forCharacterRange:color :)

 override func fillBackgroundRectArray(_ rectArray: UnsafePointer<CGRect>, count rectCount: Int, forCharacterRange charRange: NSRange, color: UIColor) { let cornerRadius: CGFloat = 8 let horizontalInset: CGFloat = 5 guard let font = (textStorage?.attributes(at: 0, effectiveRange: nil).first { $0.key == .font })?.value as? UIFont, let textContainerWidth = textContainers.first?.size.width else { return } /// Divide the text into paragraphs let lines = paragraphs(from: textStorage?.string ?? "") /// Divide the paragraphs into separate lines let strings = detectLines(from: lines, width: textContainerWidth, font: font) /// Get rects from the lines let rects = frames(for: strings, width: textContainerWidth, font: font) /// Get a contour by rects let rectsPath = path(from: rects, cornerRadius: cornerRadius, horizontalInset: horizontalInset) /// Fill it draw(rectsPath, color: color) } 

我们开始。 我们检查。 效果很好。


结论:习俗有时是困难且令人作呕的,但是当它按其应有的方式运作时,它是多么的美丽。 演员,太好了。

完整的代码可以在此处SelectionLayoutManager中找到。

参考文献


  1. NSLayoutManager
  2. NSTextContainer
  3. NSTextView
  4. 使用文本工具包绘制和管理文本

Source: https://habr.com/ru/post/zh-CN479992/


All Articles