1、文本属性 (Attributes
) 的最小单元:CTRunRef
/// 或者叫做 Glyph Run,是一组相同属性(Attributes)的字形的集合体
typedef const struct CF_BRIDGED_TYPE(id) __CTRun * CTRunRef;
CTLineRef
是渲染到屏幕上的一行字形的集合,如果再细分,那么CTRunRef
就是某一行 CTLineRef
上属性 Attributes
相同的字形的集合体;CTLineRef
是由一个或者多个CTRunRef
组成!
1.1、获取包含的字形
CTRunRef
是排版时属性 Attributes
相同的基础单位,包括一个或多个字形;
/// 字形实际上是一个 unsigned short 的类型
typedef unsigned short CGGlyph;
/// 获取 CTRunRef 包含的字形个数
CFIndex CTRunGetGlyphCount(CTRunRef run);
/// 获取 CTRunRef 包含的字形 : 该数组长度等于 CTRunGetGlyphCount() 返回值
const CGGlyph * _Nullable CTRunGetGlyphsPtr(CTRunRef run);
/** 将指定范围的字形复制到用户提供的缓冲区中
* @param range 指定范围;如果 range.location = 0 ,range.length = CTRunGetGlyphCount ,则全部复制
* 如果 range.length = 0 ,则从 range.location 开始复制到结尾;
* @param buffer 缓冲区,长度要足够使用
*/
void CTRunGetGlyphs(CTRunRef run,CFRange range,CGGlyph buffer[_Nonnull]);
1.1.1、 获取字符对应的坐标位置
CTRunRef
可以获取到每个字符对应的位置:相对于 CTLine
的原点的位置:
/// 获取存储在 CTRunRef 中的每个字形的位置
const CGPoint * _Nullable CTRunGetPositionsPtr(CTRunRef run);
/// 拷贝存储在 CTRunRef 中的指定范围的字形的位置
void CTRunGetPositions(CTRunRef run,CFRange range,CGPoint buffer[_Nonnull]);
1.1.2、获取字符对应的索引
获取 CTRunRef
每个字形的索引:映射到存储区中的字形
/// 获取在 CTRunRef 中存储的字形索引
const CFIndex * _Nullable CTRunGetStringIndicesPtr(CTRunRef run);
/// 获取(拷贝)存储在 CTRunRef 中的指定范围的字形的索引
void CTRunGetStringIndices(CTRunRef run,CFRange range,CFIndex buffer[_Nonnull]);
1.1.3、获取字符对应的 advance
文字默认排版时,宽度由 advance width
指定,但是仅靠 advances
并不足以在 CTLine
中正确地定位字形,因为 CTRunRef
可能具有非单位矩阵,或者 CTLine
的 origin
可能是非零原点!
/// 获取存储在 CTRunRef 中的每个字形的 advance
const CGSize * _Nullable CTRunGetAdvancesPtr(CTRunRef run);
/// 获取(拷贝)存储在 CTRunRef 中的指定范围的字形的 advance
void CTRunGetAdvances(CTRunRef run,CFRange range,CGSize buffer[_Nonnull]);
/** 获取(拷贝)存储在 CTRunRef 中的指定范围的字形的 advances 和 origins
* @discussion CTRunRef 的 base advances 和 origins 决定字形的位置,在用于绘图之前需要进行额外的处理。
* 当前字形的实际位置由其原点从起始位置的偏移量决定,而下一个字形的位置由当前字形base advance 从起始位置的偏移量决定。
*/
void CTRunGetBaseAdvancesAndOrigins(CTRunRef runRef,CFRange range,
CGSize advancesBuffer[_Nullable],CGPoint originsBuffer[_Nullable]);
1.2、获取CTRunRef
的文字属性
获取的文字属性,可能来自 NSAttributeString
,也可能来自于内部排版引擎的生成:
/// 获取 CTRunRef 的属性
CFDictionaryRef CTRunGetAttributes(CTRunRef run);
1.3、获取CTRunRef
的文字范围
CTRunRef
可以获取生成时的Range,以便定位到这段文字在整体的位置;
///获取用于创建 CTRunRef 的字符范围
CFRange CTRunGetStringRange(CTRunRef run);
1.4、获取CTRunRef
的排版 size
/** 获取 CTRunRef 的指定范围的字符的排版边界
* @param range 指定范围;如果 range.location = 0 ,range.length = CTRunGetGlyphCount ,则是整个 CTRunRef;
* 如果 range.length = 0 ,则从 range.location 开始复制到结尾;
* @param ascent 上行高度;回调函数,如果不需要,可以将其设置为NULL。
* @param descent 下行高度;基线距字体中最低的字形底部的距离,是一个负值
* @param leading 行距
* @result 排版宽度;如果 CTRunRef 或 CFRange 无效,则返回 0
* @discussion 行高 lineHeight = ascent + |descent| + linegap
*/
double CTRunGetTypographicBounds(CTRunRef run, CFRange range, CGFloat * _Nullable ascent,
CGFloat * _Nullable descent,CGFloat * _Nullable leading);
/** 计算 CTRunRef 中指定范围的字形绘制成图像所需要的 bounds :一个紧密包含字形的边界
* @param context 计算图像 bounds 的上下文,可以传 NULL;
* @discussion 计算这行文字绘制成图片所需要的最小 size,没有各种边距,是一种是尽可能小的理想状态的size
* @result 如果行无效,将返回 CGRectNull;
*/
CGRect CTRunGetImageBounds(CTRunRef run,CGContextRef _Nullable context,CFRange range);
1.5、其它函数
/// 由 CTRunGetStatus() 传回的位字段,用于指示 CTRunRef 的处理
typedef CF_OPTIONS(uint32_t, CTRunStatus){
kCTRunStatusNoStatus = 0, /// 没有特殊的属性 attributes
kCTRunStatusRightToLeft = (1 << 0), /// 设置文本从右向左书写
kCTRunStatusNonMonotonic = (1 << 1), ///以某种方式重新排序,字符串索引不再严格地从左到右的递增或从右到左的递减
kCTRunStatusHasNonIdentityMatrix = (1 << 2) /// CTRunRef 需要在当前 CGContext 中设置一个特定的文本矩阵来进行适当的绘图
};
/** 获取 CTRunRef 的状态
* @discussion 除了属性 attributes 之外,CTRunRef 还具有可用于加快某些操作的状态:
* 知道 CTRunRef 的方向和顺序可以为字符串索引分析提供帮助;
* 知道位置是否引用标识文本矩阵可以避免额外比较;
* @note 该状态不是严格必要的,仅仅是为了方便
*/
CTRunStatus CTRunGetStatus(CTRunRef run);
/** 获取绘制此 CTRunRef 所需的文本矩阵
* @note 一个CTLine里面会包括多个CTRun,每个CTRun都包括各自的位置信息,
* 在排版的时候可以通过CTRunGetTextMatrix获取相应的位置,
* 再通过 CGContextSetTextMatrix() 设置到CGContext
*/
CGAffineTransform CTRunGetTextMatrix(CTRunRef run);
/** 绘制 CTRunRef
* @discussion 还可以通过访问其 glyphs、positions 和text matrix 来复杂的绘制 CTRunRef。
* 与调用 CTLineDraw() 绘制包含 CTRun 的整个 CTLine 不同;
* CTRun 如果有下划线,将不会被绘制,因为下划线可能依赖于该 CTLine 中的其他CTRun;
*/
void CTRunDraw(CTRunRef run,CGContextRef context,CFRange range);
2、回调代理 CTRunDelegate
CTRunDelegate
是 CTRunRef
的代理回调,通过 Delegate
可以手动设置 CTRunRef
的Ascent
、Descent
、Width
等属性,这是图文混排的基础;插入一个空白的字符,将其字符的大小设置为(width
, height
),留出对应的大小空白区域,然后在排版结束完通过 CGContextDrawImage()
在对应的位置插入Image
就实现了图文混排的效果;
typedef const struct CF_BRIDGED_TYPE(id) __CTRunDelegate * CTRunDelegateRef;
2.1、CTRunDelegate
回调函数
/** 当 CTRunDelegate 的保留计数达到 0 且CTRunDelegate 被释放时的回调函数
* @param refCon 创建 CTRunDelegate 的传入的参数,一般是关于 Ascent、Descent、Width 等度量信息;
*/
typedef void (*CTRunDelegateDeallocateCallback)(void * refCon);
/// 上行高度的回调
typedef CGFloat (*CTRunDelegateGetAscentCallback)(void * refCon);
/// 下行高度的回调
typedef CGFloat (*CTRunDelegateGetDescentCallback)(void * refCon);
/// 宽度的回调
typedef CGFloat (*CTRunDelegateGetWidthCallback)(void * refCon);
///回调的版本号,作为参数传递给CTRunDelegateCreate() 函数
enum {
kCTRunDelegateVersion1 = 1,
kCTRunDelegateCurrentVersion = kCTRunDelegateVersion1
};
/** 包含 CTRunDelegate 的回调函数的结构
* @discussion 这些回调函数由开发者提供,用于在布局期间修改字形度量。
*/
typedef struct{
CFIndex version; ///建议设置为 kCTRunDelegateCurrentVersion
CTRunDelegateDeallocateCallback dealloc; // 设置为 NULL
CTRunDelegateGetAscentCallback getAscent; // 设置为 NULL 时,默认为 0
CTRunDelegateGetDescentCallback getDescent;
CTRunDelegateGetWidthCallback getWidth;
} CTRunDelegateCallbacks;
2.2、创建代理 CTRunDelegate
/** 创建一个代理 CTRunDelegate
* @param callbacks 该代理的回调
* @refCon 一般是关于 Ascent、Descent、Width 等度量信息
* @discussion 该代理常用来占位:保留一片空白区域绘制图片
*/
CTRunDelegateRef _Nullable CTRunDelegateCreate(const CTRunDelegateCallbacks* callbacks,void * _Nullable refCon);
/// 获取创建 CTRunDelegate 的传入的 refCon:一般是关于 Ascent、Descent、Width 等度量信息
void * CTRunDelegateGetRefCon(CTRunDelegateRef runDelegate);
3、CTRunRef
函数使用
3.1、 图文混排中图片的处理
CoreText
实际上并没有相应API直接将一个图片转换为 CTRun
并进行绘制,它所能做的只是为图片预留响应的空白区域,而真正的绘制则是交由CoreGraphics
完成。
NSAttributedStringKey const kYLAttachmentAttributeName = @"com.yl.attachment";
//富文本中的链接(图片、网页)
@interface YLAttachment : NSObject
//链接
@property (nonatomic ,copy) NSString *url;
//网页的标题
@property (nonatomic ,copy) NSString *title;
//图片的相关信息
@property (nonatomic ,strong) UIImage *image;
@property (nonatomic ,assign) CGRect imageFrame;
@end
3.1.1、文字排版时为图片的展示占位
///上行高度
static CGFloat ascentCallback(void *ref){
YLAttachment *model = (__bridge YLAttachment *)ref;
return model.imageFrame.size.height;
}
///下行高度
static CGFloat descentCallback(void *ref){
return 0;
}
///图片宽度
static CGFloat widthCallback(void *ref){
YLAttachment *model = (__bridge YLAttachment *)ref;
return model.imageFrame.size.width;
}
/** 将图片处理为 CoreText
* @param image 图片
* @param drawSize 画布的尺寸,图片的宽高不能超出 drawSize
*/
+ (NSAttributedString *)parseImage:(UIImage *)image drawSize:(CGSize)drawSize{
/**************** 计算图片宽高 **************/
CGSize imageShowSize = image.size;//屏幕上展示的图片尺寸
if (image.size.width > drawSize.width) {
imageShowSize = CGSizeMake(drawSize.width, image.size.height / image.size.width * drawSize.width);
}
YLAttachment *model = [[YLAttachment alloc]init];
model.image = image;
model.imageFrame = CGRectMake(0, 0, imageShowSize.width, imageShowSize.height);
//注意:此处返回的富文本,最主要的作用是占位!
//为图片的绘制留下空白区域
CTRunDelegateCallbacks callbacks;
memset(&callbacks, 0, sizeof(CTRunDelegateCallbacks));
callbacks.version = kCTRunDelegateVersion1;//设置回调版本,默认这个
callbacks.getAscent = ascentCallback;//上行高度
callbacks.getDescent = descentCallback;//下行高度
callbacks.getWidth = widthCallback;//图片宽度
CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (__bridge void *)model);
//使用0xFFFC作为空白占位符
unichar objectReplacementChar = 0xFFFC;
NSString *content = [NSString stringWithCharacters:&objectReplacementChar length:1];
NSMutableAttributedString *placeholder = [[NSMutableAttributedString alloc] initWithString:content attributes:@{kYLAttachmentAttributeName:model}];
CFAttributedStringSetAttribute((CFMutableAttributedStringRef)placeholder, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);
CFRelease(delegate);
return placeholder;
}
3.1.2、 矫正图片的坐标错位问题
绘制图片的时候实际上在一个 CTRunRef
中,以它坐标系为基准,以 origin
点作为原点进行绘制:使用 frameSetter
求出的 image
的坐标是不正确的,需要我们另行计算:
/** 矫正 CTFrame 中的图片坐标
* 思路: 遍历 CTFrameRef 中的所有 CTRun,检查 CTRun 否绑定图片,
* 如果是,根据 CTRun 所在 CTLine 的 origin 以及在 CTLine 中的横向偏移量计算出 CTRun 的原点,
* 加上其尺寸即为该CTRun的尺寸
*/
+ (void)setImageFrametWithCTFrame:(CTFrameRef)frame{
CFArrayRef lines = CTFrameGetLines(frame);
int lineCount = (int)CFArrayGetCount(lines);
CGPoint points[lineCount];
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), points);
for (int i = 0; i < lineCount; i ++) {//外层for循环,为了取到所有的 CTLine
CTLineRef line = CFArrayGetValueAtIndex(lines, i);
CFArrayRef glyphRuns = CTLineGetGlyphRuns(line);
int runCount = (int)CFArrayGetCount(glyphRuns);
for (int j = 0; j < runCount ; j ++) {//内层for循环,检查每个 CTRun
CTRunRef run = CFArrayGetValueAtIndex(glyphRuns, j);
CFDictionaryRef attributes = CTRunGetAttributes(run);
CTRunDelegateRef delegate = CFDictionaryGetValue(attributes, kCTRunDelegateAttributeName);;//获取代理属性
if (delegate == nil) {
continue;
}
YLAttachment *model = CTRunDelegateGetRefCon(delegate);
if (![model isKindOfClass:[YLAttachment class]]) {
continue;
}
CGPoint linePoint = points[i];//获取当前 CTLine 的原点
CGFloat ascent; //上行高度
CGFloat descent; //下行高度
CGFloat leading = 0; //行距
CGRect boundsRun;
//获取宽、高
boundsRun.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, &leading);
boundsRun.size.height = ascent + fabs(descent) + leading;
//获取对应 CTRun 的 X 偏移量
CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
boundsRun.origin.x = linePoint.x + xOffset;
boundsRun.origin.y = linePoint.y - descent - leading;//图片原点
CGPathRef path = CTFrameGetPath(frame);//获取绘制区域
CGRect colRect = CGPathGetBoundingBox(path);//获取绘制区域边框
model.imageFrame = CGRectOffset(boundsRun, colRect.origin.x, colRect.origin.y);//设置图片坐标
}
}
}
3.1.3、 获取 CTFrameRef
中的所有图片插件
///获取 CTFrameRef 中的所有图片插件
+ (NSMutableArray<YLAttachment *> *)getImagesWithCTFrame:(CTFrameRef)frame{
NSMutableArray *resultArray = [NSMutableArray array];
CFArrayRef lines = CTFrameGetLines(frame);
int lineCount = (int)CFArrayGetCount(lines);
CGPoint points[lineCount];
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), points);
for (int i = 0; i < lineCount; i ++) {//外层for循环,为了取到所有的 CTLine
CTLineRef line = CFArrayGetValueAtIndex(lines, i);
CFArrayRef glyphRuns = CTLineGetGlyphRuns(line);
int runCount = (int)CFArrayGetCount(glyphRuns);
for (int j = 0; j < runCount ; j ++) {//内层for循环,检查每个 CTRun
CTRunRef run = CFArrayGetValueAtIndex(glyphRuns, j);
CFDictionaryRef attributes = CTRunGetAttributes(run);
if (attributes) {
if (CFDictionaryContainsKey(attributes, kYLAttachmentAttributeName)) {
YLAttachment *attachment = CFDictionaryGetValue(attributes, kYLAttachmentAttributeName);;//获取属性
if (attachment && attachment.image) {
[resultArray addObject:attachment];
}
}
}
}
}
return resultArray;
}
3.1.4、 绘制图片
- (void)drawRect:(CGRect)rect{
//1.获取当前绘图上下文
CGContextRef context = UIGraphicsGetCurrentContext();
//2.旋转坐坐标系(默认和UIKit坐标是相反的)
CGContextSetTextMatrix(context, CGAffineTransformIdentity);//设置当前文本矩阵
CGContextTranslateCTM(context, 0, CGRectGetHeight(rect));//文本沿y轴移动
CGContextScaleCTM(context, 1.0, -1.0);//文本翻转成为CoreText坐标系
//3.绘制文字
CTFrameDraw(_frameRef, context);
//4.绘制图片
[[YLCoreText getImagesWithCTFrame:_frameRef] enumerateObjectsUsingBlock:^(YLAttachment * _Nonnull attachment, NSUInteger idx, BOOL * _Nonnull stop) {
CGContextDrawImage(context, attachment.imageFrame, attachment.image.CGImage);
}];
}
阅读器点击链接 | 阅读器仿真翻页 | 阅读器覆盖翻页 | 阅读器其它翻页 |
---|---|---|---|
点击链接.gif | 仿真翻页.gif | 覆盖翻页.gif | 平移、滚动、无效果等翻页.gif |
第一篇 CoreText的简单了解
第二篇 CoreText 排版与布局
第三篇 CTLineRef 的函数库及使用
第四篇 图文混排的关键 CTRunRef 与 CTRunDelegate
Demo:小说阅读器的文字分页、图文混排
网友评论