Files
ReadViewSDK/Sources/RDReaderView/EPUBUI/TextPage/RDEPUBTextPageRenderView.swift
T
shenleiandClaude Opus 4.7 70133e4e6e refactor(reader): 重构高亮选区绘制架构,对齐 WXRead 实现方案
- 移除 RDEPUBSelectableTextView,改用原生 UITextView
- 新增 NSAttributedString 自定义属性(com.rdreader.highlight/underline)注入高亮
- RDEPUBTextPageRenderView 统一绘制高亮背景、文字和选区
- RDEPUBTextSelectionController 精简,选区矩形传递给 RenderView 绘制
- 新增高亮选区复刻 WXRead 实现方案文档
- UI 测试适配新架构

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 20:57:27 +08:00

145 lines
4.7 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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