8. CoreText

苹果文档 https://developer.apple.com/documentation/coretext

Core Text是和Core Graphics配合使用的,一般是在UIView的drawRect方法中的Graphics Context上进行绘制的。 且Core Text真正负责绘制的是文本部分,图片还是需要自己去手动绘制,所以你必须关注很多绘制的细节部分。


image.png

一、CoreText框架

CoreText 框架中最常用的几个类:
(1)、CTFont
(2)、CTFontCollection
(3)、CTFontDescriptor
(4)、CTFrame
(5)、CTFramesetter
(6)、CTGlyphInfo
(7)、CTLine
(8)、CTParagraphStyle
(9)、CTRun
(10)、CTTextTab
(11)、CTTypesetter

CoreText的介绍

Core Text 是基于 iOS 3.2+ 和 OSX 10.5+ 的一种能够对文本格式和文本布局进行精细控制的文本引擎。它良好的结合了 UIKit 和 Core Graphics/Quartz:
UIKit 的 UILabel 允许你通过在 IB 中简单的拖曳添加文本,但你不能改变文本的颜色和其中的单词。 Core Graphics/Quartz几乎允许你做任何系统允许的事情,但你需要为每个字形计算位置,并画在屏幕上。
Core Text 正结合了这两者!你可以完全控制位置、布局、类似文本大小和颜色这样的属性,而 Core Text 将帮你完善其它的东西——类似文本换行、字体呈现等等。

CoreText基础用法

  //1.获取当前设备上下文
    CGContextRef context = UIGraphicsGetCurrentContext();
    //2.翻转坐标系(coreText坐标系是以左下角为坐标原点,UIKit以左上角为坐标原点)
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    CGContextTranslateCTM(context, 0, self.bounds.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);
    //3.设置绘制区域
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, CGRectMake(10, 20, self.bounds.size.width - 20, self.bounds.size.height - 40));
    //4.设置文本内容
    NSAttributedString *attString = [[NSAttributedString alloc] initWithString:@"Hello world"];
    //5.设置CTFrame
    CTFramesetterRef ctFrameSetting = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString);
    CTFrameRef ctFrame = CTFramesetterCreateFrame(ctFrameSetting, CFRangeMake(0, [attString length]), path, NULL);
    //6.在CTFrame中绘制文本关联到上下文
    CTFrameDraw(ctFrame, context);
    //7.释放变量
    CFRelease(path);
    CFRelease(ctFrameSetting);
    CFRelease(ctFrame);

TextKit

@interface XXMViewController (){
    UIImageView *imageView;
    UITextView *textView;
}

@end

@implementation XXMViewController
- (void)viewDidLoad
{
    [super viewDidLoad];
    
    XMView *view = [[XMView alloc] init];
    view.frame = CGRectMake(10, 100, 300, 300);
    view.backgroundColor = [UIColor redColor];
    [self.view addSubview:view];
    
    NSString *str = @"\tWhen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knowlages";//xxx为文字内容
    
    //初始化和定义位置
    textView = [[UITextView alloc] initWithFrame:CGRectMake(10, 20,self.view.frame.size.width-20,    self.view.frame.size.height-30)];
    //内容赋值
    textView.text = str;
    //添加到屏幕上
    [self.view addSubview:textView];
    imageView = [[UIImageView alloc] initWithFrame:CGRectMake(10, 80, 160, 100)];
    imageView.backgroundColor = [UIColor orangeColor];
    
    imageView.image = [UIImage imageNamed:@"bd_logo1"];
    [self.view addSubview:imageView];
    textView.textContainer.exclusionPaths = @[[self translatedBezierPath]];
}
- (UIBezierPath *)translatedBezierPath
{
    CGRect imageRect = [textView convertRect:imageView.frame fromView:self.view];
    UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRect:CGRectMake(imageRect.origin.x+5, imageRect.origin.y, imageRect.size.width-5, imageRect.size.height-5)];
    return bezierPath;
}
@end

严肃的就是iOS7新推出的类库Textkit,其实是在之前推出的CoreText上的封装,根据苹果的说法,他们开发了两年多才完成,而且他们在开发时候也将表情混排作为一个使用案例进行研究,所以要实现表情混排将会非常容易

