feat(reader): 实现大书快速进入与增量分页路径
- PaginationCoordinator: 新增 textReflowable 快速进入路径,支持增量章节构建 与 staged 合并策略,避免整书一次性构建的主线程阻塞 - TextBookBuilder: 重构为支持单章构建与增量合并模式 - ChapterPageCounter: 优化分页计数逻辑,支持章节级分页缓存 - PageFrameFactory: 新增 CoreText 页面帧工厂,提升排版性能 - ReaderContext/ReaderRuntime: 适配增量分页状态管理 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
488350d956
commit
c76beed03c
@@ -20,9 +20,8 @@ struct RDEPUBChapterPageCounter {
|
||||
private let pageSize: CGSize
|
||||
private let config: RDEPUBTextLayoutConfig
|
||||
private let pageBreakPolicy: RDEPUBPageBreakPolicy
|
||||
|
||||
/// CoreText 帧设置器
|
||||
private let framesetter: CTFramesetter
|
||||
|
||||
/// DTCoreText 路径可直接消费的单矩形布局区域
|
||||
private let dtLayoutRect: CGRect
|
||||
|
||||
@@ -59,27 +58,26 @@ struct RDEPUBChapterPageCounter {
|
||||
var frames: [RDEPUBTextLayoutFrame] = []
|
||||
var location = 0
|
||||
let resolvedSize = config.resolvedFrameSize(fallback: pageSize)
|
||||
let usableWidth = resolvedSize.width - config.edgeInsets.left - config.edgeInsets.right
|
||||
let usableHeight = resolvedSize.height - config.edgeInsets.top - config.edgeInsets.bottom
|
||||
let contentRect = config.contentRect(fallback: resolvedSize)
|
||||
|
||||
guard usableWidth > 0, usableHeight > 0 else {
|
||||
guard contentRect.width > 0, contentRect.height > 0 else {
|
||||
return []
|
||||
}
|
||||
|
||||
while location < attributedString.length {
|
||||
let framePath = CGMutablePath()
|
||||
let pageRect = CGRect(
|
||||
x: config.edgeInsets.left,
|
||||
y: config.edgeInsets.bottom,
|
||||
width: usableWidth,
|
||||
height: usableHeight
|
||||
let framePath = RDEPUBCoreTextPageFrameFactory.makeLayoutPath(
|
||||
pageSize: resolvedSize,
|
||||
config: config
|
||||
)
|
||||
framePath.addRect(pageRect)
|
||||
let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(location, 0), framePath, nil)
|
||||
let proposedRange = proposedRangeUsingWXReadPageCount(
|
||||
let frame = CTFramesetterCreateFrame(
|
||||
framesetter,
|
||||
CFRangeMake(location, 0),
|
||||
framePath,
|
||||
nil
|
||||
)
|
||||
let proposedRange = proposedVisibleRange(
|
||||
from: frame,
|
||||
start: location,
|
||||
usableHeight: usableHeight,
|
||||
totalLength: attributedString.length
|
||||
)
|
||||
guard proposedRange.length > 0 else {
|
||||
@@ -87,12 +85,18 @@ struct RDEPUBChapterPageCounter {
|
||||
}
|
||||
|
||||
let avoidAdjusted = factory.trimmedRangeForAvoidPageBreakInside(from: frame, proposed: proposedRange)
|
||||
let keepWithNextAdjusted = factory.trimmedRangeForKeepWithNext(from: frame, proposed: avoidAdjusted)
|
||||
let lineRanges = RDEPUBCoreTextPageFrameFactory.lineRanges(from: frame)
|
||||
let widowOrphanAdjusted = factory.trimmedRangeForWidowAndOrphanControl(
|
||||
proposed: keepWithNextAdjusted,
|
||||
lineRanges: lineRanges
|
||||
)
|
||||
let effectiveLineRanges = lineRangesWithinRange(lineRanges, range: widowOrphanAdjusted)
|
||||
|
||||
let adjusted = pageBreakPolicy.adjustedRange(
|
||||
from: avoidAdjusted,
|
||||
from: widowOrphanAdjusted,
|
||||
totalLength: attributedString.length,
|
||||
lineRanges: lineRanges,
|
||||
lineRanges: effectiveLineRanges,
|
||||
factory: factory
|
||||
)
|
||||
let trailingFragmentID = factory.nearestTrailingFragmentID(
|
||||
@@ -157,10 +161,15 @@ struct RDEPUBChapterPageCounter {
|
||||
let avoidAdjusted = factory.trimmedRangeForAvoidPageBreakInside(from: layoutFrame, proposed: proposedRange)
|
||||
let lineAdjusted = factory.trimmedRangeForKeepWithNext(from: layoutFrame, proposed: avoidAdjusted)
|
||||
let lineRanges = RDEPUBCoreTextPageFrameFactory.lineRanges(from: layoutFrame)
|
||||
let widowOrphanAdjusted = factory.trimmedRangeForWidowAndOrphanControl(
|
||||
proposed: lineAdjusted,
|
||||
lineRanges: lineRanges
|
||||
)
|
||||
let effectiveLineRanges = lineRangesWithinRange(lineRanges, range: widowOrphanAdjusted)
|
||||
let adjusted = pageBreakPolicy.adjustedRange(
|
||||
from: lineAdjusted,
|
||||
from: widowOrphanAdjusted,
|
||||
totalLength: attributedString.length,
|
||||
lineRanges: lineRanges,
|
||||
lineRanges: effectiveLineRanges,
|
||||
factory: factory
|
||||
)
|
||||
let verifiedRange: NSRange
|
||||
@@ -231,40 +240,29 @@ struct RDEPUBChapterPageCounter {
|
||||
|
||||
// MARK: - WXRead 分页对齐
|
||||
|
||||
/// 逐句对齐 WXRead `WRChapterPageCount.recalculatePageRangesForAttributedString` 的分页循环。
|
||||
private func proposedRangeUsingWXReadPageCount(
|
||||
/// 读取 CoreText frame 当前可见的字符范围;多栏 path 下也能返回整页可见区。
|
||||
private func proposedVisibleRange(
|
||||
from frame: CTFrame,
|
||||
start location: Int,
|
||||
usableHeight: CGFloat,
|
||||
totalLength: Int
|
||||
) -> NSRange {
|
||||
let lines = CTFrameGetLines(frame) as! [CTLine]
|
||||
guard !lines.isEmpty else {
|
||||
let visibleRange = CTFrameGetVisibleStringRange(frame)
|
||||
guard visibleRange.length > 0 else {
|
||||
return NSRange(location: location, length: 0)
|
||||
}
|
||||
|
||||
var origins = [CGPoint](repeating: .zero, count: lines.count)
|
||||
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &origins)
|
||||
|
||||
var pageCharCount = 0
|
||||
for (index, line) in lines.enumerated() {
|
||||
let lineRange = CTLineGetStringRange(line)
|
||||
let lineY = origins[index].y
|
||||
var ascent: CGFloat = 0
|
||||
var descent: CGFloat = 0
|
||||
CTLineGetTypographicBounds(line, &ascent, &descent, nil)
|
||||
|
||||
if lineY - ascent > usableHeight {
|
||||
break
|
||||
}
|
||||
|
||||
pageCharCount += lineRange.length
|
||||
let resolvedLocation = max(location, visibleRange.location)
|
||||
let visibleEnd = visibleRange.location + visibleRange.length
|
||||
let length = min(max(visibleEnd - resolvedLocation, 0), totalLength - resolvedLocation)
|
||||
guard length > 0 else {
|
||||
return NSRange(location: resolvedLocation, length: 0)
|
||||
}
|
||||
return NSRange(location: resolvedLocation, length: length)
|
||||
}
|
||||
|
||||
if pageCharCount == 0 {
|
||||
pageCharCount = 1
|
||||
private func lineRangesWithinRange(_ lineRanges: [NSRange], range: NSRange) -> [NSRange] {
|
||||
lineRanges.filter { lineRange in
|
||||
lineRange.location >= range.location && NSMaxRange(lineRange) <= NSMaxRange(range)
|
||||
}
|
||||
|
||||
return NSRange(location: location, length: min(pageCharCount, totalLength - location))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user