refactor: rename RDReaderView -> RDEpubReaderView, update pod config and docs
- Rename source module from RDReaderView to RDEpubReaderView - Move all source files from Sources/RDReaderView/ to Sources/RDEpubReaderView/ - Update podspec: RDReaderView.podspec -> RDEpubReaderView.podspec - Update Podfile, demo project, and CocoaPods config for new pod name - Delete old RDReaderView pod support files from ReadViewDemo/Pods - Add new RDEpubReaderView pod support files - Update documentation (API ref, architecture, UML, conventions, etc.) - Add FixedLayoutRotationTests - Update .gitignore: exclude .DS_Store, manual unpack backups, _ssoft-output
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
import UIKit
|
||||
|
||||
#if canImport(DTCoreText)
|
||||
import DTCoreText
|
||||
|
||||
final class RDEPUBTextPageRenderView: UIView {
|
||||
|
||||
enum SelectionHandle {
|
||||
case start
|
||||
case end
|
||||
}
|
||||
|
||||
var layoutFrame: DTCoreTextLayoutFrame? {
|
||||
didSet {
|
||||
invalidateStaticContent()
|
||||
}
|
||||
}
|
||||
|
||||
var drawOptions: DTCoreTextLayoutFrameDrawingOptions = DTCoreTextLayoutFrameDrawingOptions(rawValue: 1)! {
|
||||
didSet {
|
||||
invalidateStaticContent()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
private var cachedStaticImage: UIImage?
|
||||
|
||||
private var cachedStaticBoundsSize: CGSize = .zero
|
||||
|
||||
private var needsStaticContentRedraw = true
|
||||
|
||||
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 cachedStaticImage == nil
|
||||
|| cachedStaticBoundsSize != bounds.size
|
||||
|| needsStaticContentRedraw {
|
||||
cachedStaticImage = renderStaticImage(layoutFrame: layoutFrame)
|
||||
cachedStaticBoundsSize = bounds.size
|
||||
needsStaticContentRedraw = false
|
||||
}
|
||||
|
||||
if let cachedStaticImage {
|
||||
cachedStaticImage.draw(in: bounds)
|
||||
} else {
|
||||
layoutFrame.draw(in: context, options: drawOptions)
|
||||
}
|
||||
|
||||
drawSelection(in: context)
|
||||
|
||||
context.restoreGState()
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
if cachedStaticBoundsSize != bounds.size {
|
||||
invalidateStaticContent()
|
||||
}
|
||||
}
|
||||
|
||||
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 func invalidateStaticContent() {
|
||||
cachedStaticImage = nil
|
||||
needsStaticContentRedraw = true
|
||||
setNeedsDisplay()
|
||||
}
|
||||
|
||||
private func renderStaticImage(layoutFrame: DTCoreTextLayoutFrame) -> UIImage? {
|
||||
guard bounds.width > 0, bounds.height > 0 else { return nil }
|
||||
let format = UIGraphicsImageRendererFormat.default()
|
||||
format.opaque = false
|
||||
let renderer = UIGraphicsImageRenderer(size: bounds.size, format: format)
|
||||
return renderer.image { _ in
|
||||
guard let staticContext = UIGraphicsGetCurrentContext() else { return }
|
||||
layoutFrame.draw(in: staticContext, options: drawOptions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension CGPoint {
|
||||
|
||||
func distance(to point: CGPoint) -> CGFloat {
|
||||
hypot(x - point.x, y - point.y)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user