CoreText 学习笔记(下)

作者: ChinaChong | 来源:发表于2018-12-04 10:15 被阅读11次

    唐巧原博客地址:
    基于 CoreText 的排版引擎:进阶 | 唐巧的博客

    前篇写了CoreText绘制纯文本的学习心得,本篇将继续记录学习图文混排的心得。与前篇一样,本文还是主要记录一下宏观大纲,不会把细节的内容搬过来。如果各位同学有兴趣,可以去 基于 CoreText 的排版引擎:进阶 | 唐巧的博客

    CoreText图文混排

    首先了解一个概念,什么是 CTLineCTRun

    CTFrame内部,是由多个CTLine来组成的,每个CTLine代表一行,每个CTLine又是由多个CTRun来组成,每个CTRun代表一组显示风格一致的文本。我们不用手工管理CTLineCTRun的创建过程。

    下图是一个CTLineCTRun的示意图,可以看到,第三行的CTLine是由 2 个CTRun构成的,第一个CTRun为红色大字号的左边部分,第二个CTRun为右边字体较小的部分。

    引自:《基于 CoreText 的排版引擎:进阶 | 唐巧的博客》

    图文混排具体思路

    图文混排的思路与纯文本排版的思路大体一致。与前篇一样,前三步仍然是:

    1. 获取绘制上下文context

    2. 反转坐标系

    3. 获取绘制区域高度

    由于前篇说过了前三步,完全一样,就不再说了。从第四步开始有变化,并增加了第五步绘制图片,请看下面脑图

    第四步 第五步

    第四步:创建富文本字符串(NSAttributedString)

    与前篇的第四步相比只有创建富文本字符串(NSAttributedString)这里不一样,所以只说这里。

    可以把图文混排看作是绘制了两次,说到底第一次绘制的都是纯文本,与前篇基本一致。CoreText并不支持绘制图片,所以在第一次绘制时是用一个特殊字符作为占位符,给图片预留出绘制位置。第二次绘制图片,把对应的图片绘制到对应的占位符位置上。

    下面是创建第一次绘制所需富文本的源码:

    NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] init];
    
    NSMutableAttributedString *aStr1 = [[NSMutableAttributedString alloc] initWithString:@"图文混排需要绘制两遍,第一遍绘制文本,但是要在应该绘制图片的位置留下占位符" attributes:@{(id)kCTForegroundColorAttributeName : [UIColor redColor]}];
    
    NSMutableAttributedString *imgPlh1 = [self makePlaceholder:self.imageArray.firstObject];
    
    NSMutableAttributedString *aStr2 = [[NSMutableAttributedString alloc] initWithString:@"给占位符设置代理,代理包括了占位符的宽高" attributes:@{(id)kCTForegroundColorAttributeName : [UIColor blueColor]}];
    
    NSMutableAttributedString *imgPlh2 = [self makePlaceholder:self.imageArray.lastObject];
    
    NSMutableAttributedString *aStr3 = [[NSMutableAttributedString alloc] initWithString:@"最后需要确定图片的绘制位置,CTRunGetTypographicBounds()获取图片占位符的宽高,CTLineGetOffsetForStringIndex()获取占位符在这一行中x方向偏移量" attributes:@{(id)kCTForegroundColorAttributeName : [UIColor blackColor]}];
    
    [attributedString appendAttributedString:aStr1];
    [attributedString appendAttributedString:imgPlh1];
    [attributedString appendAttributedString:aStr2];
    [attributedString appendAttributedString:imgPlh2];
    [attributedString appendAttributedString:aStr3];
    

    在构造富文本的时候代码中调用了 makePlaceholder: 方法,传进去的参数是包含图片信息的字典。

    方法一进来就先构造了图片占位符的代理,代理通过三个回调函数保存了占位符的宽高。widthCallback 返回的是宽度,ascentCallback + descentCallback 就是高度。ascentCallback 返回的是文字底线向上的距离, descentCallback 返回的是文字底线向下的距离,这两者之和构成了占位符的高度。三个回调函数的参数 void *ref 是函数 CTRunDelegateCreate(); 传进去的 imgDict

    接着使用 0xFFFC 作为空白占位符,构造富文本(NSMutableAttributedString)。最后调用 CFAttributedStringSetAttribute() 函数设置占位符的代理。

    需要注意的地方是:三个回调函数中的参数是 void * 类型,不是OC指针,所以在调用函数 CTRunDelegateCreate(); 时传参数先要将OC对象 imgDict 转换成 void * 类型。在转换的时候要注意 imgDict 出了 makePlaceholder: 函数作用域后的引用计数,如果 imgDict 是在 makePlaceholder: 函数作用域内初始化的参数,出了作用域就会被销毁,在转换类型时可以使用 __bridge_retained,具体原因可以去看我写的另一篇博客 iOS __bridge那些事

    makePlaceholder:方法源码

    #pragma mark - 生成图片空白的占位符,并且设置其CTRunDelegate信息。
    
    static CGFloat ascentCallback(void *ref){ // 从文字底线开始向上的边距
        return [(NSNumber*)[(__bridge NSDictionary*)ref objectForKey:@"height"] floatValue];
    }
    
    static CGFloat descentCallback(void *ref){ // 从文字底线开始向下的边距
        return 0;
    }
    
    static CGFloat widthCallback(void* ref){
        return [(NSNumber*)[(__bridge NSDictionary*)ref objectForKey:@"width"] floatValue];
    }
    
    - (NSMutableAttributedString *)makePlaceholder:(NSDictionary *)imgDict {
        CTRunDelegateCallbacks callbacks;
        memset(&callbacks, 0, sizeof(CTRunDelegateCallbacks)); // 将callbacks内存清零
        callbacks.version = kCTRunDelegateVersion1;
        callbacks.getAscent = ascentCallback;
        callbacks.getDescent = descentCallback;
        callbacks.getWidth = widthCallback;
        // 通过 ascentCallback + descentCallback + widthCallback 就使得delegate拥有了图片位置的宽高
        CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (__bridge void *)(imgDict));
        
        // 使用 0xFFFC 作为空白的占位符
        unichar objectReplacementChar = 0xFFFC;
        NSString * content = [NSString stringWithCharacters:&objectReplacementChar length:1];
        NSMutableAttributedString * space =
        [[NSMutableAttributedString alloc] initWithString:content
                                               attributes:@{}];
        // 给 AttributedString 设置代理,代理包括了图片占位符的宽高
        CFAttributedStringSetAttribute((CFMutableAttributedStringRef)space,
                                       CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);
        CFRelease(delegate);
        return space;
    }
    

    第五步:绘制图片

    绘制图片的函数是 CGContextDrawImage(CGContextRef c, CGRect rect, CGImageRef image)

    CGContextDrawImage 函数需要三个参数,第一个 CGContext c 绘制上下文在第一步时已获取过。

    第二个参数 CGRect rect 是图片绘制的具体位置,我们会在接下来专门说一说怎么样确定图片绘制的具体位置。

    第三个参数 CGImageRef image 是你想想要绘制的图片。可以是本地图片,也可以是网络图片,总之这个参数是已知的。

    先说第三个参数,我使用的懒加载构造的数组包含字典的数据结构:

    #pragma mark - 图片数据源
    
    - (NSMutableArray *)imageArray {
        if (_imageArray == nil) {
            _imageArray = [NSMutableArray array];
            NSMutableDictionary *dict1 = [NSMutableDictionary dictionary];
            dict1[@"height"] = @50;
            dict1[@"width"] = @50;
            dict1[@"name"] = @"wxz1.jpeg";
            
            NSMutableDictionary *dict2 = [NSMutableDictionary dictionary];
            dict2[@"height"] = @220;
            dict2[@"width"] = @240;
            dict2[@"name"] = @"wxz2.jpg";
            
            [_imageArray addObject:dict1];
            [_imageArray addObject:dict2];
        }
        return _imageArray;
    }
    

    图片资源有了,然后就需要计算图片绘制的具体位置了。捋一捋下面源码的思路:最终目的是找到占位符所在的几个 run ,通过函数 CTRunGetTypographicBounds() 可以从 run 中获取占位符的宽和高,通过函数 CTLineGetOffsetForStringIndex() 可以从 run 中获取其在本行中 x 方向的偏移量,加上行原点坐标的 x 值就是占位符的实际 x 值。

    #pragma mark - 确定图片绘制位置
    
    - (void)confirmImagePosition {
        if (self.imageArray.count == 0) {
            return;
        }
        // 从ctFrame里拿出所有“行”
        NSArray *lines = (NSArray *)CTFrameGetLines(self.ctFrame);
        // 总“行”数
        NSUInteger lineCount = [lines count];
        // 这个 lineOrigins 数组用来保存所有“行”的原点坐标
        CGPoint lineOrigins[lineCount];
        // 取出所有“行”的原点坐标,保存到 lineOrigins 数组中去
        CTFrameGetLineOrigins(self.ctFrame, CFRangeMake(0, 0), lineOrigins);
        
        int imgIndex = 0;
        NSMutableDictionary *imageData = self.imageArray[0];
        
        for (int i = 0; i < lineCount; ++i) {
            if (imageData == nil) {
                break;
            }
            // 循环取出每一“行”
            CTLineRef line = (__bridge CTLineRef)lines[i];
            // 从“行”里取出所有 run
            NSArray * runObjArray = (NSArray *)CTLineGetGlyphRuns(line);
            for (id runObj in runObjArray) {
                // 循环取出每一个 run
                CTRunRef run = (__bridge CTRunRef)runObj;
                // 从 run 里取出 Attributes
                NSDictionary *runAttributes = (NSDictionary *)CTRunGetAttributes(run);
                // 从 Attributes 里取出 CTRunDelegate
                CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[runAttributes valueForKey:(id)kCTRunDelegateAttributeName];
                // delegate为空则跳过这一个 run ,因为我们要做的是设置图片的imagePosition,只有图片设置了delegate
                if (delegate == nil) {
                    continue;
                }
                /*
                 * RefCon 是给图片设置代理时传进去的参数 imgDict
                 *
                 * CTRunDelegateCreate(&callbacks, (__bridge void *)(imgDict));
                 */
                NSDictionary * metaDic = CTRunDelegateGetRefCon(delegate);
                if (![metaDic isKindOfClass:[NSDictionary class]]) {
                    continue;
                }
                
                CGRect runBounds;
                CGFloat ascent;
                CGFloat descent;
                // 从 run 里获取占位符的宽高,即图片应有的宽高
                runBounds.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
                runBounds.size.height = ascent + descent;
                
                // 获取这个 run 在这一“行”中 x轴方向的偏移量
                CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
                // 确定 run 的绘制坐标
                runBounds.origin.x = lineOrigins[i].x + xOffset;
                runBounds.origin.y = lineOrigins[i].y;
                runBounds.origin.y -= descent;
                
                imageData[@"imagePosition"] = @(runBounds);
                
                imgIndex++;
                if (imgIndex == self.imageArray.count) {
                    imageData = nil;
                    break;
                } else {
                    imageData = self.imageArray[imgIndex];
                }
            }
        }
    }
    

    至此图文混排思路已经全部说完了,不喜欢剧透的同学可以去自己尝试一下,做一个图文混排的demo了。

    牛刀小试

    按照上面的思路,我写了一个小demo,依然只是为了快速记忆上面的逻辑,不考虑代码的结构优化。

    //
    //  GCDisplayView.h
    //  CoreTextImageText
    //
    //  Created by 崇 on 2018.
    //  Copyright © 2018 崇. All rights reserved.
    //
    
    #import <UIKit/UIKit.h>
    
    @interface GCDisplayView : UIView
    
    @property (nonatomic, assign) CGFloat textHeight;
    
    @end
    
    
    //
    //  GCDisplayView.m
    //  CoreTextImageText
    //
    //  Created by 崇 on 2018.
    //  Copyright © 2018 崇. All rights reserved.
    //
    
    #import "GCDisplayView.h"
    #import <CoreText/CoreText.h>
    
    @interface GCDisplayView()
    
    @property (nonatomic, assign) CTFramesetterRef framesetter;
    @property (nonatomic, assign) CTFrameRef ctFrame;
    @property (nonatomic, strong) NSMutableArray *imageArray;
    
    @end
    
    @implementation GCDisplayView
    
    - (instancetype)initWithCoder:(NSCoder *)aDecoder {
        self = [super initWithCoder:aDecoder];
        if (self) {
            // 创建 CTFrame
            [self createCTFrame];
        }
        return self;
    }
    
    - (void)drawRect:(CGRect)rect {
        // 获取绘制上下文
        CGContextRef context = UIGraphicsGetCurrentContext();
        
        // 初始化文本矩阵
        CGContextSetTextMatrix(context, CGAffineTransformIdentity);
        
        // 平移一个View高度
        CGContextTranslateCTM(context, 0, self.bounds.size.height);
        
        // 反转 y 轴
        CGContextScaleCTM(context, 1.0, -1.0);
        
        // 绘制
        CTFrameDraw(self.ctFrame, context);
        
        // 绘制图片
        for (NSDictionary *imageDict in self.imageArray) {
            UIImage *image = [UIImage imageNamed:imageDict[@"name"]];
            if (image) {
                CGContextDrawImage(context, [imageDict[@"imagePosition"] CGRectValue], image.CGImage);
            }
        }
        
        // 释放
        CFRelease(self.ctFrame);
        CFRelease(self.framesetter);
    }
    
    #pragma mark - 创建 CTFrame
    
    - (void)createCTFrame {
        /*
         创建 CTFrame 需要两个参数:CTFramesetter 和 CGMutablePath。
         
         先创建 CTFramesetter,利用 CTFramesetter 计算出绘制区域高度后再创建 CGMutablePath。
         
         创建 CTFramesetter 需要先创建 NSAttributedString。
         */
        
        NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] init];
    
        NSMutableAttributedString *aStr1 = [[NSMutableAttributedString alloc] initWithString:@"图文混排需要绘制两遍,第一遍绘制文本,但是要在应该绘制图片的位置留下占位符" attributes:@{(id)kCTForegroundColorAttributeName : [UIColor redColor]}];
    
        NSMutableAttributedString *imgPlh1 = [self makePlaceholder:self.imageArray.firstObject];
    
        NSMutableAttributedString *aStr2 = [[NSMutableAttributedString alloc] initWithString:@"给占位符设置代理,代理包括了占位符的宽高" attributes:@{(id)kCTForegroundColorAttributeName : [UIColor blueColor]}];
    
        NSMutableAttributedString *imgPlh2 = [self makePlaceholder:self.imageArray.lastObject];
    
        NSMutableAttributedString *aStr3 = [[NSMutableAttributedString alloc] initWithString:@"最后需要确定图片的绘制位置,CTRunGetTypographicBounds()获取图片占位符的宽高,CTLineGetOffsetForStringIndex()获取占位符在这一行中x方向偏移量" attributes:@{(id)kCTForegroundColorAttributeName : [UIColor blackColor]}];
    
        [attributedString appendAttributedString:aStr1];
        [attributedString appendAttributedString:imgPlh1];
        [attributedString appendAttributedString:aStr2];
        [attributedString appendAttributedString:imgPlh2];
        [attributedString appendAttributedString:aStr3];
        
        // 用创建好的 attString 创建 framesetter
        self.framesetter =
        CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributedString);
        
        // 获得要绘制的区域的高度
        CGSize restrictSize = CGSizeMake(self.bounds.size.width, CGFLOAT_MAX);
        CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(self.framesetter, CFRangeMake(0,0), nil, restrictSize, nil);
        self.textHeight = coreTextSize.height;
        
        // 创建 CGMutablePath
        CGMutablePathRef path = CGPathCreateMutable();
        CGPathAddRect(path, NULL, CGRectMake(0, 0, self.bounds.size.width, self.textHeight));
        
        // 创建 ctFrame
        self.ctFrame =
        CTFramesetterCreateFrame(self.framesetter, CFRangeMake(0, 0), path, NULL);
        
        // 确定图片绘制位置
        [self confirmImagePosition];
        
        CFRelease(path);
    }
    
    #pragma mark - 生成图片空白的占位符,并且设置其CTRunDelegate信息。
    
    static CGFloat ascentCallback(void *ref){ // 从文字底线开始向上的边距
        return [(NSNumber*)[(__bridge NSDictionary*)ref objectForKey:@"height"] floatValue];
    }
    
    static CGFloat descentCallback(void *ref){ // 从文字底线开始向下的边距
        return 1;
    }
    
    static CGFloat widthCallback(void* ref){
        
        return [(NSNumber*)[(__bridge NSDictionary*)ref objectForKey:@"width"] floatValue];
    }
    
    - (NSMutableAttributedString *)makePlaceholder:(NSDictionary *)imgDict {
        CTRunDelegateCallbacks callbacks;
        memset(&callbacks, 0, sizeof(CTRunDelegateCallbacks)); // 将callbacks内存清零
        callbacks.version = kCTRunDelegateVersion1;
        callbacks.getAscent = ascentCallback;
        callbacks.getDescent = descentCallback;
        callbacks.getWidth = widthCallback;
        // 通过 ascentCallback + descentCallback + widthCallback 就使得delegate拥有了图片位置的宽高
        CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (__bridge void *)(imgDict));
        
        // 使用 0xFFFC 作为空白的占位符
        unichar objectReplacementChar = 0xFFFC;
        NSString * content = [NSString stringWithCharacters:&objectReplacementChar length:1];
        NSMutableAttributedString * space =
        [[NSMutableAttributedString alloc] initWithString:content
                                               attributes:@{}];
        // 给 AttributedString 设置代理,代理包括了图片占位符的宽高
        CFAttributedStringSetAttribute((CFMutableAttributedStringRef)space,
                                       CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);
        CFRelease(delegate);
        return space;
    }
    
    #pragma mark - 确定图片绘制位置
    
    - (void)confirmImagePosition {
        if (self.imageArray.count == 0) {
            return;
        }
        // 从ctFrame里拿出所有“行”
        NSArray *lines = (NSArray *)CTFrameGetLines(self.ctFrame);
        // 总“行”数
        NSUInteger lineCount = [lines count];
        // 这个 lineOrigins 数组用来保存所有“行”的原点坐标
        CGPoint lineOrigins[lineCount];
        // 取出所有“行”的原点坐标,保存到 lineOrigins 数组中去
        CTFrameGetLineOrigins(self.ctFrame, CFRangeMake(0, 0), lineOrigins);
        
        int imgIndex = 0;
        NSMutableDictionary *imageData = self.imageArray[0];
        
        for (int i = 0; i < lineCount; ++i) {
            if (imageData == nil) {
                break;
            }
            // 循环取出每一“行”
            CTLineRef line = (__bridge CTLineRef)lines[i];
            // 从“行”里取出所有 run
            NSArray * runObjArray = (NSArray *)CTLineGetGlyphRuns(line);
            for (id runObj in runObjArray) {
                // 循环取出每一个 run
                CTRunRef run = (__bridge CTRunRef)runObj;
                // 从 run 里取出 Attributes
                NSDictionary *runAttributes = (NSDictionary *)CTRunGetAttributes(run);
                // 从 Attributes 里取出 CTRunDelegate
                CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[runAttributes valueForKey:(id)kCTRunDelegateAttributeName];
                // delegate为空则跳过这一个 run ,因为我们要做的是设置图片的imagePosition,只有图片设置了delegate
                if (delegate == nil) {
                    continue;
                }
                /*
                 * RefCon 是给图片设置代理时传进去的参数 imgDict
                 *
                 * CTRunDelegateCreate(&callbacks, (__bridge void *)(imgDict));
                 */
                NSDictionary * metaDic = CTRunDelegateGetRefCon(delegate);
                if (![metaDic isKindOfClass:[NSDictionary class]]) {
                    continue;
                }
                
                CGRect runBounds;
                CGFloat ascent;
                CGFloat descent;
                // 从 run 里获取占位符的宽高,即图片应有的宽高
                runBounds.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
                runBounds.size.height = ascent + descent;
                
                // 获取这个 run 在这一“行”中 x轴方向的偏移量
                CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
                // 确定 run 的绘制坐标
                runBounds.origin.x = lineOrigins[i].x + xOffset;
                runBounds.origin.y = lineOrigins[i].y;
                runBounds.origin.y -= descent;
                
                imageData[@"imagePosition"] = @(runBounds);
                
                imgIndex++;
                if (imgIndex == self.imageArray.count) {
                    imageData = nil;
                    break;
                } else {
                    imageData = self.imageArray[imgIndex];
                }
            }
        }
    }
    
    #pragma mark - 图片数据源
    
    - (NSMutableArray *)imageArray {
        if (_imageArray == nil) {
            _imageArray = [NSMutableArray array];
            NSMutableDictionary *dict1 = [NSMutableDictionary dictionary];
            dict1[@"height"] = @50;
            dict1[@"width"] = @50;
            dict1[@"name"] = @"wxz1.jpeg";
            
            NSMutableDictionary *dict2 = [NSMutableDictionary dictionary];
            dict2[@"height"] = @220;
            dict2[@"width"] = @240;
            dict2[@"name"] = @"wxz2.jpg";
            
            [_imageArray addObject:dict1];
            [_imageArray addObject:dict2];
        }
        return _imageArray;
    }
    
    @end
    
    

    运行情况

    相关文章

      网友评论

        本文标题:CoreText 学习笔记(下)

        本文链接:https://www.haomeiwen.com/subject/qspzxqtx.html