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>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
948004eed1
commit
70133e4e6e
@@ -5,6 +5,7 @@ import DTCoreText
|
||||
|
||||
/// 基于 DTCoreText 的 Core Text 直接绘制视图
|
||||
/// 将 DTCoreText 的排版结果直接绘制到 UIView 上,跳过 UITextView 的间接渲染。
|
||||
/// 对齐 WXRead 的 WRPageView:在同一 drawRect 中绘制高亮背景、文字和选区。
|
||||
final class RDEPUBTextPageRenderView: UIView {
|
||||
var layoutFrame: DTCoreTextLayoutFrame? {
|
||||
didSet {
|
||||
@@ -18,6 +19,27 @@ final class RDEPUBTextPageRenderView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
/// 用于高亮绘制的 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
|
||||
@@ -29,13 +51,94 @@ final class RDEPUBTextPageRenderView: UIView {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user