feat(epub): align wxread css and overlay pagination behavior
This commit is contained in:
@@ -679,8 +679,9 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
|
||||
if publication.readingProfile == .textReflowable {
|
||||
let renderer = resolvedTextRenderer()
|
||||
let builder = RDEPUBTextBookBuilder(renderer: renderer, cache: textBookCache)
|
||||
let pageSize = currentTextPageSize()
|
||||
let layoutConfig = currentTextLayoutConfig(pageSize: pageSize)
|
||||
let builder = RDEPUBTextBookBuilder(renderer: renderer, cache: textBookCache, layoutConfig: layoutConfig)
|
||||
let renderStyle = currentTextRenderStyle()
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
||||
@@ -913,7 +914,7 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
let restoreLocation = currentVisibleLocation() ?? persistenceLocation()
|
||||
let pageSize = currentTextPageSize()
|
||||
let style = currentTextRenderStyle()
|
||||
let builder = RDPlainTextBookBuilder()
|
||||
let builder = RDPlainTextBookBuilder(layoutConfig: currentTextLayoutConfig(pageSize: pageSize))
|
||||
if let newBook = try? builder.build(textFileURL: textFileURL, pageSize: pageSize, style: style) {
|
||||
applyTextBook(newBook, restoreLocation: restoreLocation)
|
||||
}
|
||||
@@ -1231,6 +1232,26 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
)
|
||||
}
|
||||
|
||||
private func currentTextLayoutConfig(pageSize: CGSize) -> RDEPUBTextLayoutConfig {
|
||||
let viewportSize = currentLayoutContext().viewportSize
|
||||
let derivedFrameSize = CGSize(
|
||||
width: max(viewportSize.width, pageSize.width),
|
||||
height: max(viewportSize.height, pageSize.height)
|
||||
)
|
||||
return RDEPUBTextLayoutConfig(
|
||||
frameWidth: derivedFrameSize.width,
|
||||
frameHeight: derivedFrameSize.height,
|
||||
edgeInsets: .zero,
|
||||
numberOfColumns: 1,
|
||||
columnGap: 20,
|
||||
avoidOrphans: true,
|
||||
avoidWidows: true,
|
||||
avoidPageBreakInsideEnabled: true,
|
||||
hyphenation: true,
|
||||
imageMaxHeightRatio: 0.85
|
||||
)
|
||||
}
|
||||
|
||||
private func resolvedTextRenderer() -> RDEPUBTextRenderer {
|
||||
RDEPUBDTCoreTextRenderer()
|
||||
}
|
||||
|
||||
@@ -445,12 +445,12 @@ final class RDEPUBTextContentView: UIView {
|
||||
case .highlight:
|
||||
content.addAttribute(
|
||||
.backgroundColor,
|
||||
value: UIColor(hexString: highlight.color, alpha: 0.45) ?? UIColor(red: 248 / 255, green: 225 / 255, blue: 108 / 255, alpha: 0.45),
|
||||
value: UIColor(rdHexString: highlight.color, alpha: 0.45) ?? UIColor(red: 248 / 255, green: 225 / 255, blue: 108 / 255, alpha: 0.45),
|
||||
range: relativeRange
|
||||
)
|
||||
case .underline:
|
||||
content.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: relativeRange)
|
||||
if let color = UIColor(hexString: highlight.color, alpha: 1) {
|
||||
if let color = UIColor(rdHexString: highlight.color, alpha: 1) {
|
||||
content.addAttribute(.underlineColor, value: color, range: relativeRange)
|
||||
}
|
||||
}
|
||||
@@ -545,7 +545,7 @@ final class RDEPUBTextContentView: UIView {
|
||||
let rects = interactionController.selectionRects(for: absoluteRange)
|
||||
guard !rects.isEmpty else { continue }
|
||||
|
||||
let color = UIColor(hexString: highlight.color, alpha: 0.45)
|
||||
let color = UIColor(rdHexString: highlight.color, alpha: 0.45)
|
||||
?? UIColor(red: 248 / 255, green: 225 / 255, blue: 108 / 255, alpha: 0.45)
|
||||
let decoration = RDEPUBTextOverlayDecoration(
|
||||
kind: highlight.style == .underline ? .underline : .highlight,
|
||||
@@ -712,17 +712,3 @@ extension RDEPUBTextContentView: UITextViewDelegate {
|
||||
delegate?.textContentView(self, didChangeSelection: selection)
|
||||
}
|
||||
}
|
||||
|
||||
private extension UIColor {
|
||||
convenience init?(hexString: String, alpha: CGFloat) {
|
||||
var value = hexString.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
value = value.replacingOccurrences(of: "#", with: "")
|
||||
guard value.count == 6, let hex = Int(value, radix: 16) else { return nil }
|
||||
self.init(
|
||||
red: CGFloat((hex >> 16) & 0xFF) / 255,
|
||||
green: CGFloat((hex >> 8) & 0xFF) / 255,
|
||||
blue: CGFloat(hex & 0xFF) / 255,
|
||||
alpha: alpha
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ final class RDEPUBWebContentView: UIView {
|
||||
weak var delegate: RDEPUBWebContentViewDelegate?
|
||||
|
||||
private let epubWebView = RDEPUBWebView()
|
||||
private let decorationOverlayView = RDEPUBWebDecorationOverlayView()
|
||||
private let pageNumberLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = UIFont.systemFont(ofSize: 13)
|
||||
@@ -40,8 +41,12 @@ final class RDEPUBWebContentView: UIView {
|
||||
layer.masksToBounds = true
|
||||
|
||||
addSubview(epubWebView)
|
||||
addSubview(decorationOverlayView)
|
||||
addSubview(pageNumberLabel)
|
||||
epubWebView.delegate = self
|
||||
epubWebView.onDecorationsResolved = { [weak self] decorations in
|
||||
self?.decorationOverlayView.applyDecorations(decorations)
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@@ -51,6 +56,7 @@ final class RDEPUBWebContentView: UIView {
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
epubWebView.frame = bounds
|
||||
decorationOverlayView.frame = bounds
|
||||
|
||||
let labelSize = pageNumberLabel.sizeThatFits(CGSize(width: bounds.width, height: 20))
|
||||
pageNumberLabel.frame = CGRect(
|
||||
@@ -71,13 +77,18 @@ final class RDEPUBWebContentView: UIView {
|
||||
backgroundColor = theme.contentBackgroundColor
|
||||
pageNumberLabel.textColor = theme.contentTextColor
|
||||
pageNumberLabel.text = "\(pageNumber) / \(totalPages)"
|
||||
decorationOverlayView.applyDecorations([])
|
||||
epubWebView.load(publication: publication, request: request)
|
||||
setNeedsLayout()
|
||||
}
|
||||
|
||||
func releaseResources() {
|
||||
decorationOverlayView.applyDecorations([])
|
||||
epubWebView.reset()
|
||||
epubWebView.delegate = self
|
||||
epubWebView.onDecorationsResolved = { [weak self] decorations in
|
||||
self?.decorationOverlayView.applyDecorations(decorations)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +116,5 @@ extension RDEPUBWebContentView: RDEPUBWebViewDelegate {
|
||||
func epubWebView(_ webView: RDEPUBWebView, didLogJavaScriptError message: String) {
|
||||
delegate?.epubWebContentView(self, didLogJavaScriptError: message)
|
||||
}
|
||||
|
||||
func epubWebViewDidFinishRendering(_ webView: RDEPUBWebView) {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import UIKit
|
||||
|
||||
/// WebView 路径的原生装饰覆盖层。
|
||||
/// 对标 WXRead 的页面级装饰绘制思路:JS 只负责提供 rect,实际视觉绘制由原生 CGContext 完成。
|
||||
final class RDEPUBWebDecorationOverlayView: UIView {
|
||||
private var decorations: [RDEPUBTextOverlayDecoration] = []
|
||||
private let verticalAdjustment: CGFloat = -1
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .clear
|
||||
isOpaque = false
|
||||
isUserInteractionEnabled = false
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func applyDecorations(_ decorations: [RDEPUBTextOverlayDecoration]) {
|
||||
self.decorations = decorations.filter { !$0.rects.isEmpty }
|
||||
setNeedsDisplay()
|
||||
}
|
||||
|
||||
override func draw(_ rect: CGRect) {
|
||||
guard let context = UIGraphicsGetCurrentContext() else { return }
|
||||
|
||||
for decoration in decorations {
|
||||
switch decoration.kind {
|
||||
case .underline:
|
||||
context.setStrokeColor(decoration.color.cgColor)
|
||||
context.setLineWidth(2)
|
||||
for underlineRect in decoration.rects {
|
||||
let y = underlineRect.maxY - 1
|
||||
context.move(to: CGPoint(x: underlineRect.minX, y: y))
|
||||
context.addLine(to: CGPoint(x: underlineRect.maxX, y: y))
|
||||
context.strokePath()
|
||||
}
|
||||
default:
|
||||
context.setFillColor(decoration.color.cgColor)
|
||||
for selectionRect in decoration.rects {
|
||||
let adjustedRect = selectionRect.offsetBy(dx: 0, dy: verticalAdjustment)
|
||||
let path = UIBezierPath(roundedRect: adjustedRect.insetBy(dx: -1, dy: -1), cornerRadius: 4)
|
||||
context.addPath(path.cgPath)
|
||||
context.fillPath()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,7 +119,20 @@ public final class RDURLReaderController: UIViewController {
|
||||
let bookTitle = bookURL.deletingPathExtension().lastPathComponent
|
||||
let pageSize = currentTextPageSize()
|
||||
let renderStyle = currentTextRenderStyle()
|
||||
let builder = RDPlainTextBookBuilder()
|
||||
let builder = RDPlainTextBookBuilder(
|
||||
layoutConfig: RDEPUBTextLayoutConfig(
|
||||
frameWidth: pageSize.width,
|
||||
frameHeight: pageSize.height,
|
||||
edgeInsets: .zero,
|
||||
numberOfColumns: 1,
|
||||
columnGap: 20,
|
||||
avoidOrphans: true,
|
||||
avoidWidows: true,
|
||||
avoidPageBreakInsideEnabled: true,
|
||||
hyphenation: true,
|
||||
imageMaxHeightRatio: 0.85
|
||||
)
|
||||
)
|
||||
if let textBook = try? builder.build(textFileURL: bookURL, pageSize: pageSize, style: renderStyle) {
|
||||
controller = RDEPUBReaderController(
|
||||
textBook: textBook,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import UIKit
|
||||
|
||||
extension UIColor {
|
||||
convenience init?(rdHexString: String, alpha: CGFloat) {
|
||||
var value = rdHexString.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
value = value.replacingOccurrences(of: "#", with: "")
|
||||
guard value.count == 6, let hex = Int(value, radix: 16) else { return nil }
|
||||
self.init(
|
||||
red: CGFloat((hex >> 16) & 0xFF) / 255,
|
||||
green: CGFloat((hex >> 8) & 0xFF) / 255,
|
||||
blue: CGFloat(hex & 0xFF) / 255,
|
||||
alpha: alpha
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user