苹果引入TextKit的目的并非要取代已有的CoreText框架,虽然CoreText的主要作用也是用于文字的排版和渲染,但它是一种先进而又处于底层技术,如果我们需要将文本内容直接渲染到图形上下文(Graphics context)时,从性能和易用性来考虑最佳方案就是使用CoreText


富文本

富文本是什么呢?
简单的说,附带有每一个文字属性的字符串,就是富文本。
在iOS中,我们有一个专门的类来处理富文本 AttributeString


富文本的基本使用方法

AttributedString也分为NSAttributedStringNSMutableAttributedString两个类,类似于String,我就不赘述了。
富文本本质上没有什么难度,只要给指定的字符串附上指定的属性就好了。下面给出富文本的一些基本方法。

  • -initWithString:`以NSString初始化一个富文本对象
  • -setAttributes:range:为富文本中的一段范围添加一些属性,第一个参数是个NSDictionary字典,第二个参数是NSRange。
  • -addAttribute:value:range:添加一个属性
  • -addAttributes:range:添加多个属性
  • -removeAttribute:range:移除属性
NSDictionary * dic = @{NSFontAttributeName:[UIFont fontWithName:@"Zapfino" size:20],NSForegroundColorAttributeName:[UIColor redColor],NSUnderlineStyleAttributeName:@(NSUnderlineStyleSingle)};
    NSMutableAttributedString * attributeStr = [[NSMutableAttributedString alloc] initWithString:@"0我是一个富文本,9听说我有很多属性,19I will try。32这里清除属性."];
//    设置属性
    [attributeStr setAttributes:dic range:NSMakeRange(0, attributeStr.length)];
//    添加属性
    [attributeStr addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:30] range:NSMakeRange(9, 10)];
    [attributeStr addAttribute:NSForegroundColorAttributeName value:[UIColor cyanColor] range:NSMakeRange(13, 13)];
//    添加多个属性
    NSDictionary * dicAdd = @{NSBackgroundColorAttributeName:[UIColor yellowColor],NSLigatureAttributeName:@1};
    [attributeStr addAttributes:dicAdd range:NSMakeRange(19, 13)];
//    移除属性
    [attributeStr removeAttribute:NSFontAttributeName range:NSMakeRange(32, 9)];
    UILabel * label = [[UILabel alloc] initWithFrame:CGRectMake(100, 100, 200, 400)];
    label.numberOfLines = 0;
    label.attributedText = attributeStr;

这里你要注意一下,给label的一定是给他的attributedText属性,你给text是不行的。
是不是用起来很简单,富文本,跟字典没什么区别么。


CoreText绘制富文本

CoreText实现图文混排其实就是在富文本中插入一个空白的图片占位符的富文本字符串,通过代理设相关的图片尺寸信息,根据从富文本得到的frame计算图片绘制的frame再绘制图片这么一个过程。

先来整体代码

#import "XMView.h"

#import <CoreText/CoreText.h>

@implementation XMView
-(void)drawRect:(CGRect)rect
{
    [super drawRect:rect];
    //1.获取当前上下文
    CGContextRef context = UIGraphicsGetCurrentContext();
    //翻转坐标系步骤
    //设置当前文本矩阵
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    //文本沿y轴移动
    CGContextTranslateCTM(context, 0, self.bounds.size.height);
    //文本翻转成为CoreText坐标系
    CGContextScaleCTM(context, 1, -1);
    
    NSMutableAttributedString * attributeStr = [[NSMutableAttributedString alloc] initWithString:@"\tWhen I will learn CoreText, i think it will hard for me.But it is easy.\n\tIn fact,if you bengin learn, you can know that every thing is easy when you start.you just need some knowlages"];
    
    //2.设置文本内容的属性
    //1设置部分文字颜色
    [attributeStr addAttribute:(id)kCTForegroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(0 , 27)];
    //2设置部分文字字体
    CGFloat fontSize = 20;
    CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
    [attributeStr addAttribute:(id)kCTFontAttributeName value:(__bridge id)fontRef range:NSMakeRange(0, 27)];
    //3设置斜体
    CTFontRef italicFontRef = CTFontCreateWithName((CFStringRef)[UIFont italicSystemFontOfSize:20].fontName, 16, NULL);
    [attributeStr addAttribute:(id)kCTFontAttributeName value:(__bridge id)italicFontRef range:NSMakeRange(27, 9)];
    //4设置下划线
    [attributeStr addAttribute:(id)kCTUnderlineStyleAttributeName value:(id)[NSNumber numberWithInteger:kCTUnderlineStyleDouble] range:NSMakeRange(36, 10)];
    //5设置下划线颜色
    [attributeStr addAttribute:(id)kCTUnderlineColorAttributeName value:(id)[UIColor greenColor].CGColor range:NSMakeRange(36, 10)];
    //6设置空心字
    long number1 = 2;
    CFNumberRef numRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt8Type, &number1);
    [attributeStr addAttribute:(id)kCTStrokeWidthAttributeName value:(__bridge id)numRef range:NSMakeRange(56, 10)];
    //7设置字体间距
    long number = 10;
    CFNumberRef num = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt8Type, &number);
    [attributeStr addAttribute:(id)kCTKernAttributeName value:(__bridge id)num range:NSMakeRange(40, 10)];
    //8设置行间距
    CGFloat lineSpacing = 10;
    const CFIndex kNumberOfSettings = 3;
    CTParagraphStyleSetting theSettings[kNumberOfSettings] = {
        {kCTParagraphStyleSpecifierLineSpacingAdjustment, sizeof(CGFloat), &lineSpacing},
        {kCTParagraphStyleSpecifierMaximumLineHeight, sizeof(CGFloat), &lineSpacing},
        {kCTParagraphStyleSpecifierMinimumLineHeight, sizeof(CGFloat), &lineSpacing}
    };
    CTParagraphStyleRef theParagraphRef = CTParagraphStyleCreate(theSettings, kNumberOfSettings);
    [attributeStr addAttribute:(id)kCTParagraphStyleAttributeName value:(__bridge id)theParagraphRef range:NSMakeRange(0, [attributeStr length])];
    
#pragma mark - 空白占位符及代理设置
    //3.CTRunDelegateCallBacks:用于保存指针的结构体,由CTRun delegate进行回调
    CTRunDelegateCallbacks callBacks;
    memset(&callBacks,0,sizeof(CTRunDelegateCallbacks));
    callBacks.version = kCTRunDelegateVersion1;
    callBacks.getAscent = ascentCallBacks;
    callBacks.getDescent = descentCallBacks;
    callBacks.getWidth = widthCallBacks;
    
    //图片信息字典
    NSDictionary * dicPic = @{@"height":@129,@"width":@400};
    
    //创建CTRunDelegate的代理
    CTRunDelegateRef delegate = CTRunDelegateCreate(& callBacks, (__bridge void *)dicPic);
    unichar placeHolder = 0xFFFC;    //使用oxFFFC作为空白占位符
    NSString * placeHolderStr = [NSString stringWithCharacters:&placeHolder length:1];
    NSMutableAttributedString * placeHolderAttrStr = [[NSMutableAttributedString alloc] initWithString:placeHolderStr];
    CFAttributedStringSetAttribute((CFMutableAttributedStringRef)placeHolderAttrStr, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);
    CFRelease(delegate);
    [attributeStr insertAttributedString:placeHolderAttrStr atIndex:12];
    CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributeStr);
    
    //4.根据attributeStr生成CTFramesetterRef
    //1.创建绘制区域,显示的区域可以用CGMUtablePathRef生成任意的形状
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, self.bounds);
    NSInteger length = attributeStr.length;
    CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, length), path, NULL);
    CTFrameDraw(frame, context);
    
    //5.绘制图像
    UIImage * image = [UIImage imageNamed:@"bd_logo1"];
    CGRect imgFrm = [self calculateImageRectWithFrame:frame];
    CGContextDrawImage(context,imgFrm, image.CGImage);
    //6.释放
    CFRelease(frame);
    CFRelease(path);
    CFRelease(frameSetter);
}
static CGFloat ascentCallBacks(void * ref)
{
    return [(NSNumber *)[(__bridge NSDictionary *)ref valueForKey:@"height"] floatValue];
}
static CGFloat descentCallBacks(void * ref)
{
    return 0;
}
static CGFloat widthCallBacks(void * ref)
{
    return [(NSNumber *)[(__bridge NSDictionary *)ref valueForKey:@"width"] floatValue];
}

-(CGRect)calculateImageRectWithFrame:(CTFrameRef)frame
{
    NSArray * arrLines = (NSArray *)CTFrameGetLines(frame);
    NSInteger count = [arrLines count];
    CGPoint points[count];
    CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), points);
    for (int i = 0; i < count; i ++) {
        CTLineRef line = (__bridge CTLineRef)arrLines[i];
        NSArray * arrGlyphRun = (NSArray *)CTLineGetGlyphRuns(line);
        for (int j = 0; j < arrGlyphRun.count; j ++) {
            CTRunRef run = (__bridge CTRunRef)arrGlyphRun[j];
            NSDictionary * attributes = (NSDictionary *)CTRunGetAttributes(run);            CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[attributes valueForKey:(id)kCTRunDelegateAttributeName];
            if (delegate == nil) {
                continue;
            }
            NSDictionary * dic = CTRunDelegateGetRefCon(delegate);
            if (![dic isKindOfClass:[NSDictionary class]]) {
                continue;
            }
            CGPoint point = points[i];
            CGFloat ascent;
            CGFloat descent;
            CGRect boundsRun;
            boundsRun.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
            boundsRun.size.height = ascent + descent;
            CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
            boundsRun.origin.x = point.x + xOffset;
            boundsRun.origin.y = point.y - descent;
            CGPathRef path = CTFrameGetPath(frame);
            CGRect colRect = CGPathGetBoundingBox(path);
            CGRect imageBounds = CGRectOffset(boundsRun, colRect.origin.x, colRect.origin.y);
            return imageBounds;
        }
    }
    return CGRectZero;
}
#pragma mark - CoreText基础用法
/**
 *  基础的coreText用法
 */
- (void)baseCoreTextUsingMethod {
    //1.获取当前设备上下文
    CGContextRef context = UIGraphicsGetCurrentContext();
    //2.翻转坐标系(coreText坐标系是以左下角为坐标原点,UIKit以左上角为坐标原点)
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    CGContextTranslateCTM(context, 0, self.bounds.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);
    //3.设置绘制区域
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, CGRectMake(10, 20, self.bounds.size.width - 20, self.bounds.size.height - 40));
    //4.设置文本内容
    NSAttributedString *attributeStr = [[NSAttributedString alloc] initWithString:@"Hello world"];
    //5.设置CTFrame
    CTFramesetterRef ctFrameSetting = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributeStr);
    CTFrameRef ctFrame = CTFramesetterCreateFrame(ctFrameSetting, CFRangeMake(0, [attributeStr length]), path, NULL);
    //6.在CTFrame中绘制文本关联到上下文
    CTFrameDraw(ctFrame, context);
    //7.释放变量
    CFRelease(path);
    CFRelease(ctFrameSetting);
    CFRelease(ctFrame);
}

不瞒你说,我看着代码都烦,也怕,所以放心,老司机会一句一句给你解释的。


分段解析

准备工作

CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetTextMatrix(context, CGAffineTransformIdentity);    
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);

先要来一个背景介绍哈

/*
 coreText 起初是为OSX设计的,而OSX得坐标原点是左下角,y轴正方向朝上。iOS中坐标原点是左上角,y轴正方向向下。
 若不进行坐标转换,则文字从下开始,还是倒着的
    如下图(盗的图,别打我)
 */

image.png
这四句什么意思呢?
首先第一句。
CGContextRef context = UIGraphicsGetCurrentContext();//获取当前绘制上下文

为什么要回去上下文呢?因为我们所有的绘制操作都是在上下文上进行绘制的。

然后剩下的三句。
CGContextSetTextMatrix(context, CGAffineTransformIdentity);//设置字形的变换矩阵为不做图形变换  
    CGContextTranslateCTM(context, 0, self.bounds.size.height);//平移方法,将画布向上平移一个屏幕高  
    CGContextScaleCTM(context, 1.0, -1.0);//缩放方法,x轴缩放系数为1,则不变,y轴缩放系数为-1,则相当于以x轴为轴旋转180度

正如之上的背景说的,coreText使用的是系统坐标,然而我们平时所接触的iOS的都是屏幕坐标,所以要将屏幕坐标系转换系统坐标系,这样才能与我们想想的坐标互相对应。
事实上呢,这三句是翻转画布的固定写法,这三句你以后会经常看到的。

继续。


图片的代理的设置

/*
  事实上,图文混排就是在要插入图片的位置插入一个富文本类型的占位符。通过CTRUNDelegate设置图片
*/

NSMutableAttributedString * attributeStr = [[NSMutableAttributedString alloc] initWithString:@"\n这里在测试图文混排,\n我是一个富文本"];//这句不用我多说吧,最起码得有个富文本啊才能插入不是。

/*
 设置一个回调结构体,告诉代理该回调那些方法
 */
CTRunDelegateCallbacks callBacks;//创建一个回调结构体,设置相关参数
memset(&callBacks,0,sizeof(CTRunDelegateCallbacks));//memset将已开辟内存空间 callbacks 的首 n 个字节的值设为值 0, 相当于对CTRunDelegateCallbacks内存空间初始化
callBacks.version = kCTRunDelegateVersion1;//设置回调版本,默认这个
callBacks.getAscent = ascentCallBacks;//设置图片顶部距离基线的距离
callBacks.getDescent = descentCallBacks;//设置图片底部距离基线的距离
callBacks.getWidth = widthCallBacks;//设置图片宽度

注意了,这里经GreyLove提醒有重要改动,就是这里,添加了memset,码字的时候少忘码了一句话。怪我粗心,十分抱歉,我会通知每一个留言的同学。

为什么要设置一个回调结构体呢?
因为coreText中大量的调用c的方法。事实上你会发现大部分跟系统底层有关的都需要调c的方法。所以设置代理要按照人家的方法来啊。

看看这几句代码也很好懂,就是注释中写的意思。
后三句分别就是说当我需要走这些代理的时候都会走那些代理方法
好吧,扯到这又要补充知识了。这个距离什么东西呢?

image.png

对对,这呢就是一个CTRun的尺寸图,什么你问CTRun是啥?还没到那呢,后面会详细介绍。
在这你只要知道,一会我们绘制图片的时候实际上实在一个CTRun中绘制这个图片,那么CTRun绘制的坐标系中,他会以origin点作为原点进行绘制。
基线为过原点的x轴ascent即为CTRun顶线距基线的距离descent即为底线距基线的距离
我们绘制图片应该从原点开始绘制,图片的高度及宽度及CTRun的高度及宽度,我们通过代理设置CTRun的尺寸间接设置图片的尺寸。


/*
 创建一个代理
*/
    NSDictionary * dicPic = @{@"height":@129,@"width":@400};//创建一个图片尺寸的字典,初始化代理对象需要
    CTRunDelegateRef delegate = CTRunDelegateCreate(& callBacks, (__bridge void *)dicPic);//创建代理

上面只是设置了回调结构体,然而我们还没有告诉这个代理我们要的图片尺寸
所以这句话就在设置代理的时候绑定了一个返回图片尺寸的字典
事实上此处你可以绑定任意对象。此处你绑定的对象既是回调方法中的参数ref

好吧就然说到这我就直接把那三个回调方法说了吧,放在一起比较好理解一些。

static CGFloat ascentCallBacks(void * ref)
{
    return [(NSNumber *)[(__bridge NSDictionary *)ref valueForKey:@"height"] floatValue];
}
static CGFloat descentCallBacks(void * ref)
{
    return 0;
}
static CGFloat widthCallBacks(void * ref)
{
    return [(NSNumber *)[(__bridge NSDictionary *)ref valueForKey:@"width"] floatValue];
}

上文说过,ref既是创建代理是绑定的对象。所以我们在这里,从字典中分别取出图片的宽和高

值得注意的是,由于是c的方法,所以也没有什么对象的概念。是一个指针类型的数据。不过oc的对象其实也就是c的结构体。我们可以通过类型转换获得oc中的字典。
__bridge既是C的结构体转换成OC对象时需要的一个修饰词

老司机敲字慢啊,敲到这都两个小时了,容我喝口水。

你们喝过红色的尖叫么?老司机喝了那种烟头泡的水之后精神满满的继续敲字。(那水超难喝,你可以挑战一下)
诶,说好的严肃呢?


图片的插入

首先创建一个富文本类型的图片占位符,绑定我们的代理

unichar placeHolder = 0xFFFC;//创建空白字符
    NSString * placeHolderStr = [NSString stringWithCharacters:&placeHolder length:1];//已空白字符生成字符串
    NSMutableAttributedString * placeHolderAttrStr = [[NSMutableAttributedString alloc] initWithString:placeHolderStr];//用字符串初始化占位符的富文本
    CFAttributedStringSetAttribute((CFMutableAttributedStringRef)placeHolderAttrStr, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);//给字符串中的范围中字符串设置代理
    CFRelease(delegate);//释放(__bridge进行C与OC数据类型的转换,C为非ARC,需要手动管理)

这里富文本的知识上文中已经介绍过了。不过老司机猜你有三个疑问。

  • 这个添加属性的方法怎么是这个样子的?
    因为这里是添加CTRunDelegate这种数据类型,要用CoreText专门的方法,不过其实就是形式不同,作用一样的。
  • 为什么这里富文本类型转换的时候不用_bridge呢?老司机你不是说需要修饰词么?你是不是骗我?(markDown语法冲突我少打一个下划线)
    真没有,事实上不是所有数据转换的时候都需要__bridge。你要问我怎么区分?那好我告诉你,C中就是传递指针的数据就不用。比如说字符串,数组。原因老司机现在解释不通,等我能组织好语言的。
  • 为什么还要释放?我是ARC环境啊
    不好意思,我也是。不过为什么要释放呢?因为你进行了类型转换之后就不属于对象了,也不再归自动引用计数机制管理了,所以你得手动管理咯。

然后将占位符插入到我们的富文本中

[attributeStr insertAttributedString:placeHolderAttrStr atIndex:12];//将占位符插入原富文本

此处我就不赘述了,富文本的知识你只要类比字典就好了。
至此,我们已经生成好了我们要的带有图片信息的富文本了,接下来我们只要在画布上绘制出来这个富文本就好了。


绘制

绘制呢,又分成两部分,绘制文本绘制图片。你问我为什么还分成两个?

因为富文本中你添加的图片只是一个带有图片尺寸的空白占位符啊,你绘制的时候他只会绘制出相应尺寸的空白占位符,所以什么也显示不了啊。
那怎么显示图片啊?拿到占位符的坐标,在占位符的地方绘制相应大小的图片就好了。恩,说到这,图文混排的原理已经说完了。

先来绘制文本吧。


绘制文本
CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributeStr);//一个frame的工厂,负责生成frame
CGMutablePathRef path = CGPathCreateMutable();//创建绘制区域
CGPathAddRect(path, NULL, self.bounds);//添加绘制尺寸
NSInteger length = attributeStr.length;
CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0,length), path, NULL);//工厂根据绘制区域及富文本(可选范围,多次设置)设置frame
CTFrameDraw(frame, context);//根据frame绘制文字

frameSetter是根据富文本生成的一个frame生成的工厂,你可以通过framesetter以及你想要绘制的富文本的范围获取该CTRun的frame
但是你需要注意的是,获取的frame是仅绘制你所需要的那部分富文本的frame。即当前情况下,你绘制范围定为(10,1),那么你得到的尺寸是只绘制(10,1)的尺寸,他应该从屏幕左上角开始(因为你改变了坐标系),而不是当你绘制全部富文本时他该在的位置

然后建立一会绘制的尺寸,实际上就是在指定你的绘制范围
接着生成整个富文本绘制所需要的frame。因为范围是全部文本,所以获取的frame即为全部文本的frame(此处老司机希望你一定要搞清楚全部与指定范围获取的frame他们都是从左上角开始的,否则你会进入一个奇怪的误区,稍后会提到的)。
最后,根据你获得的frame,绘制全部富文本


绘制图片

上面你已经绘制出文字,不过没有图片哦,接下来绘制图片。
绘制图片用下面这个方法,通用的哦

CGContextDrawImage(context,imgFrm, image.CGImage);//绘制图片

我们可以看到这个方法有三个参数,分别是context,frame,以及image
要什么就给他什么好咯,context和image都好说,context就是当前的上下文,最开始获得那个。image就是你要添加的那个图片,不过是CGImage类型。通过UIImage转出CGImage就好了,我们重点讲一下frame的获取。

frame的获取

记得我之前说的误区么?这里我们要获得Image的frame,你有没有想过我们的frameSetter?

我也想过,不过就像我说的,你单独用frameSetter求出的image的frame是不正确的,那是只绘制image而得的坐标,所以哪种方法不能用哦,要用下面的方法。

你们一定发现,我获取frame的方法单独写了一个方法,为什么呢?
1.将代码分离,方便修改。
2.最主要的是这部分代码到哪里都能用,达到复用效果。

NSArray * arrLines = (NSArray *)CTFrameGetLines(frame);//根据frame获取需要绘制的线的数组
NSInteger count = [arrLines count];//获取线的数量
CGPoint points[count];//建立起点的数组(cgpoint类型为结构体,故用C语言的数组)
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), points);//获取起点

第一句呢,获取绘制frame中的所有CTLine。CTLine,又不知道了吧,老司机又要无耻的盗图了。

image.png

上面呢,我们能看到一个CTFrame绘制的原理。

  • CTLine 可以看做Core Text绘制中的一行的对象 通过它可以获得当前行的line ascent,line descent ,line leading,还可以获得Line下的所有Glyph Runs
  • CTRun 或者叫做 Glyph Run,是一组共享想相同attributes(属性)的字形的集合体

一个CTFrame有几个CTLine组成,有几行文字就有几行CTLine。一个CTLine有包含多个CTRun,一个CTRun是所有属性都相同的那部分富文本的绘制单元。所以CTRun是CTFrame的基本绘制单元
接着说我们的代码。
为什么我获取的数组需要进行类型转换呢?因为CTFrameGetLines()返回值是CFArrayRef类型的数据。就是一个c的数组类型吧,暂且先这么理解,所以需要转换。

那为什么不用__bridge呢?记得么,我说过,本身就传地址的数据是不用桥接的。就是这样。
然后获取数组的元素个数。有什么用呢,因为我们要用到每个CTLine的原点坐标进行计算。每个CTLine都有自己的origin。所以要生成一个相同元素个数的数组去盛放origin对象
然后用CTFrameGetLineOrigins获取所有原点。
到此,我们计算frame的准备工作完成了。才完成准备工作。


计算frame

思路呢,就是遍历我们的frame中的所有CTRun,检查他是不是我们绑定图片的那个,如果是,根据该CTRun所在CTLine的origin以及CTRun在CTLine中的横向偏移量计算出CTRun的原点,加上其尺寸即为该CTRun的尺寸。

跟绕口令是的,不过就是这么个思路。

for (int i = 0; i < count; i ++) {//遍历线的数组
        CTLineRef line = (__bridge CTLineRef)arrLines[i];
        NSArray * arrGlyphRun = (NSArray *)CTLineGetGlyphRuns(line);//获取GlyphRun数组(GlyphRun:高效的字符绘制方案)
        for (int j = 0; j < arrGlyphRun.count; j ++) {//遍历CTRun数组
            CTRunRef run = (__bridge CTRunRef)arrGlyphRun[j];//获取CTRun
            NSDictionary * attributes = (NSDictionary *)CTRunGetAttributes(run);//获取CTRun的属性
            CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[attributes valueForKey:(id)kCTRunDelegateAttributeName];//获取代理
            if (delegate == nil) {//非空
                continue;
            }
            NSDictionary * dic = CTRunDelegateGetRefCon(delegate);//判断代理字典
            if (![dic isKindOfClass:[NSDictionary class]]) {
                continue;
            }
            CGPoint point = points[i];//获取一个起点
            CGFloat ascent;//获取上距
            CGFloat descent;//获取下距
            CGRect boundsRun;//创建一个frame
            boundsRun.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
            boundsRun.size.height = ascent + descent;//取得高
            CGFloat xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);//获取x偏移量
            boundsRun.origin.x = point.x + xOffset;//point是行起点位置,加上每个字的偏移量得到每个字的x
            boundsRun.origin.y = point.y - descent;//计算原点
            CGPathRef path = CTFrameGetPath(frame);//获取绘制区域
            CGRect colRect = CGPathGetBoundingBox(path);//获取剪裁区域边框
            CGRect imageBounds = CGRectOffset(boundsRun, colRect.origin.x, colRect.origin.y);
            return imageBounds;

有了上面的思路这里就很好理解了。
外层for循环呢,是为了取到所有的CTLine
类型转换什么的我就不多说了,然后通过CTLineGetGlyphRuns获取一个CTLine中的所有CTRun
里层for循环是检查每个CTRun。
通过CTRunGetAttributes拿到该CTRun的所有属性
通过kvc取得属性中的代理属性
接下来判断代理属性是否为空。因为图片的占位符我们是绑定了代理的,而文字没有。以此区分文字和图片。
如果代理不为空,通过CTRunDelegateGetRefCon取得生成代理时绑定的对象判断类型是否是我们绑定的类型,防止取得我们之前为其他的富文本绑定过代理
如果两条都符合,ok,这就是我们要的那个CTRun
开始计算该CTRun的frame吧。
获取原点和获取宽高被。
通过CTRunGetTypographicBounds取得宽,ascent和descent。有了上面的介绍我们应该知道图片的高度就是ascent+descent了吧。
接下来获取原点。
CTLineGetOffsetForStringIndex获取对应CTRun的X偏移量
取得对应CTLine的原点的Y,减去图片的下边距才是图片的原点,这点应该很好理解。
至此,我们已经获得了图片的frame了。因为只绑定了一个图片,所以直接return就好了,如果多张图片可以继续遍历返回数组。
获取到图片的frame,我们就可以绘制图片了,用上面介绍的方法。


哦,别忘了手动释放你创建的对象哦。

CFRelease(frame);
CFRelease(path);
CFRelease(frameSetter);

大功告成。


好了,至此你已经完成图片的绘制了。只要在ViewController里面引入你绘制CoreText文本的View正常的初始化添加子视图就可以了。

作者:老司机Wicky
链接:https://www.jianshu.com/p/6db3289fb05d
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

参考:
CoreText(一):基本用法
系列文章:

CoreText原理及基本使用方法

# iOS:基于CoreText的排版引擎

CoreText分页

TextKit 实现文本分页

下面也提供一下CoreText的分页方法,毕竟CoreText分页的速度会快一点,同样在设置字体的时候,中文内容要用<code>Heiti</code>或<code>PingFang</code>

//CoreText 分页
+ (NSArray *)coreTextPaging:(NSAttributedString *)str textFrame:(CGRect)textFrame{
    NSMutableArray *pagingResult = [NSMutableArray array];
    CFAttributedStringRef cfAttStr = (__bridge CFAttributedStringRef)str;
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(cfAttStr);
    CGPathRef path = CGPathCreateWithRect(textFrame, NULL);
    int textPos = 0; 
    NSUInteger strLength = [str length];
    while (textPos < strLength)  {
        //设置路径
        CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(textPos, 0), path, NULL);
        //生成frame
        CFRange frameRange = CTFrameGetVisibleStringRange(frame);
        NSRange ra = NSMakeRange(frameRange.location, frameRange.length);
       //获取范围并转换为NSRange,然后以NSAttributedString形式保存
        [pagingResult addObject:[str attributedSubstringFromRange:ra]];
        //移动当前文本位置
        textPos += frameRange.length;
        CFRelease(frame);
    }
    CGPathRelease(path);
    CFRelease(framesetter);
    return pagingResult;
}

简单的demo:demo地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容