如何在CALayer子类中绘制带背景与内边距的NSAttributedString?
在CALayer子类中绘制带行背景和内边距的NSAttributedString
要实现每行文本带背景框和自定义内边距,不能直接用CTFrameDraw一次性绘制全部文本,需要拆分到单行(CTLine)层级处理:先为每行计算带内边距的背景区域,绘制背景后再调整位置绘制文本。
完整实现代码
1. 定义Layer子类属性
先在子类中声明需要的配置属性,方便后续调整:
class AttributedTextLayer: CALayer { // 要绘制的富文本 var attributedString: NSAttributedString? { didSet { setNeedsDisplay() } } // 文本与背景框的内边距 var textInsets: UIEdgeInsets = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8) { didSet { setNeedsDisplay() } } // 默认行背景色 private let defaultLineBackgroundColor: UIColor = .lightGray }
2. 重写绘制方法
在draw(in:)中按行处理文本与背景:
override func draw(in ctx: CGContext) { guard let attributedString = attributedString else { return } // 坐标系转换:CoreText默认左下为原点,转为UIKit的左上原点坐标系 ctx.saveGState() ctx.translateBy(x: 0, y: bounds.height) ctx.scaleBy(x: 1.0, y: -1.0) // 计算文本实际绘制区域:整体区域扣除内边距 let textContentRect = bounds.inset(by: textInsets) let textPath = CGPath(rect: textContentRect, transform: nil) // 创建CoreText框架对象 let framesetter = CTFramesetterCreateWithAttributedString(attributedString as CFAttributedString) let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attributedString.length), textPath, nil) // 获取所有文本行及行的原点位置 guard let lines = CTFrameGetLines(frame) as? [CTLine] else { return } var lineOrigins = [CGPoint](repeating: .zero, count: lines.count) CTFrameGetLineOrigins(frame, CFRangeMake(0, lines.count), &lineOrigins) // 遍历每行绘制背景和文本 for (index, line) in lines.enumerated() { let lineOrigin = lineOrigins[index] // 获取行的精确边界(包含字体上下留白) let lineBounds = CTLineGetBoundsWithOptions(line, [.useOpticalBounds]) // 计算行背景框的rect:行边界 + 自定义内边距 var backgroundRect = CGRect( x: textContentRect.origin.x + lineOrigin.x - textInsets.left, y: textContentRect.origin.y + lineOrigin.y - textInsets.top, width: lineBounds.width + textInsets.left + textInsets.right, height: lineBounds.height + textInsets.top + textInsets.bottom ) // 修正y轴位置:CTLine的origin是基线位置,需调整到行顶部 backgroundRect.origin.y -= lineBounds.origin.y // 绘制行背景 ctx.setFillColor(defaultLineBackgroundColor.cgColor) ctx.fill(backgroundRect) // 绘制文本:平移上下文到行的正确位置 ctx.saveGState() ctx.translateBy( x: textContentRect.origin.x + lineOrigin.x, y: textContentRect.origin.y + lineOrigin.y - lineBounds.origin.y ) CTLineDraw(line, ctx) ctx.restoreGState() } ctx.restoreGState() }
关键细节说明
- 坐标系适配:CoreText与UIKit坐标系方向相反,通过
translate和scale转换后,绘制逻辑更符合日常习惯。 - 行背景计算:利用
CTLineGetBoundsWithOptions获取包含字体光学边界的行范围,加上自定义内边距得到背景框,同时修正基线偏移带来的位置误差。 - 文本定位:每行绘制时单独平移上下文,确保文本在背景框内保持设置的内边距。
扩展:支持单行自定义背景色
如果需要为不同行设置不同背景色,可以给NSAttributedString添加自定义属性:
// 自定义属性Key private let lineBackgroundColorAttributeKey = NSAttributedString.Key(rawValue: "LineBackgroundColor") // 使用时为指定文本范围设置背景色 let mutableAttrStr = NSMutableAttributedString(string: "第一行文本\n第二行不同背景的文本") mutableAttrStr.addAttribute(lineBackgroundColorAttributeKey, value: UIColor.systemBlue, range: NSRange(location: 6, length: 11))
然后在绘制每行时读取对应颜色:
// 替换原背景色设置代码 let lineRange = CTLineGetStringRange(line) if let customColor = attributedString.attribute(lineBackgroundColorAttributeKey, at: lineRange.location, effectiveRange: nil) as? UIColor { ctx.setFillColor(customColor.cgColor) } else { ctx.setFillColor(defaultLineBackgroundColor.cgColor) }
内容的提问来源于stack exchange,提问作者Vladimir Anisimov
相关产品推荐
相关产品推荐

