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(属性)的字形的集合体;
渲染流程
- 当我们需要排版时,可以对字符串设置各种格式,生成NSAttributeString;
- 然后用NSAttributeString去创建CTFramesetter类,
- CTFramesetter会处理排版信息,然后生成排版后的结果CTFrame;
- CTFrame是一段或者多段文本,每段文本又由多行文字组成,每行的表示为CTLine;
- CTLine是一行文本,每行文本由多个CTRun组成,CTRun是一小段连续的字形;
- CTTypeSetter负责上下文相关排版处理,比如说换行,每个CTFrame中都会有一个CTTypeSetter; 他们之间的关系图如下:
总的来说,CTFramesetter是生成CTFrame的工厂类,初始化参数是attributed string,会在内部创建CTTypesetter并进行实际的排版;
CTLine类似每一行的文字,CTRun是一行中具有相同属性的连续字形,比如说“我正在分享阅读器”,就会由三个CTRun组成,分别是“我正在”、“分享”、“阅读器”(因为“分享”两个字加粗了,否则就会是一个CTRun)。
CoreText的使用流程:
- 使用core text就是有一个要显示的string,
- 然后定义这个string每个部分的样式生成富文本attributedString
- 由富文本生成 CTFramesetter
- CTFramesetter得到CTFrame
- 使用绘制(CTFrameDraw)CTFrame
关键函数介绍
由富文本字符串得到CTFramesetter
- CTFramesetterCreateWithAttributedString(att as CFAttributedString)
-
CFAttributedString是NSAttributedString的CF对象,可以直接强转;
CTFramesetterRef CTFramesetterCreateWithAttributedString( CFAttributedStringRef string );
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都已生成。
通过上面的介绍,把这几个函数连起来,就是数据准备阶段的核心方法:
- 根据txt内容生成String -> 在由String生成富文本-> 由富文本生成framesetter,
- 根据页面大小计算生成单页的CTFrame
- 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 = 0var frameArray: [CTFrame] = []var i: Int = 0repeat {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..<endIndex])pageStart = pageRange.location + pageRange.lengthprint("第\(i)页" ,pageRange, onePage)i+=1frameArray.append(frame)} while(pageStart < contentStr.count )self.frameArray = frameArray}
渲染方法
-
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.identityctx.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