图文混排, 文本点击事件, 文本动画(不在讨论之内,可以通过对view来处理),如果是简单的操作,可以用UITextView
来处理,当需要特殊的定制化的时候可以采用CoreText
,它是一个强大的文本处理框架,支持所有文本排列相关的定制化操作,配合CoreGraphics
可以实现图文混排功能。
实现原理
CoreText是一个底层的文本绘制框架,它真正绘制的其实只是文本部分,如果涉及到图片还是需要手动去的绘制的,它主要是通过对文本的段落样式,行样式,字体大小,样式配置信息统一搜集后,计算出它的layout再进行绘制,它主要包括以下几个关键类。
- CTFramesetter (排版)
- CFFrame (段落,框架)
- CFLine (行)
- CTRun (文字)
- CFTTypeSetter(排字)
原点位置
- CoreText中的文本原点位置和UIView是基于X轴承镜像对称,Y坐标相反
Appkit->NSView: leftBottom
UIKit->UIView: topRight
CoreGraphic->Context: letBottom
CoreText对象模型
CFAttributedStringRef: 属性字符串,用于存储所徐哟啊绘制额文字字符和属性
CTFramesetterRef: 对应的是CTFramesetter,通过
CFAttributedStringRef
进行初始化,它作为CTFrame
的生产工厂,负责根据对应的Path
生成对应的CTFrame
CTFrame: 可以通过
CTFrameDraw
函数直接绘制到Context
上,在绘制之前可以通过CTLine
进行一些参数的微调CTLine:
CTFrame
内部是由多个CTLine
组成,每个CTLine
可以看作一行CTRun: 或者叫做
Glyph Run
,每个CTLine
是由多恶搞CTRun
组成,CTRun
是一组显示风格一致的文本(类似Flutter中的TextSpan
),是一组有着相同attributes
(属性)的字型集合体
通常处理步骤
- 获取当前的上下文对象
- 翻转坐标系,Y轴对称
- 创建
AttributedString
,转化为对应的CTFramesetterRef
- 创建绘制区域:
CGPathRef
- 根据
CTFramesetterRef
和CGPathRef
创建CTFrame
CGFrameDraw
绘制文字
func layoutParagraph() {
let context = UIGraphicsGetCurrentContext()!
//1. 反转Y,Coregraphics的坐标系在坐下角
context.translateBy(x: 0, y: self.bounds.size.height);
context.scaleBy(x: 1.0, y: -1.0)
context.textMatrix = CGAffineTransform.identity;
//2. 创建路径
let path = CGMutablePath();
let rect = CGRect(x: 10.0, y: 10.0, 200.0, height: 200.0);
path.addRect(rect)
//3.创建`CFAttributedString`
let data = "Hello, World! I know nothing in the world that has as much power as a word. Sometimes I write one, and I look at it, until it begins to shine.".withCString{ $0 }
let cfString = CFStringCreateWithCString(kCFAllocatorDefault,data, CFStringBuiltInEncodings.UTF8.rawValue)
let attrString =
CFAttributedStringCreateMutable(kCFAllocatorDefault, 0)!;
CFAttributedStringReplaceString(attrString, CFRangeMake(0, 0), cfString)
//4.设置部分文本的属性
let colorSpace = CGColorSpaceCreateDeviceRGB()
let components = [CGFloat(0.0), CGFloat(0.3), CGFloat(0.3), CGFloat(0.8) ].withUnsafeBufferPointer{ $0 }
let redColor = CGColor(colorSpace: colorSpace, components: components.baseAddress!)
CFAttributedStringSetAttribute(attrString, CFRangeMake(0, 12),
kCTForegroundColorAttributeName, redColor);
//5.创建文本布局对象
let framesetter = CTFramesetterCreateWithAttributedString(attrString)
//6. 绘制片段
let frame = CTFramesetterCreateFrame(framesetter,
CFRangeMake(0,0), path, nil);
//7.执行绘制
CTFrameDraw(frame, context)
}
局部点击判断
- 由于
CoreText
中的文本是基于CTFrame
进行排版的,它包括了多个CTLine
,所以很容易得到各个line
的位置和大小,判断点击在不在某个line
上,CTLine
又可以进一步判断点击的文字是否在CTLine
的指定坐标上,通过便利这个String的NSTextCheckingResult
结果,根据Rang
计算出文字的具体位置。
图文混排
CoreText
本身不支持图片绘制,图片绘制需要使用Core Graphics
,CoreText
只是通过CCTRun
的设置为图片的绘制提供预留的空间,这个设置需要使用CTRunDelegate
,CTRunDelegate作为CTRun相关属性或操作扩展的一个入口,使得我们可以对CTRun做一些自定义的行为。为图片留位置的方法就是加入一个空白的CTRun,自定义其ascent,descent,width等参数,使得绘制文本的时候留下空白位置给相应的图片。然后图片在相应的空白位置上使用Core Graphics接口进行绘制。使用CTRunDelegateCreate可以创建一个CTRunDelegate,它接收两个参数,一个是callbacks结构体,一个是所有callback调用的时候需要传入的对象。 callbacks的结构体为CTRunDelegateCallbacks,主要是包含一些回调函数,比如有返回当前run的ascent,descent,width这些值的回调函数,至于函数中如何鉴别当前是哪个run,可以在CTRunDelegateCreate的第二个参数来达到目的,因为CTRunDelegateCreate的第二个参数会作为每一个回调调用时的入参。
参考链接
CoreText Programming Guide: https://developer.apple.com/library/archive/documentation/StringsTextFonts/Conceptual/CoreText_Programming/Introduction/Introduction.html#//apple_ref/doc/uid/TP40005533