ReadViewSDK/Sources/RDReaderView/EPUBUI/TextPage/RDEPUBTextPageRenderView.swift
shenlei c65c190b71 feat: EPUB阅读器搜索、注释、CFI模块及大书远距跳转优化
- 实现EPUB阅读器搜索功能及选中注释功能
- 优化CFI模块,修复代码审查发现的11个问题
- 实现大书远距目录跳转与后台补全优化方案
- 优化设置面板与章节运行时联动
- 重构及大量改进优化
2026-06-22 20:26:34 +08:00

235 lines
7.2 KiB
Swift

import UIKit
#if canImport(DTCoreText)
import DTCoreText
final class RDEPUBTextPageRenderView: UIView {
enum SelectionHandle {
case start
case end
}
var layoutFrame: DTCoreTextLayoutFrame? {
didSet {
setNeedsDisplay()
}
}
var drawOptions: DTCoreTextLayoutFrameDrawingOptions = DTCoreTextLayoutFrameDrawingOptions(rawValue: 1)! {
didSet {
setNeedsDisplay()
}
}
var attributedDisplayContent: NSAttributedString? {
didSet {
setNeedsDisplay()
}
}
var selectionRects: [CGRect] = [] {
didSet {
setNeedsDisplay()
}
}
var selectionColor: UIColor = UIColor(red: 70 / 255, green: 140 / 255, blue: 1, alpha: 0.24)
private let selectionHandleColor = UIColor(red: 20 / 255, green: 122 / 255, blue: 1, alpha: 1)
private let selectionHandleStemWidth: CGFloat = 2.5
private let selectionHandleKnobRadius: CGFloat = 7
private let selectionHandleHitSlop: CGFloat = 20
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")
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext(),
let layoutFrame else { return }
context.saveGState()
if let attributedDisplayContent {
drawHighlights(in: context, attributedString: attributedDisplayContent, layoutFrame: layoutFrame)
}
layoutFrame.draw(in: context, options: drawOptions)
drawSelection(in: context)
context.restoreGState()
}
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()
}
}
}
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
}
private func drawSelection(in context: CGContext) {
guard !selectionRects.isEmpty else { return }
selectionColor.setFill()
for rect in selectionRects {
context.fill(rect)
}
drawSelectionHandles(in: context)
}
func selectionHandle(at point: CGPoint) -> SelectionHandle? {
guard let handleGeometry = selectionHandleGeometry else { return nil }
let startDistance = point.distance(to: handleGeometry.startKnobCenter)
let endDistance = point.distance(to: handleGeometry.endKnobCenter)
let maxDistance = selectionHandleKnobRadius + selectionHandleHitSlop
let startMatched = startDistance <= maxDistance
let endMatched = endDistance <= maxDistance
switch (startMatched, endMatched) {
case (true, true):
return startDistance <= endDistance ? .start : .end
case (true, false):
return .start
case (false, true):
return .end
default:
return nil
}
}
func selectionContains(_ point: CGPoint) -> Bool {
selectionRects.contains { rect in
rect.insetBy(dx: -6, dy: -8).contains(point)
}
}
private var selectionHandleGeometry: (startKnobCenter: CGPoint, endKnobCenter: CGPoint)? {
guard let firstRect = selectionRects.first,
let lastRect = selectionRects.last else {
return nil
}
let startKnobCenter = CGPoint(
x: firstRect.minX,
y: firstRect.minY - selectionHandleKnobRadius
)
let endKnobCenter = CGPoint(
x: lastRect.maxX,
y: lastRect.maxY + selectionHandleKnobRadius
)
return (startKnobCenter: startKnobCenter, endKnobCenter: endKnobCenter)
}
private func drawSelectionHandles(in context: CGContext) {
guard let firstRect = selectionRects.first,
let lastRect = selectionRects.last else {
return
}
context.saveGState()
context.setFillColor(selectionHandleColor.cgColor)
let stemHalfWidth = selectionHandleStemWidth / 2
let startStem = CGRect(
x: firstRect.minX - stemHalfWidth,
y: firstRect.minY - selectionHandleKnobRadius * 2,
width: selectionHandleStemWidth,
height: firstRect.height + selectionHandleKnobRadius * 2
)
context.fill(startStem)
let startKnob = CGRect(
x: firstRect.minX - selectionHandleKnobRadius,
y: firstRect.minY - selectionHandleKnobRadius * 2,
width: selectionHandleKnobRadius * 2,
height: selectionHandleKnobRadius * 2
)
context.fillEllipse(in: startKnob)
let endStem = CGRect(
x: lastRect.maxX - stemHalfWidth,
y: lastRect.minY,
width: selectionHandleStemWidth,
height: lastRect.height + selectionHandleKnobRadius * 2
)
context.fill(endStem)
let endKnob = CGRect(
x: lastRect.maxX - selectionHandleKnobRadius,
y: lastRect.maxY,
width: selectionHandleKnobRadius * 2,
height: selectionHandleKnobRadius * 2
)
context.fillEllipse(in: endKnob)
context.restoreGState()
}
}
private extension CGPoint {
func distance(to point: CGPoint) -> CGFloat {
hypot(x - point.x, y - point.y)
}
}
#endif