import UIKit #if canImport(DTCoreText) import DTCoreText /// 基于 DTCoreText 的 Core Text 直接绘制视图 /// 将 DTCoreText 的排版结果直接绘制到 UIView 上,跳过 UITextView 的间接渲染。 /// 对齐 WXRead 的 WRPageView:在同一 drawRect 中绘制高亮背景、文字和选区。 final class RDEPUBTextPageRenderView: UIView { var layoutFrame: DTCoreTextLayoutFrame? { didSet { setNeedsDisplay() } } var drawOptions: DTCoreTextLayoutFrameDrawingOptions = DTCoreTextLayoutFrameDrawingOptions(rawValue: 1)! { didSet { setNeedsDisplay() } } /// 用于高亮绘制的 attributed string(包含 com.rdreader.highlight 等自定义属性) var attributedDisplayContent: NSAttributedString? { didSet { setNeedsDisplay() } } // MARK: - 选区绘制(对齐 WXRead 的 _drawSelectionInContext:) /// 选区矩形数组,由 RDEPUBTextSelectionController 设置 var selectionRects: [CGRect] = [] { didSet { setNeedsDisplay() } } /// 选区颜色(蓝色半透明,对齐 WXRead) var selectionColor: UIColor = UIColor(red: 70 / 255, green: 140 / 255, blue: 1, alpha: 0.24) // MARK: - Init override init(frame: CGRect) { super.init(frame: frame) backgroundColor = .clear isOpaque = false contentMode = .redraw } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: - Draw override func draw(_ rect: CGRect) { guard let context = UIGraphicsGetCurrentContext(), let layoutFrame else { return } context.saveGState() // 1. 绘制高亮背景(在文字下方,对齐 WXRead 的 drawHighlightsInContext:) if let attributedDisplayContent { drawHighlights(in: context, attributedString: attributedDisplayContent, layoutFrame: layoutFrame) } // 2. 绘制文字 layoutFrame.draw(in: context, options: drawOptions) // 3. 绘制选区(在文字上方) drawSelection(in: context) context.restoreGState() } // MARK: - 高亮绘制(对齐 WXRead 的 WRCoreTextLayoutFrame.drawHighlightsInContext:) private func drawHighlights( in context: CGContext, attributedString: NSAttributedString, layoutFrame: DTCoreTextLayoutFrame ) { let fullRange = NSRange(location: 0, length: attributedString.length) // 绘制高亮背景 attributedString.enumerateAttribute(kRDEPUBHighlightAttributeName, in: fullRange) { value, range, _ in guard let color = value as? UIColor else { return } let rects = computeHighlightRects(for: range, layoutFrame: layoutFrame) color.setFill() for rect in rects { context.fill(rect) } } // 绘制下划线 attributedString.enumerateAttribute(kRDEPUBUnderlineAttributeName, in: fullRange) { value, range, _ in guard let color = value as? UIColor else { return } let rects = computeHighlightRects(for: range, layoutFrame: layoutFrame) color.setStroke() context.setLineWidth(2) for rect in rects { let y = rect.maxY - 1 context.move(to: CGPoint(x: rect.minX, y: y)) context.addLine(to: CGPoint(x: rect.maxX, y: y)) context.strokePath() } } } /// 计算指定字符范围的视觉矩形(对齐 WXRead 的 rectsForRange: 算法) private func computeHighlightRects(for range: NSRange, layoutFrame: DTCoreTextLayoutFrame) -> [CGRect] { var rects: [CGRect] = [] guard let lines = layoutFrame.lines as? [DTCoreTextLayoutLine] else { return rects } for line in lines { let lineRange = line.stringRange() let overlap = NSIntersectionRange(range, lineRange) guard overlap.length > 0 else { continue } let startX = line.offset(forStringIndex: overlap.location) let endX = line.offset(forStringIndex: overlap.location + overlap.length) let rect = CGRect( x: line.baselineOrigin.x + startX, y: line.baselineOrigin.y - line.ascent, width: endX - startX, height: line.ascent + line.descent ) rects.append(rect) } return rects } // MARK: - 选区绘制 private func drawSelection(in context: CGContext) { guard !selectionRects.isEmpty else { return } selectionColor.setFill() for rect in selectionRects { context.fill(rect) } } } #endif