iOS使用CoreText完成txt阅读器

 CoreText是一个高效处理字符和字形转换和进行文字排版的框架,API基于C语言。

常见的CoreText类介绍

(1)、CFAttributedStringRef
属性字符串,用于存储需要绘制的文字字符和字符属性

(2)、CTFramesetterRef
framesetter对应的类型是 CTFramesetter,通过CFAttributedStringRef进行初始化,它作为CTFrame对象的生产工厂,负责根据path生产对应的CTFrame;

(3)、CTFrame
CTFrame是可以通过CTFrameDraw函数直接绘制到context上的,当然你可以在绘制之前,操作CTFrame中的CTLine,进行一些参数的微调;

(3)、CTLine
在CTFrame内部是由多个CTLine来组成的,每个CTLine代表一行;可以看做Core Text绘制中的一行的对象 通过它可以获得当前行的line ascent,line descent ,line leading,还可以获得Line下的所有Glyph Runs;

(4)、CTRun
或者叫做 Glyph Run,每个CTLine又是由多个CTRun组成的,每个CTRun代表一组显示风格一致的文本,是一组共享想相同attributes(属性)的字形的集合体;

渲染流程

  1. 当我们需要排版时,可以对字符串设置各种格式,生成NSAttributeString;
  2. 然后用NSAttributeString去创建CTFramesetter类,
  3. CTFramesetter会处理排版信息,然后生成排版后的结果CTFrame;
  4. CTFrame是一段或者多段文本,每段文本又由多行文字组成,每行的表示为CTLine;
  5. CTLine是一行文本,每行文本由多个CTRun组成,CTRun是一小段连续的字形;
  6. CTTypeSetter负责上下文相关排版处理,比如说换行,每个CTFrame中都会有一个CTTypeSetter; 他们之间的关系图如下:

iOS使用CoreText完成txt阅读器_第1张图片

总的来说,CTFramesetter是生成CTFrame的工厂类,初始化参数是attributed string,会在内部创建CTTypesetter并进行实际的排版;

CTLine类似每一行的文字,CTRun是一行中具有相同属性的连续字形,比如说“我正在分享阅读器”,就会由三个CTRun组成,分别是“我正在”、“分享”、“阅读器”(因为“分享”两个字加粗了,否则就会是一个CTRun)。

iOS使用CoreText完成txt阅读器_第2张图片

 CoreText的使用流程:

iOS使用CoreText完成txt阅读器_第3张图片

  1. 使用core text就是有一个要显示的string,
  2. 然后定义这个string每个部分的样式生成富文本attributedString
  3. 由富文本生成 CTFramesetter
  4. CTFramesetter得到CTFrame
  5. 使用绘制(CTFrameDraw)CTFrame 

关键函数介绍

由富文本字符串得到CTFramesetter

  • CTFramesetterCreateWithAttributedString(att as CFAttributedString)
    • CFAttributedString是NSAttributedString的CF对象,可以直接强转;

CTFramesetter包含了富文本字符串的布局信息和相关属性,供后续的绘制操作使用。最主要的作用就是生成下面的CTFrame。

通过调用 CTFramesetterCreateWithAttributedString 函数,可以将富文本字符串转换为 Core Text 的布局对象,为后续的绘制操作提供所需的文本排版和属性信息。这样,你就可以使用 Core Text 提供的更多功能来自定义文本的布局、字体、颜色等,并实现高度定制化的文本渲染效果。

CTFramesetterRef 对象并不直接进行绘制操作,它只包含了文本布局的信息。要将文本绘制到图形上下文中,还需要使用 CTFrameDraw 函数创建并绘制 CTFrameRef 对象。

生成CTFrame

  • CTFramesetterCreateFrame(framesetter, CFRangeMake(pageStart, 0), path, nil)

使用 CTFramesetterRef 对象、文本范围、路径和其他参数创建一个 CTFrameRef 对象,

CTFrame是排版数据,可直接通过重写View的drawRect方法渲染到页面上

  • framesetter:上面创建的CTFramesetterRef
  • stringRange:要使用的文本范围,即 CFRange 结构体。
    • 可以通过设置 CFRangeMake 参数来确定要使用的富文本字符串的起始位置和长度
    • 如果范围的长度部分设置为0,比如CFRangeMake(location, 0),则会尽可能的填满CTFrame,将继续添加行,直到文本或空间用完。
  • path:绘制文本的路径,即 CGPathRef 类型对象。
    • 路径定义了文本应该在画布上的布局方式和区域。
    • 一般传渲染View的bounds即可
  • frameAttributes:可选的附加属性字典,提供额外的布局控制和属性设置。

计算分页

  • CTFrameGetVisibleStringRange(frame)

