图文混排的关键:CTRunRef 与 CTRunDelegate

2020-08-23  本文已影响0人  苏沫离

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 可能具有非单位矩阵,或者 CTLineorigin 可能是非零原点!

/// 获取存储在 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

CTRunDelegateCTRunRef 的代理回调,通过 Delegate 可以手动设置 CTRunRefAscentDescentWidth等属性,这是图文混排的基础;插入一个空白的字符,将其字符的大小设置为(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:小说阅读器的文字分页、图文混排

上一篇下一篇

猜你喜欢

热点阅读