搭建小说系统源码,如何实现读书的分页功能

目标

  1. 使用 TextKit 快速分页
  2. 使用 UIPageViewController

支持平台

iOS, iPadOS
也许还支持 Mac Calalyst ?

使用语言

Swift

视图结构

|- UIViewController // 根视图, 可添加菜单显示, 手势操作等
    |- UIPageController // 章节视图, 一页对应一章
        | - UIPageController // 章节内容分页视图, 将单章内容进行分页显示
        |   | - UIViewController // 单页显示视图, 对应单页数据
        |   |   |- UITextView // 文字视图
        |   |
        |   | - UIViewController
        |   |   |- UITextView
        |   |
        |   | ...
        |
        | - UIPageController
        |   | - UIViewController
        |   |   |- UITextView
        |   |
        |   | - UIViewController
        |   |   |- UITextView
        |   |
        |   | ...
        |
        | ...

小说系统源码的章节内容分页视图中, 只要在返回单页显示视图的代理中返回 nil, 即可实现章节内容翻到最后一页时, 继续翻页翻到下一章节的逻辑

分页实现

首先, 一定要先确定好 TextView 的大小与内容间距, 即文字显示区域的大小, 这将严重影响到分页后的数据能不能正常显示

其次, 首行缩进最好用空格代替, 而不是用 NSParagraphStylefirstLineHeadIndent 属性来实现, 否则会出现某段落从中间被分开, 下一页依然被缩进的情况

首行缩进的空格数量可用以下逻辑计算:

let normalWidth = "你好".size(font: textFont).width // 请根据内容语言改变文字
let speaceWidth = " ".size(font: textFont).width // 一个空格的宽
let speaceCount = Int(normalWidth / speaceWidth)
let speace = String(repeating: " ", count: speaceCount)

然后在每段前添加空格

let result = content.string.components(separatedBy: "\n").map { "\(speace)\($0)" }

这样就可以在每段首行添加一个合适的缩进了

接下来就是重点的分页了


第一步, 前期参数准备:

  1. 准备好处理完成的 NSAttributedString, 最好包含各种字体, 颜色, 格式等设置信息, 避免分页视图拿到数据后再次生成 NSAttributedString , 重复设置内容样式导致的分页不准的情况

  2. 准备好文字显示区域大小的参数


第二步, 开始分页:
准备数据:

// 创建 NSLayoutManager, 所有的分页逻辑开端
let layoutManager = NSLayoutManager()

// 如果没有给特定部分文字区域设置单独的布局, 可设置此项为 false, 以提高性能
layoutManager.allowsNonContiguousLayout = false

// 使用之前准备好的 NSAttributedString 进行初始化 NSTextStorage
let textStorage = NSTextStorage(attributedString: string)
textStorage.addLayoutManager(layoutManager)

// 设定文字显示区域参数
let viewSize: CGSize = CGSize(width: textAreaWidth, height:  textAreaHeight)

// 设定 textView 的内间距
let textInsets = UIEdgeInsets.zero
let textViewFrame = CGRect(x: 0, y: 0, width: viewSize.width, height: viewSize.height)

// 开始分页
var glyphRange: Int = 0
var numberOfGlyphs: Int = 0

分页循环:

var ranges: [NSRange] = []
repeat {
    let textContainer = NSTextContainer(size: viewSize)
    layoutManager.addTextContainer(textContainer)
    
    // 不断创建 textView 让 NSLayoutManager 进行内容分页
    let textView = UITextView(frame: textViewFrame, textContainer: textContainer)
    textView.isEditable = false
    textView.isSelectable = false
    textView.textContainerInset = textInsets
    textView.showsVerticalScrollIndicator = false
    textView.showsHorizontalScrollIndicator = false
    textView.isScrollEnabled = false // 禁止滑动, 否则计算结果将不再准确
    textView.bounces = false
    textView.bouncesZoom = false
    
    // 获取当前分页内容所在位置
    let range = layoutManager.glyphRange(for: textContainer)
    ranges.append(range)
    
    // 判定是否分页完成
    glyphRange = NSMaxRange(range)
    numberOfGlyphs = layoutManager.numberOfGlyphs
} while glyphRange < numberOfGlyphs - 1

CoreText 版本:

var ranges: [NSRange] = []
let framesetter = CTFramesetterCreateWithAttributedString(string)
var textPosition = 0
while textPosition < string.length {
    let path = CGPath(rect: textViewFrame.inset(by: textInsets), transform: nil)
    let frame = CTFramesetterCreateFrame(framesetter, .init(location: textPosition, length: 0), path, nil)
    let stringRange = CTFrameGetVisibleStringRange(frame)
    
    let range: NSRange = .init(location: stringRange.location, length: stringRange.length)
    textPosition += stringRange.length
    ranges.append(range)
}

至此, 就得到了带有格式的全文 NSAttributedString, 和分页区域的 ranges


第三步, 显示分页数据,小说系统源码的章节内容分页视图中, 将单章的 NSAttributedString 和分到的 range 分配给每一个单页显示视图, 在 UITextView 中直接设置 attributedTextattributedString.attributedSubstring(from: range)

UITextView 的设置务必于分页循环时的 UITextView 保持一致
声明:本文由云豹科技转发自Norld博客,如有侵权请联系作者删除

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值