iOS程序猿iOSiOS 开发每天分享优质文章

CoreText 学习笔记(下)

2018-12-04  本文已影响11人  ChinaChong

唐巧原博客地址:
基于 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

运行情况

上一篇下一篇

猜你喜欢

热点阅读