You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 05:16:34