- 移除 RDEPUBSelectableTextView,改用原生 UITextView - 新增 NSAttributedString 自定义属性(com.rdreader.highlight/underline)注入高亮 - RDEPUBTextPageRenderView 统一绘制高亮背景、文字和选区 - RDEPUBTextSelectionController 精简,选区矩形传递给 RenderView 绘制 - 新增高亮选区复刻 WXRead 实现方案文档 - UI 测试适配新架构 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
145 lines
4.7 KiB
Swift
145 lines
4.7 KiB
Swift
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
|