diff --git a/Doc/index.md b/Doc/index.md index 15137c1..c1d4288 100644 --- a/Doc/index.md +++ b/Doc/index.md @@ -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 分层、类型器升级) | ## 目录约定 diff --git a/ReadViewDemo/ReadViewDemo.xcodeproj/project.pbxproj b/ReadViewDemo/ReadViewDemo.xcodeproj/project.pbxproj index 0d517cc..1a1aebb 100644 --- a/ReadViewDemo/ReadViewDemo.xcodeproj/project.pbxproj +++ b/ReadViewDemo/ReadViewDemo.xcodeproj/project.pbxproj @@ -185,10 +185,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks.sh\"\n"; diff --git a/ReadViewDemo/ReadViewDemo.xcworkspace/xcuserdata/shen.xcuserdatad/UserInterfaceState.xcuserstate b/ReadViewDemo/ReadViewDemo.xcworkspace/xcuserdata/shen.xcuserdatad/UserInterfaceState.xcuserstate index 447cab9..add3653 100644 Binary files a/ReadViewDemo/ReadViewDemo.xcworkspace/xcuserdata/shen.xcuserdatad/UserInterfaceState.xcuserstate and b/ReadViewDemo/ReadViewDemo.xcworkspace/xcuserdata/shen.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Sources/RDReaderView/EPUBTextRendering/RDEPUBDTCoreTextRenderer.swift b/Sources/RDReaderView/EPUBTextRendering/RDEPUBDTCoreTextRenderer.swift index 24310b2..35cecb5 100644 --- a/Sources/RDReaderView/EPUBTextRendering/RDEPUBDTCoreTextRenderer.swift +++ b/Sources/RDReaderView/EPUBTextRendering/RDEPUBDTCoreTextRenderer.swift @@ -104,7 +104,8 @@ public struct RDEPUBDTCoreTextRenderer: RDEPUBTextRenderer { guard let element else { return } RDEPUBTextRendererSupport.prepareHTMLElementForReaderRendering( element, - style: request.style + style: request.style, + maxImageSize: resolvedMaxImageSize(for: request) ) } return builder?.generatedAttributedString() @@ -116,7 +117,7 @@ public struct RDEPUBDTCoreTextRenderer: RDEPUBTextRenderer { /// 确保最终行距与用户设置的 style.lineSpacing 一致。 private func dtOptions(request: RDEPUBTextChapterRenderRequest) -> [AnyHashable: Any] { let style = request.style - let screenBounds = UIScreen.main.bounds.insetBy(dx: 20, dy: 28) + let maxImageSize = resolvedMaxImageSize(for: request) var options: [AnyHashable: Any] = [ NSTextSizeMultiplierDocumentOption: 1.0, DTDefaultFontFamily: style.font.familyName, @@ -124,7 +125,7 @@ public struct RDEPUBDTCoreTextRenderer: RDEPUBTextRenderer { DTDefaultFontSize: style.font.pointSize, DTDefaultLineHeightMultiplier: max((style.font.lineHeight + style.lineSpacing) / max(style.font.lineHeight, 1), 1), DTUseiOS6Attributes: true, - DTMaxImageSize: NSValue(cgSize: screenBounds.size) + DTMaxImageSize: NSValue(cgSize: maxImageSize) ] if let baseURL = request.context.baseURL { @@ -136,5 +137,18 @@ public struct RDEPUBDTCoreTextRenderer: RDEPUBTextRenderer { 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 } diff --git a/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift b/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift index 2d9ad0e..f0f7090 100644 --- a/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift +++ b/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift @@ -293,7 +293,9 @@ public final class RDEPUBTextBookBuilder { baseURL: parser.fileURL(forRelativePath: item.href)?.deletingLastPathComponent(), style: style, resourceResolver: publication.resourceResolver, - contentLanguageCode: publication.metadata.language + contentLanguageCode: publication.metadata.language, + pageSize: pageSize, + layoutConfig: layoutConfig ) // 渲染 HTML → NSAttributedString diff --git a/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookCache.swift b/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookCache.swift index 2d57b6c..3c9d021 100644 --- a/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookCache.swift +++ b/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookCache.swift @@ -123,7 +123,7 @@ final class ChapterPaginationArchive: NSObject, NSSecureCoding { public final class RDEPUBTextBookCache { /// 缓存模式版本号,变更时旧缓存自动失效 - public var schemaVersion: Int = 1 + public var schemaVersion: Int = 4 /// 串行队列,保证缓存读写的线程安全 private let queue = DispatchQueue(label: "com.rdreader.textbookcache", qos: .utility) diff --git a/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextLayouter.swift b/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextLayouter.swift index dfaaf41..1930713 100644 --- a/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextLayouter.swift +++ b/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextLayouter.swift @@ -155,27 +155,31 @@ struct RDEPUBTextLayouter { totalLength: attributedString.length, lineRanges: lineRanges ) + let verifiedRange = verifiedDisplayRange(for: adjusted.range) let trailingFragmentID = nearestTrailingFragmentID( - endingAt: adjusted.range.location + adjusted.range.length, + endingAt: verifiedRange.location + verifiedRange.length, fragmentOffsets: fragmentOffsets ) + let diagnostics = verifiedRange == adjusted.range + ? adjusted.diagnostics + : adjusted.diagnostics + ["verified-display-range \(NSStringFromRange(adjusted.range)) -> \(NSStringFromRange(verifiedRange))"] frames.append( RDEPUBTextLayoutFrame( - contentRange: adjusted.range, + contentRange: verifiedRange, breakReason: adjusted.breakReason, - blockRange: adjusted.blockRange, - attachmentRanges: adjusted.attachmentRanges, - attachmentKinds: adjusted.attachmentKinds, - blockKinds: adjusted.blockKinds, - semanticHints: adjusted.semanticHints, - attachmentPlacements: adjusted.attachmentPlacements, + blockRange: blockRange(at: max(verifiedRange.location, verifiedRange.location + verifiedRange.length - 1)), + attachmentRanges: attachmentRanges(in: verifiedRange), + attachmentKinds: attachmentKinds(in: verifiedRange), + blockKinds: blockKinds(in: verifiedRange), + semanticHints: semanticHints(in: verifiedRange), + attachmentPlacements: attachmentPlacements(in: verifiedRange), 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 { location += max(visibleRange.length, 1) continue @@ -185,6 +189,30 @@ struct RDEPUBTextLayouter { 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 private static func makeLayoutPath(pageSize: CGSize, config: RDEPUBTextLayoutConfig) -> CGPath { diff --git a/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRenderer.swift b/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRenderer.swift index 683cc0b..33d96c5 100644 --- a/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRenderer.swift +++ b/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRenderer.swift @@ -286,10 +286,21 @@ public struct RDEPUBTextChapterContext: Equatable { public struct RDEPUBTextChapterRenderRequest { public var context: RDEPUBTextChapterContext 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.style = style + self.pageSize = pageSize + self.layoutConfig = layoutConfig } } diff --git a/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift b/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift index 84e9af7..b086463 100644 --- a/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift +++ b/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift @@ -197,7 +197,9 @@ enum RDEPUBTextRendererSupport { baseURL: URL?, style: RDEPUBTextRenderStyle, resourceResolver: RDEPUBResourceResolver?, - contentLanguageCode: String? = nil + contentLanguageCode: String? = nil, + pageSize: CGSize? = nil, + layoutConfig: RDEPUBTextLayoutConfig? = nil ) -> RDEPUBTextChapterRenderRequest { let normalizedHTML = injectPaginationSemanticMarkers(into: normalizeHTML(rawHTML)) let stylesheetHrefReplacements = inlineLinkedStyleSheets( @@ -255,7 +257,12 @@ enum RDEPUBTextRendererSupport { stylesheet: RDEPUBTextStyleSheetPackage(layers: layers), resourceDiagnostics: resourceDiagnostics ) - return RDEPUBTextChapterRenderRequest(context: context, style: style) + return RDEPUBTextChapterRenderRequest( + context: context, + style: style, + pageSize: pageSize, + layoutConfig: layoutConfig + ) } /// HTML 规范化:清理冗余字符(CR、多余换行),规范化附件 HTML 标记 @@ -285,15 +292,20 @@ enum RDEPUBTextRendererSupport { #if canImport(DTCoreText) static func prepareHTMLElementForReaderRendering( _ element: DTHTMLElement, - style: RDEPUBTextRenderStyle + style: RDEPUBTextRenderStyle, + maxImageSize: CGSize? = nil ) { guard let attachment = element.textAttachment else { return } 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), 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) { element.displayStyle = .inline } else if isCoverAttachment(attachment) { @@ -390,6 +402,38 @@ enum RDEPUBTextRendererSupport { private static func normalizeAttachmentHTMLMarkers(in html: String) -> String { var normalized = html + if let bodyPicContainerRegex = try? NSRegularExpression( + pattern: #"]*class\s*=\s*["'][^"']*\b(?:qrbodyPic|bodyPic)\b[^"']*["'][^>]*)>([\s\S]*?)"#, + options: [.caseInsensitive] + ) { + normalized = replaceMatches( + using: bodyPicContainerRegex, + in: normalized + ) { tag in + guard let imageTagRegex = try? NSRegularExpression(pattern: #"]*>"#, 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( pattern: #"]*class\s*=\s*["'][^"']*\bqqreader-footnote\b[^"']*["'][^>]*)>"#, options: [.caseInsensitive] diff --git a/Sources/RDReaderView/EPUBUI/RDEPUBPageLayoutSnapshot.swift b/Sources/RDReaderView/EPUBUI/RDEPUBPageLayoutSnapshot.swift index 27ec33d..c78808e 100644 --- a/Sources/RDReaderView/EPUBUI/RDEPUBPageLayoutSnapshot.swift +++ b/Sources/RDReaderView/EPUBUI/RDEPUBPageLayoutSnapshot.swift @@ -139,9 +139,10 @@ struct RDEPUBPageLayoutSnapshot { var lines: [RDEPUBPageLine] = [] var runs: [RDEPUBPageRun] = [] var attachments: [RDEPUBPageAttachment] = [] + let pageOffset = page.pageStartOffset for dtLine in dtLines { - let lineRange = dtLine.stringRange() + let lineRange = offset(dtLine.stringRange(), by: pageOffset) let line = RDEPUBPageLine( stringRange: lineRange, frame: dtLine.frame, @@ -154,7 +155,7 @@ struct RDEPUBPageLayoutSnapshot { if let glyphRuns = dtLine.glyphRuns as? [DTCoreTextGlyphRun] { for run in glyphRuns { - let runRange = run.stringRange() + let runRange = offset(run.stringRange(), by: pageOffset) let isAttachment = run.attachment != nil runs.append( RDEPUBPageRun( @@ -182,7 +183,7 @@ struct RDEPUBPageLayoutSnapshot { } } - let visibleRange = layoutFrame.visibleStringRange() + let visibleRange = offset(layoutFrame.visibleStringRange(), by: pageOffset) return RDEPUBPageLayoutSnapshot( page: page, @@ -210,5 +211,12 @@ struct RDEPUBPageLayoutSnapshot { : nil 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 } diff --git a/Sources/RDReaderView/EPUBUI/RDEPUBReaderController.swift b/Sources/RDReaderView/EPUBUI/RDEPUBReaderController.swift index 520ddb3..0c8fd85 100644 --- a/Sources/RDReaderView/EPUBUI/RDEPUBReaderController.swift +++ b/Sources/RDReaderView/EPUBUI/RDEPUBReaderController.swift @@ -165,6 +165,8 @@ public final class RDEPUBReaderController: UIViewController { private var currentBrightness: CGFloat private var didStartInitialLoad = false private var isRepaginating = false + private var lastTextPaginationPageSize: CGSize? + private var isReconcilingTextPaginationSize = false private var paginationToken = UUID() private var paginator: RDEPUBPaginator? private var searchState: RDEPUBSearchState? @@ -695,6 +697,7 @@ public final class RDEPUBReaderController: UIViewController { if publication.readingProfile == .textReflowable { let renderer = resolvedTextRenderer() let pageSize = currentTextPageSize() + lastTextPaginationPageSize = pageSize let layoutConfig = currentTextLayoutConfig(pageSize: pageSize) let builder = RDEPUBTextBookBuilder(renderer: renderer, cache: textBookCache, layoutConfig: layoutConfig) let renderStyle = currentTextRenderStyle() @@ -1258,12 +1261,12 @@ public final class RDEPUBReaderController: UIViewController { } private func currentTextPageSize() -> CGSize { - let viewportSize = currentLayoutContext().viewportSize - let insets = configuration.reflowableContentInsets - return CGSize( - width: max(viewportSize.width - insets.left - insets.right, 1), - height: max(viewportSize.height - insets.top - insets.bottom, 1) - ) + let pageNum = readerView.currentPage >= 0 ? readerView.currentPage : nil + let resolvedSize = readerView.resolvedSinglePageSize(pageNum: pageNum) + if resolvedSize.width > 0, resolvedSize.height > 0 { + return resolvedSize + } + return currentLayoutContext().viewportSize } private func currentTextRenderStyle() -> RDEPUBTextRenderStyle { @@ -1278,15 +1281,10 @@ 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, + frameWidth: max(pageSize.width, 1), + frameHeight: max(pageSize.height, 1), + edgeInsets: configuration.reflowableContentInsets, numberOfColumns: configuration.numberOfColumns, columnGap: configuration.columnGap, avoidOrphans: true, @@ -1834,6 +1832,7 @@ extension RDEPUBReaderController: RDReaderDataSource, RDReaderDelegate { public func pageNum(readerView: RDReaderView, pageNum: Int) { updateCurrentSelection(nil) + reconcileTextPaginationSizeIfNeeded(for: pageNum) let totalPages = pageCountOfReaderView(readerView: readerView) if totalPages > 0, pageNum == totalPages - 1 { @@ -1859,6 +1858,36 @@ extension RDEPUBReaderController: RDReaderDataSource, RDReaderDelegate { _ = isLandscape 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 渲染路径) diff --git a/Sources/RDReaderView/EPUBUI/RDEPUBTextContentView.swift b/Sources/RDReaderView/EPUBUI/RDEPUBTextContentView.swift index 37829b8..0d8063d 100644 --- a/Sources/RDReaderView/EPUBUI/RDEPUBTextContentView.swift +++ b/Sources/RDReaderView/EPUBUI/RDEPUBTextContentView.swift @@ -265,7 +265,7 @@ final class RDEPUBTextContentView: UIView { ) #if canImport(DTCoreText) - let displayContent = NSMutableAttributedString(attributedString: page.chapterContent) + let displayContent = NSMutableAttributedString(attributedString: page.content) let fullRange = NSRange(location: 0, length: displayContent.length) displayContent.addAttribute( .foregroundColor, @@ -275,7 +275,7 @@ final class RDEPUBTextContentView: UIView { coreTextContentView.isHidden = false coreTextContentView.backgroundColor = .clear coreTextDisplayContent = displayContent - coreTextDisplayRange = page.contentRange + coreTextDisplayRange = NSRange(location: 0, length: displayContent.length) textView.isHidden = true textView.isUserInteractionEnabled = false textView.attributedText = nil diff --git a/Sources/RDReaderView/EPUBUI/RDURLReaderController.swift b/Sources/RDReaderView/EPUBUI/RDURLReaderController.swift index a9cdac0..536676d 100644 --- a/Sources/RDReaderView/EPUBUI/RDURLReaderController.swift +++ b/Sources/RDReaderView/EPUBUI/RDURLReaderController.swift @@ -123,7 +123,7 @@ public final class RDURLReaderController: UIViewController { layoutConfig: RDEPUBTextLayoutConfig( frameWidth: pageSize.width, frameHeight: pageSize.height, - edgeInsets: .zero, + edgeInsets: epubConfiguration.reflowableContentInsets, numberOfColumns: 1, columnGap: 20, avoidOrphans: true, @@ -179,15 +179,11 @@ public final class RDURLReaderController: UIViewController { print("[ReadViewDemo] automation \(prefix) -> page \(page) href \(href) progression \(progression)") } - /// 计算文本分页的页面尺寸 - /// 基于屏幕尺寸和配置的内边距 + /// 计算文本分页的页面尺寸。 + /// 分页器使用完整 viewport,真实可绘制区域由 layoutConfig.edgeInsets 控制, + /// 以保持与页面展示时的内容几何一致。 private func currentTextPageSize() -> CGSize { - let viewportSize = 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) - ) + UIScreen.main.bounds.size } /// 根据当前配置生成文本渲染样式 diff --git a/Sources/RDReaderView/ReaderView/RDReaderView.swift b/Sources/RDReaderView/ReaderView/RDReaderView.swift index 2ec1b0a..8c155a2 100644 --- a/Sources/RDReaderView/ReaderView/RDReaderView.swift +++ b/Sources/RDReaderView/ReaderView/RDReaderView.swift @@ -1265,6 +1265,47 @@ extension RDReaderView { 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