修改分页问题
This commit is contained in:
parent
0e7d952fe5
commit
83b705b9ae
@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
| 文档 | 说明 |
|
| 文档 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| [ReflowableEPUB_WXReadRenderer_Design.md](FeatureSolution/ReflowableEPUB_WXReadRenderer_Design.md) | 基于读书反编译的 CoreText 渲染架构,设计 Reflowable EPUB 的增强文本渲染方案(CSS 分层、类型器升级) |
|
| [ReflowableEPUB_WXReadRenderer_Design.md](FeatureSolution/ReflowableEPUB_WXReadRenderer_Design.md) | 基于读书的 CoreText 渲染架构,设计 Reflowable EPUB 的增强文本渲染方案(CSS 分层、类型器升级) |
|
||||||
|
|
||||||
## 目录约定
|
## 目录约定
|
||||||
|
|
||||||
|
|||||||
@ -185,10 +185,14 @@
|
|||||||
inputFileListPaths = (
|
inputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
name = "[CP] Embed Pods Frameworks";
|
name = "[CP] Embed Pods Frameworks";
|
||||||
outputFileListPaths = (
|
outputFileListPaths = (
|
||||||
"${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
"${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
);
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks.sh\"\n";
|
||||||
|
|||||||
Binary file not shown.
@ -104,7 +104,8 @@ public struct RDEPUBDTCoreTextRenderer: RDEPUBTextRenderer {
|
|||||||
guard let element else { return }
|
guard let element else { return }
|
||||||
RDEPUBTextRendererSupport.prepareHTMLElementForReaderRendering(
|
RDEPUBTextRendererSupport.prepareHTMLElementForReaderRendering(
|
||||||
element,
|
element,
|
||||||
style: request.style
|
style: request.style,
|
||||||
|
maxImageSize: resolvedMaxImageSize(for: request)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return builder?.generatedAttributedString()
|
return builder?.generatedAttributedString()
|
||||||
@ -116,7 +117,7 @@ public struct RDEPUBDTCoreTextRenderer: RDEPUBTextRenderer {
|
|||||||
/// 确保最终行距与用户设置的 style.lineSpacing 一致。
|
/// 确保最终行距与用户设置的 style.lineSpacing 一致。
|
||||||
private func dtOptions(request: RDEPUBTextChapterRenderRequest) -> [AnyHashable: Any] {
|
private func dtOptions(request: RDEPUBTextChapterRenderRequest) -> [AnyHashable: Any] {
|
||||||
let style = request.style
|
let style = request.style
|
||||||
let screenBounds = UIScreen.main.bounds.insetBy(dx: 20, dy: 28)
|
let maxImageSize = resolvedMaxImageSize(for: request)
|
||||||
var options: [AnyHashable: Any] = [
|
var options: [AnyHashable: Any] = [
|
||||||
NSTextSizeMultiplierDocumentOption: 1.0,
|
NSTextSizeMultiplierDocumentOption: 1.0,
|
||||||
DTDefaultFontFamily: style.font.familyName,
|
DTDefaultFontFamily: style.font.familyName,
|
||||||
@ -124,7 +125,7 @@ public struct RDEPUBDTCoreTextRenderer: RDEPUBTextRenderer {
|
|||||||
DTDefaultFontSize: style.font.pointSize,
|
DTDefaultFontSize: style.font.pointSize,
|
||||||
DTDefaultLineHeightMultiplier: max((style.font.lineHeight + style.lineSpacing) / max(style.font.lineHeight, 1), 1),
|
DTDefaultLineHeightMultiplier: max((style.font.lineHeight + style.lineSpacing) / max(style.font.lineHeight, 1), 1),
|
||||||
DTUseiOS6Attributes: true,
|
DTUseiOS6Attributes: true,
|
||||||
DTMaxImageSize: NSValue(cgSize: screenBounds.size)
|
DTMaxImageSize: NSValue(cgSize: maxImageSize)
|
||||||
]
|
]
|
||||||
|
|
||||||
if let baseURL = request.context.baseURL {
|
if let baseURL = request.context.baseURL {
|
||||||
@ -136,5 +137,18 @@ public struct RDEPUBDTCoreTextRenderer: RDEPUBTextRenderer {
|
|||||||
|
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func resolvedMaxImageSize(for request: RDEPUBTextChapterRenderRequest) -> CGSize {
|
||||||
|
if let pageSize = request.pageSize {
|
||||||
|
let layoutConfig = request.layoutConfig ?? .default
|
||||||
|
let contentRect = layoutConfig.contentRect(fallback: pageSize)
|
||||||
|
let maxWidth = max(round(contentRect.width), 1)
|
||||||
|
let maxHeight = max(round(contentRect.height * layoutConfig.imageMaxHeightRatio), 1)
|
||||||
|
return CGSize(width: maxWidth, height: maxHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
let screenBounds = UIScreen.main.bounds.insetBy(dx: 20, dy: 28)
|
||||||
|
return CGSize(width: max(round(screenBounds.width), 1), height: max(round(screenBounds.height * 0.85), 1))
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@ -293,7 +293,9 @@ public final class RDEPUBTextBookBuilder {
|
|||||||
baseURL: parser.fileURL(forRelativePath: item.href)?.deletingLastPathComponent(),
|
baseURL: parser.fileURL(forRelativePath: item.href)?.deletingLastPathComponent(),
|
||||||
style: style,
|
style: style,
|
||||||
resourceResolver: publication.resourceResolver,
|
resourceResolver: publication.resourceResolver,
|
||||||
contentLanguageCode: publication.metadata.language
|
contentLanguageCode: publication.metadata.language,
|
||||||
|
pageSize: pageSize,
|
||||||
|
layoutConfig: layoutConfig
|
||||||
)
|
)
|
||||||
|
|
||||||
// 渲染 HTML → NSAttributedString
|
// 渲染 HTML → NSAttributedString
|
||||||
|
|||||||
@ -123,7 +123,7 @@ final class ChapterPaginationArchive: NSObject, NSSecureCoding {
|
|||||||
public final class RDEPUBTextBookCache {
|
public final class RDEPUBTextBookCache {
|
||||||
|
|
||||||
/// 缓存模式版本号,变更时旧缓存自动失效
|
/// 缓存模式版本号,变更时旧缓存自动失效
|
||||||
public var schemaVersion: Int = 1
|
public var schemaVersion: Int = 4
|
||||||
|
|
||||||
/// 串行队列,保证缓存读写的线程安全
|
/// 串行队列,保证缓存读写的线程安全
|
||||||
private let queue = DispatchQueue(label: "com.rdreader.textbookcache", qos: .utility)
|
private let queue = DispatchQueue(label: "com.rdreader.textbookcache", qos: .utility)
|
||||||
|
|||||||
@ -155,27 +155,31 @@ struct RDEPUBTextLayouter {
|
|||||||
totalLength: attributedString.length,
|
totalLength: attributedString.length,
|
||||||
lineRanges: lineRanges
|
lineRanges: lineRanges
|
||||||
)
|
)
|
||||||
|
let verifiedRange = verifiedDisplayRange(for: adjusted.range)
|
||||||
let trailingFragmentID = nearestTrailingFragmentID(
|
let trailingFragmentID = nearestTrailingFragmentID(
|
||||||
endingAt: adjusted.range.location + adjusted.range.length,
|
endingAt: verifiedRange.location + verifiedRange.length,
|
||||||
fragmentOffsets: fragmentOffsets
|
fragmentOffsets: fragmentOffsets
|
||||||
)
|
)
|
||||||
|
let diagnostics = verifiedRange == adjusted.range
|
||||||
|
? adjusted.diagnostics
|
||||||
|
: adjusted.diagnostics + ["verified-display-range \(NSStringFromRange(adjusted.range)) -> \(NSStringFromRange(verifiedRange))"]
|
||||||
|
|
||||||
frames.append(
|
frames.append(
|
||||||
RDEPUBTextLayoutFrame(
|
RDEPUBTextLayoutFrame(
|
||||||
contentRange: adjusted.range,
|
contentRange: verifiedRange,
|
||||||
breakReason: adjusted.breakReason,
|
breakReason: adjusted.breakReason,
|
||||||
blockRange: adjusted.blockRange,
|
blockRange: blockRange(at: max(verifiedRange.location, verifiedRange.location + verifiedRange.length - 1)),
|
||||||
attachmentRanges: adjusted.attachmentRanges,
|
attachmentRanges: attachmentRanges(in: verifiedRange),
|
||||||
attachmentKinds: adjusted.attachmentKinds,
|
attachmentKinds: attachmentKinds(in: verifiedRange),
|
||||||
blockKinds: adjusted.blockKinds,
|
blockKinds: blockKinds(in: verifiedRange),
|
||||||
semanticHints: adjusted.semanticHints,
|
semanticHints: semanticHints(in: verifiedRange),
|
||||||
attachmentPlacements: adjusted.attachmentPlacements,
|
attachmentPlacements: attachmentPlacements(in: verifiedRange),
|
||||||
trailingFragmentID: trailingFragmentID,
|
trailingFragmentID: trailingFragmentID,
|
||||||
diagnostics: adjusted.diagnostics
|
diagnostics: diagnostics
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
let nextLocation = adjusted.range.location + adjusted.range.length
|
let nextLocation = verifiedRange.location + verifiedRange.length
|
||||||
guard nextLocation > location else {
|
guard nextLocation > location else {
|
||||||
location += max(visibleRange.length, 1)
|
location += max(visibleRange.length, 1)
|
||||||
continue
|
continue
|
||||||
@ -185,6 +189,30 @@ struct RDEPUBTextLayouter {
|
|||||||
|
|
||||||
return frames
|
return frames
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func verifiedDisplayRange(for range: NSRange) -> NSRange {
|
||||||
|
guard range.length > 0,
|
||||||
|
!attachmentRanges(in: range).isEmpty else {
|
||||||
|
return range
|
||||||
|
}
|
||||||
|
|
||||||
|
let pageContent = attributedString.attributedSubstring(from: range)
|
||||||
|
guard let layouter = DTCoreTextLayouter(attributedString: pageContent) else {
|
||||||
|
return range
|
||||||
|
}
|
||||||
|
|
||||||
|
layouter.shouldCacheLayoutFrames = false
|
||||||
|
guard let layoutFrame = layouter.layoutFrame(with: dtLayoutRect, range: NSRange(location: 0, length: 0)) else {
|
||||||
|
return range
|
||||||
|
}
|
||||||
|
|
||||||
|
let visibleRange = layoutFrame.visibleStringRange()
|
||||||
|
guard visibleRange.length > 0, visibleRange.length < range.length else {
|
||||||
|
return range
|
||||||
|
}
|
||||||
|
|
||||||
|
return NSRange(location: range.location, length: visibleRange.length)
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
private static func makeLayoutPath(pageSize: CGSize, config: RDEPUBTextLayoutConfig) -> CGPath {
|
private static func makeLayoutPath(pageSize: CGSize, config: RDEPUBTextLayoutConfig) -> CGPath {
|
||||||
|
|||||||
@ -286,10 +286,21 @@ public struct RDEPUBTextChapterContext: Equatable {
|
|||||||
public struct RDEPUBTextChapterRenderRequest {
|
public struct RDEPUBTextChapterRenderRequest {
|
||||||
public var context: RDEPUBTextChapterContext
|
public var context: RDEPUBTextChapterContext
|
||||||
public var style: RDEPUBTextRenderStyle
|
public var style: RDEPUBTextRenderStyle
|
||||||
|
/// 当前章节将被分页到的页面尺寸;用于让附件缩放贴近真实 page size。
|
||||||
|
public var pageSize: CGSize?
|
||||||
|
/// 当前分页布局配置;用于生成与 WXRead 更接近的内容区尺寸。
|
||||||
|
public var layoutConfig: RDEPUBTextLayoutConfig?
|
||||||
|
|
||||||
public init(context: RDEPUBTextChapterContext, style: RDEPUBTextRenderStyle) {
|
public init(
|
||||||
|
context: RDEPUBTextChapterContext,
|
||||||
|
style: RDEPUBTextRenderStyle,
|
||||||
|
pageSize: CGSize? = nil,
|
||||||
|
layoutConfig: RDEPUBTextLayoutConfig? = nil
|
||||||
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.style = style
|
self.style = style
|
||||||
|
self.pageSize = pageSize
|
||||||
|
self.layoutConfig = layoutConfig
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -197,7 +197,9 @@ enum RDEPUBTextRendererSupport {
|
|||||||
baseURL: URL?,
|
baseURL: URL?,
|
||||||
style: RDEPUBTextRenderStyle,
|
style: RDEPUBTextRenderStyle,
|
||||||
resourceResolver: RDEPUBResourceResolver?,
|
resourceResolver: RDEPUBResourceResolver?,
|
||||||
contentLanguageCode: String? = nil
|
contentLanguageCode: String? = nil,
|
||||||
|
pageSize: CGSize? = nil,
|
||||||
|
layoutConfig: RDEPUBTextLayoutConfig? = nil
|
||||||
) -> RDEPUBTextChapterRenderRequest {
|
) -> RDEPUBTextChapterRenderRequest {
|
||||||
let normalizedHTML = injectPaginationSemanticMarkers(into: normalizeHTML(rawHTML))
|
let normalizedHTML = injectPaginationSemanticMarkers(into: normalizeHTML(rawHTML))
|
||||||
let stylesheetHrefReplacements = inlineLinkedStyleSheets(
|
let stylesheetHrefReplacements = inlineLinkedStyleSheets(
|
||||||
@ -255,7 +257,12 @@ enum RDEPUBTextRendererSupport {
|
|||||||
stylesheet: RDEPUBTextStyleSheetPackage(layers: layers),
|
stylesheet: RDEPUBTextStyleSheetPackage(layers: layers),
|
||||||
resourceDiagnostics: resourceDiagnostics
|
resourceDiagnostics: resourceDiagnostics
|
||||||
)
|
)
|
||||||
return RDEPUBTextChapterRenderRequest(context: context, style: style)
|
return RDEPUBTextChapterRenderRequest(
|
||||||
|
context: context,
|
||||||
|
style: style,
|
||||||
|
pageSize: pageSize,
|
||||||
|
layoutConfig: layoutConfig
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// HTML 规范化:清理冗余字符(CR、多余换行),规范化附件 HTML 标记
|
/// HTML 规范化:清理冗余字符(CR、多余换行),规范化附件 HTML 标记
|
||||||
@ -285,15 +292,20 @@ enum RDEPUBTextRendererSupport {
|
|||||||
#if canImport(DTCoreText)
|
#if canImport(DTCoreText)
|
||||||
static func prepareHTMLElementForReaderRendering(
|
static func prepareHTMLElementForReaderRendering(
|
||||||
_ element: DTHTMLElement,
|
_ element: DTHTMLElement,
|
||||||
style: RDEPUBTextRenderStyle
|
style: RDEPUBTextRenderStyle,
|
||||||
|
maxImageSize: CGSize? = nil
|
||||||
) {
|
) {
|
||||||
guard let attachment = element.textAttachment else { return }
|
guard let attachment = element.textAttachment else { return }
|
||||||
let pointSize = max(element.fontDescriptor.pointSize, style.font.pointSize)
|
let pointSize = max(element.fontDescriptor.pointSize, style.font.pointSize)
|
||||||
let maxSize = CGSize(
|
let fallbackSize = CGSize(
|
||||||
width: round(UIScreen.main.bounds.insetBy(dx: 20, dy: 28).width),
|
width: round(UIScreen.main.bounds.insetBy(dx: 20, dy: 28).width),
|
||||||
height: round(UIScreen.main.bounds.insetBy(dx: 20, dy: 28).height * 0.85)
|
height: round(UIScreen.main.bounds.insetBy(dx: 20, dy: 28).height * 0.85)
|
||||||
)
|
)
|
||||||
normalizeAttachmentLayoutForWXRead(attachment, fontPointSize: pointSize, maxImageSize: maxSize)
|
normalizeAttachmentLayoutForWXRead(
|
||||||
|
attachment,
|
||||||
|
fontPointSize: pointSize,
|
||||||
|
maxImageSize: maxImageSize ?? fallbackSize
|
||||||
|
)
|
||||||
if isFootnoteAttachment(attachment) {
|
if isFootnoteAttachment(attachment) {
|
||||||
element.displayStyle = .inline
|
element.displayStyle = .inline
|
||||||
} else if isCoverAttachment(attachment) {
|
} else if isCoverAttachment(attachment) {
|
||||||
@ -390,6 +402,38 @@ enum RDEPUBTextRendererSupport {
|
|||||||
private static func normalizeAttachmentHTMLMarkers(in html: String) -> String {
|
private static func normalizeAttachmentHTMLMarkers(in html: String) -> String {
|
||||||
var normalized = html
|
var normalized = html
|
||||||
|
|
||||||
|
if let bodyPicContainerRegex = try? NSRegularExpression(
|
||||||
|
pattern: #"<div\b([^>]*class\s*=\s*["'][^"']*\b(?:qrbodyPic|bodyPic)\b[^"']*["'][^>]*)>([\s\S]*?)</div>"#,
|
||||||
|
options: [.caseInsensitive]
|
||||||
|
) {
|
||||||
|
normalized = replaceMatches(
|
||||||
|
using: bodyPicContainerRegex,
|
||||||
|
in: normalized
|
||||||
|
) { tag in
|
||||||
|
guard let imageTagRegex = try? NSRegularExpression(pattern: #"<img\b[^>]*>"#, options: [.caseInsensitive]) else {
|
||||||
|
return tag
|
||||||
|
}
|
||||||
|
|
||||||
|
return replaceMatches(
|
||||||
|
using: imageTagRegex,
|
||||||
|
in: tag
|
||||||
|
) { imageTag in
|
||||||
|
mergeHTMLAttributes(
|
||||||
|
into: imageTag,
|
||||||
|
requiredClass: "bodyPic",
|
||||||
|
styleFragments: [
|
||||||
|
"wr-vertical-center-style:2",
|
||||||
|
"max-width:100%",
|
||||||
|
"height:auto",
|
||||||
|
"display:block",
|
||||||
|
"margin-left:auto",
|
||||||
|
"margin-right:auto"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let footnoteRegex = try? NSRegularExpression(
|
if let footnoteRegex = try? NSRegularExpression(
|
||||||
pattern: #"<img\b([^>]*class\s*=\s*["'][^"']*\bqqreader-footnote\b[^"']*["'][^>]*)>"#,
|
pattern: #"<img\b([^>]*class\s*=\s*["'][^"']*\bqqreader-footnote\b[^"']*["'][^>]*)>"#,
|
||||||
options: [.caseInsensitive]
|
options: [.caseInsensitive]
|
||||||
|
|||||||
@ -139,9 +139,10 @@ struct RDEPUBPageLayoutSnapshot {
|
|||||||
var lines: [RDEPUBPageLine] = []
|
var lines: [RDEPUBPageLine] = []
|
||||||
var runs: [RDEPUBPageRun] = []
|
var runs: [RDEPUBPageRun] = []
|
||||||
var attachments: [RDEPUBPageAttachment] = []
|
var attachments: [RDEPUBPageAttachment] = []
|
||||||
|
let pageOffset = page.pageStartOffset
|
||||||
|
|
||||||
for dtLine in dtLines {
|
for dtLine in dtLines {
|
||||||
let lineRange = dtLine.stringRange()
|
let lineRange = offset(dtLine.stringRange(), by: pageOffset)
|
||||||
let line = RDEPUBPageLine(
|
let line = RDEPUBPageLine(
|
||||||
stringRange: lineRange,
|
stringRange: lineRange,
|
||||||
frame: dtLine.frame,
|
frame: dtLine.frame,
|
||||||
@ -154,7 +155,7 @@ struct RDEPUBPageLayoutSnapshot {
|
|||||||
|
|
||||||
if let glyphRuns = dtLine.glyphRuns as? [DTCoreTextGlyphRun] {
|
if let glyphRuns = dtLine.glyphRuns as? [DTCoreTextGlyphRun] {
|
||||||
for run in glyphRuns {
|
for run in glyphRuns {
|
||||||
let runRange = run.stringRange()
|
let runRange = offset(run.stringRange(), by: pageOffset)
|
||||||
let isAttachment = run.attachment != nil
|
let isAttachment = run.attachment != nil
|
||||||
runs.append(
|
runs.append(
|
||||||
RDEPUBPageRun(
|
RDEPUBPageRun(
|
||||||
@ -182,7 +183,7 @@ struct RDEPUBPageLayoutSnapshot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let visibleRange = layoutFrame.visibleStringRange()
|
let visibleRange = offset(layoutFrame.visibleStringRange(), by: pageOffset)
|
||||||
|
|
||||||
return RDEPUBPageLayoutSnapshot(
|
return RDEPUBPageLayoutSnapshot(
|
||||||
page: page,
|
page: page,
|
||||||
@ -210,5 +211,12 @@ struct RDEPUBPageLayoutSnapshot {
|
|||||||
: nil
|
: nil
|
||||||
return (placement, kind)
|
return (placement, kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func offset(_ range: NSRange, by offset: Int) -> NSRange {
|
||||||
|
guard range.location != NSNotFound else {
|
||||||
|
return range
|
||||||
|
}
|
||||||
|
return NSRange(location: range.location + offset, length: range.length)
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@ -165,6 +165,8 @@ public final class RDEPUBReaderController: UIViewController {
|
|||||||
private var currentBrightness: CGFloat
|
private var currentBrightness: CGFloat
|
||||||
private var didStartInitialLoad = false
|
private var didStartInitialLoad = false
|
||||||
private var isRepaginating = false
|
private var isRepaginating = false
|
||||||
|
private var lastTextPaginationPageSize: CGSize?
|
||||||
|
private var isReconcilingTextPaginationSize = false
|
||||||
private var paginationToken = UUID()
|
private var paginationToken = UUID()
|
||||||
private var paginator: RDEPUBPaginator?
|
private var paginator: RDEPUBPaginator?
|
||||||
private var searchState: RDEPUBSearchState?
|
private var searchState: RDEPUBSearchState?
|
||||||
@ -695,6 +697,7 @@ public final class RDEPUBReaderController: UIViewController {
|
|||||||
if publication.readingProfile == .textReflowable {
|
if publication.readingProfile == .textReflowable {
|
||||||
let renderer = resolvedTextRenderer()
|
let renderer = resolvedTextRenderer()
|
||||||
let pageSize = currentTextPageSize()
|
let pageSize = currentTextPageSize()
|
||||||
|
lastTextPaginationPageSize = pageSize
|
||||||
let layoutConfig = currentTextLayoutConfig(pageSize: pageSize)
|
let layoutConfig = currentTextLayoutConfig(pageSize: pageSize)
|
||||||
let builder = RDEPUBTextBookBuilder(renderer: renderer, cache: textBookCache, layoutConfig: layoutConfig)
|
let builder = RDEPUBTextBookBuilder(renderer: renderer, cache: textBookCache, layoutConfig: layoutConfig)
|
||||||
let renderStyle = currentTextRenderStyle()
|
let renderStyle = currentTextRenderStyle()
|
||||||
@ -1258,12 +1261,12 @@ public final class RDEPUBReaderController: UIViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func currentTextPageSize() -> CGSize {
|
private func currentTextPageSize() -> CGSize {
|
||||||
let viewportSize = currentLayoutContext().viewportSize
|
let pageNum = readerView.currentPage >= 0 ? readerView.currentPage : nil
|
||||||
let insets = configuration.reflowableContentInsets
|
let resolvedSize = readerView.resolvedSinglePageSize(pageNum: pageNum)
|
||||||
return CGSize(
|
if resolvedSize.width > 0, resolvedSize.height > 0 {
|
||||||
width: max(viewportSize.width - insets.left - insets.right, 1),
|
return resolvedSize
|
||||||
height: max(viewportSize.height - insets.top - insets.bottom, 1)
|
}
|
||||||
)
|
return currentLayoutContext().viewportSize
|
||||||
}
|
}
|
||||||
|
|
||||||
private func currentTextRenderStyle() -> RDEPUBTextRenderStyle {
|
private func currentTextRenderStyle() -> RDEPUBTextRenderStyle {
|
||||||
@ -1278,15 +1281,10 @@ public final class RDEPUBReaderController: UIViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func currentTextLayoutConfig(pageSize: CGSize) -> RDEPUBTextLayoutConfig {
|
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(
|
return RDEPUBTextLayoutConfig(
|
||||||
frameWidth: derivedFrameSize.width,
|
frameWidth: max(pageSize.width, 1),
|
||||||
frameHeight: derivedFrameSize.height,
|
frameHeight: max(pageSize.height, 1),
|
||||||
edgeInsets: .zero,
|
edgeInsets: configuration.reflowableContentInsets,
|
||||||
numberOfColumns: configuration.numberOfColumns,
|
numberOfColumns: configuration.numberOfColumns,
|
||||||
columnGap: configuration.columnGap,
|
columnGap: configuration.columnGap,
|
||||||
avoidOrphans: true,
|
avoidOrphans: true,
|
||||||
@ -1834,6 +1832,7 @@ extension RDEPUBReaderController: RDReaderDataSource, RDReaderDelegate {
|
|||||||
|
|
||||||
public func pageNum(readerView: RDReaderView, pageNum: Int) {
|
public func pageNum(readerView: RDReaderView, pageNum: Int) {
|
||||||
updateCurrentSelection(nil)
|
updateCurrentSelection(nil)
|
||||||
|
reconcileTextPaginationSizeIfNeeded(for: pageNum)
|
||||||
|
|
||||||
let totalPages = pageCountOfReaderView(readerView: readerView)
|
let totalPages = pageCountOfReaderView(readerView: readerView)
|
||||||
if totalPages > 0, pageNum == totalPages - 1 {
|
if totalPages > 0, pageNum == totalPages - 1 {
|
||||||
@ -1859,6 +1858,36 @@ extension RDEPUBReaderController: RDReaderDataSource, RDReaderDelegate {
|
|||||||
_ = isLandscape
|
_ = isLandscape
|
||||||
pendingPresentationRestoreLocation = currentVisibleLocation() ?? persistenceLocation()
|
pendingPresentationRestoreLocation = currentVisibleLocation() ?? persistenceLocation()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func reconcileTextPaginationSizeIfNeeded(for pageNum: Int) {
|
||||||
|
guard textBook != nil,
|
||||||
|
!isRepaginating,
|
||||||
|
!isReconcilingTextPaginationSize,
|
||||||
|
pageNum >= 0,
|
||||||
|
let lastTextPaginationPageSize else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let resolvedSize = readerView.resolvedSinglePageSize(pageNum: pageNum)
|
||||||
|
guard resolvedSize.width > 0,
|
||||||
|
resolvedSize.height > 0 else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let sizeChanged = abs(resolvedSize.width - lastTextPaginationPageSize.width) > 0.5
|
||||||
|
|| abs(resolvedSize.height - lastTextPaginationPageSize.height) > 0.5
|
||||||
|
guard sizeChanged else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isReconcilingTextPaginationSize = true
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
self.isReconcilingTextPaginationSize = false
|
||||||
|
guard self.textBook != nil, !self.isRepaginating else { return }
|
||||||
|
self.repaginatePreservingCurrentLocation()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Web 内容视图代理(EPUB 固定布局/Web 渲染路径)
|
// MARK: - Web 内容视图代理(EPUB 固定布局/Web 渲染路径)
|
||||||
|
|||||||
@ -265,7 +265,7 @@ final class RDEPUBTextContentView: UIView {
|
|||||||
)
|
)
|
||||||
|
|
||||||
#if canImport(DTCoreText)
|
#if canImport(DTCoreText)
|
||||||
let displayContent = NSMutableAttributedString(attributedString: page.chapterContent)
|
let displayContent = NSMutableAttributedString(attributedString: page.content)
|
||||||
let fullRange = NSRange(location: 0, length: displayContent.length)
|
let fullRange = NSRange(location: 0, length: displayContent.length)
|
||||||
displayContent.addAttribute(
|
displayContent.addAttribute(
|
||||||
.foregroundColor,
|
.foregroundColor,
|
||||||
@ -275,7 +275,7 @@ final class RDEPUBTextContentView: UIView {
|
|||||||
coreTextContentView.isHidden = false
|
coreTextContentView.isHidden = false
|
||||||
coreTextContentView.backgroundColor = .clear
|
coreTextContentView.backgroundColor = .clear
|
||||||
coreTextDisplayContent = displayContent
|
coreTextDisplayContent = displayContent
|
||||||
coreTextDisplayRange = page.contentRange
|
coreTextDisplayRange = NSRange(location: 0, length: displayContent.length)
|
||||||
textView.isHidden = true
|
textView.isHidden = true
|
||||||
textView.isUserInteractionEnabled = false
|
textView.isUserInteractionEnabled = false
|
||||||
textView.attributedText = nil
|
textView.attributedText = nil
|
||||||
|
|||||||
@ -123,7 +123,7 @@ public final class RDURLReaderController: UIViewController {
|
|||||||
layoutConfig: RDEPUBTextLayoutConfig(
|
layoutConfig: RDEPUBTextLayoutConfig(
|
||||||
frameWidth: pageSize.width,
|
frameWidth: pageSize.width,
|
||||||
frameHeight: pageSize.height,
|
frameHeight: pageSize.height,
|
||||||
edgeInsets: .zero,
|
edgeInsets: epubConfiguration.reflowableContentInsets,
|
||||||
numberOfColumns: 1,
|
numberOfColumns: 1,
|
||||||
columnGap: 20,
|
columnGap: 20,
|
||||||
avoidOrphans: true,
|
avoidOrphans: true,
|
||||||
@ -179,15 +179,11 @@ public final class RDURLReaderController: UIViewController {
|
|||||||
print("[ReadViewDemo] automation \(prefix) -> page \(page) href \(href) progression \(progression)")
|
print("[ReadViewDemo] automation \(prefix) -> page \(page) href \(href) progression \(progression)")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 计算文本分页的页面尺寸
|
/// 计算文本分页的页面尺寸。
|
||||||
/// 基于屏幕尺寸和配置的内边距
|
/// 分页器使用完整 viewport,真实可绘制区域由 layoutConfig.edgeInsets 控制,
|
||||||
|
/// 以保持与页面展示时的内容几何一致。
|
||||||
private func currentTextPageSize() -> CGSize {
|
private func currentTextPageSize() -> CGSize {
|
||||||
let viewportSize = UIScreen.main.bounds.size
|
UIScreen.main.bounds.size
|
||||||
let insets = epubConfiguration.reflowableContentInsets
|
|
||||||
return CGSize(
|
|
||||||
width: max(viewportSize.width - insets.left - insets.right, 1),
|
|
||||||
height: max(viewportSize.height - insets.top - insets.bottom, 1)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 根据当前配置生成文本渲染样式
|
/// 根据当前配置生成文本渲染样式
|
||||||
|
|||||||
@ -1265,6 +1265,47 @@ extension RDReaderView {
|
|||||||
return cell?.containerView
|
return cell?.containerView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 返回当前阅读模式下单页内容容器的实际尺寸。
|
||||||
|
/// 优先使用已经显示出来的内容视图尺寸;若页面尚未装载,则回退到当前模式下的理论单页尺寸。
|
||||||
|
public func resolvedSinglePageSize(pageNum: Int? = nil) -> CGSize {
|
||||||
|
let targetPage = pageNum ?? (currentPage >= 0 ? currentPage : nil)
|
||||||
|
if let targetPage,
|
||||||
|
let contentView = pageContentView(pageNum: targetPage),
|
||||||
|
contentView.bounds.width > 0,
|
||||||
|
contentView.bounds.height > 0 {
|
||||||
|
return contentView.bounds.size
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentDisplayType == .pageCurl {
|
||||||
|
if let childViewController = pageViewController.viewControllers?.first as? RDReaderPageChildViewController,
|
||||||
|
childViewController.view.bounds.width > 0,
|
||||||
|
childViewController.view.bounds.height > 0 {
|
||||||
|
return childViewController.view.bounds.size
|
||||||
|
}
|
||||||
|
if pageViewController.view.bounds.width > 0, pageViewController.view.bounds.height > 0 {
|
||||||
|
return pageViewController.view.bounds.size
|
||||||
|
}
|
||||||
|
return bounds.size
|
||||||
|
}
|
||||||
|
|
||||||
|
let containerBounds = collectionView.bounds.size == .zero ? bounds.size : collectionView.bounds.size
|
||||||
|
guard containerBounds.width > 0, containerBounds.height > 0 else {
|
||||||
|
return bounds.size
|
||||||
|
}
|
||||||
|
|
||||||
|
switch currentDisplayType {
|
||||||
|
case .horizontalScroll:
|
||||||
|
return CGSize(
|
||||||
|
width: containerBounds.width / CGFloat(max(pagesPerScreen, 1)),
|
||||||
|
height: containerBounds.height
|
||||||
|
)
|
||||||
|
case .verticalScroll:
|
||||||
|
return containerBounds
|
||||||
|
case .pageCurl:
|
||||||
|
return bounds.size
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var cellViewKey: Int8 = 0
|
private var cellViewKey: Int8 = 0
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user