CTFrameGetVisibleStringRange 函数的作用是获取给定文本框架(CTFrame)中可见的文本范围。可见的范围是指在当前文本框架大小和路径下实际可见的文本部分。

返回值: CTFrameGetVisibleStringRange 函数返回一个 CFRange 结构体,表示给定文本框架中可见的文本范围。该范围包括起始位置(location)和长度(length)信息。

比如原文有1W字,当前的frame只能显示200字,那么返回的Range就是(0,200),下一页在从200的基础上进行计算,比如第二页算出为(200,430),在下一页就从430开始计算,如此循环就可计算出这1W字需要分多少页,并且每页内容的CTFrame都已生成。

通过上面的介绍,把这几个函数连起来,就是数据准备阶段的核心方法:

  1. 根据txt内容生成String -> 在由String生成富文本-> 由富文本生成framesetter,
  2. 根据页面大小计算生成单页的CTFrame
  3. CTFrame获取当前Frame有效的文字显示范围,下一页的location累加,循环计算分页,保存得到每页的内容范围和每页的CTFrame
func createCTFrame(contentStr: String) {

    let range = NSMakeRange(0, contentStr.count)
    let att = NSMutableAttributedString(string: contentStr)

    att.addAttribute(.foregroundColor, value: UIColor.lightGray, range: range)

    att.addAttribute(.font, value: UIFont.systemFont(ofSize: 22), range: range)

    let framesetter = CTFramesetterCreateWithAttributedString(att as CFAttributedString)
    let path = CGPath(rect: self.readView.bounds, transform: nil)

    var pageStart = 0
    var frameArray: [CTFrame] = []

    var i: Int = 0

    repeat {

        let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(pageStart, 0), path, nil)

        let pageRange = CTFrameGetVisibleStringRange(frame)

        let beginIndex = contentStr.index(contentStr.startIndex, offsetBy: pageRange.location)
        let endIndex = contentStr.index(beginIndex, offsetBy: pageRange.length)
        let onePage = String(contentStr[beginIndex..

渲染方法

  • CTFrameDraw(frame, ctx)

渲染的核心方法,CTFrameDraw 方法的作用是将指定的文本框架对象绘制到图形上下文中,实现文本的可视化呈现。

具体做法:在继承UIView的子类中,重写drawrect方法,里面最重要的一行就是CTFrameDraw(frame, ctx),即可完成渲染:

/// 绘制
override func draw(_ rect: CGRect) {
    guard let frame = frameRef, let ctx = UIGraphicsGetCurrentContext() else {
        return
    }
    ctx.textMatrix = CGAffineTransform.identity
    ctx.translateBy(x: 0, y: bounds.size.height)
    ctx.scaleBy(x: 1.0, y: -1.0)
    CTFrameDraw(frame, ctx)
}

除了 CTFrameDraw , 还想要对文本内容有更精细的控制,可以使用CTLineDraw,CTRunDraw

  • void CTLineDraw(CTLineRef line,CGContextRef context )

  • void CTRunDraw( CTRunRef run, CGContextRef context,CFRange range ) 

CTLineDraw 函数绘制的是单行文本,需要在CGContext中设置好position,在图文混排时,可以用到。

CTRunDraw 函数绘制的是单个文本运行,YYText使用的渲染方法就是CTRunDraw,对于控制特别精细的可以但是CTRun控制渲染。

以下是在学习的过程中找到的资料:

CoreText的基础知识了解:

CoreText实战讲解,手把手教你实现图文、点击高亮、自定义截断功能 - 简书

文字排版入门—— 排版基础、CoreText和图文混排-腾讯云开发者社区-腾讯云

iOS 基于CoreText的排版引擎 - 简书

比较完整的txt阅读器demo:

iOS: .txt 小说阅读器功能开发的 5 个老套路 - 掘金

套路继续, .txt 小说阅读器功能开发 - 掘金

最简版demo: 使用coretext计算分页并渲染,上面demo的功能多,导致核心逻辑淹没在业务代码中,找起来麻烦,所以做了一个只展示核心原理的最简demo : 

博客园系列文章:

https://www.cnblogs.com/summer-blog/p/6030641.html

https://www.cnblogs.com/summer-blog/p/6030885.html

https://www.cnblogs.com/summer-blog/p/6044118.html

https://www.cnblogs.com/summer-blog/p/6402664.html

比较精细的阅读器思路,页面行高重排,目前我们还用不到

我在七猫做阅读器——排版篇

从基础的各种CoreText渲染,到页面之间切换动画都有独立的demo,最后有一个把CoreText渲染+页面切换集成在一起的demo 

小说阅读器的设计和实现 - 简书

阅读器多种翻页的设计与实现 - 简书

GitHub - loyinglin/LearnCoreText

你可能感兴趣的:(ios,cocoa,macos)