- 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
214 lines
6.3 KiB
Swift
214 lines
6.3 KiB
Swift
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
|