CoreText那些事儿

一、前言

当我们的产品某天站在你身后拍着你的肩膀说:"Hi guys,看看这个效果可以做不,对,这段文字的首字大写,文字主体呢用黑色,而文字中某些词句可以显示为红色...",你可能会沉思一会,考虑是否用WebView+html或者直接制定一套规则然后用CoreGraphics去画,采用WebView+html的方式不失为一个办法,但是如果需求是TableView中每个Cell都要这么显示,并且内容不定,cell高度可变,哈哈,光管理每个Cell中WebView的加载和高度计算就够头疼了,还要考虑内存问题,你懂的,WebView可是个吃内存的大户;那么CoreGraphics+规则呢?拜托,你是要山寨一个Word吗?其实呢,苹果已经为我们提供了一种富文本的解决方案:CoreText!

二、一些概念

本着严谨的治学态度,先来了解一下一些基本概念,这些概念有助于让我们知道屏幕上出现的文字是如何显示,如果觉得只想知道CoreText是怎么用的,那么可以直接跳过这一章节。

我们平时接触最多的排版系统应该就是微软的Word了吧,作为一名程序员,我习惯在使用一些软件的时候思考他们是怎么做的,对于Word这种软件,如果我来开发,简单的方案就是字形+字号+前景色+背景色进行图像绘制(暂时不考虑下划线、删除线和阴影等附加效果),比如一行文字,首先根据设置的字体获得各个字的字形描述,字形描述可以是一个矩阵,矩阵中每个点有两个值,代表显示前景色或背景色,然后根据设置的字号按一定的算法缩放矩阵,最后根据前景色和背景色绘制出这个字的图像,当这一行所有的字的图像生成好后,那么这一行的高度应该是这一行中高度最大的字图像的高度,然后加上行间距。当然现实情况是,Word比这个复杂的多的多,可以先看一下下面一些概念性的东西,会更好的理解排版系统是怎么工作的(我觉得不管是Word还是CoreText,原理大同小异)。

2.1 文字排版的概念

  • 字体(Font):和我们平时说的字体不同,计算机意义上的字体表示的是同一大小,同一样式(Style)字形的集合。从这个意义上来说,当我们为文字设置粗体,斜体时其实是使用了另外一种字体(下划线不算)。而平时我们所说的字体只是具有相同设计属性的字体集合,即Font Family或typeface。

  • 字符(Character)和字形(Glyphs):排版过程中一个重要的步骤就是从字符到字形的转换,字符表示信息本身,而字形是它的图形表现形式。字符一般就是指某种编码,如Unicode编码,而字形则是这些编码对应的图片。但是他们之间不是一一对应关系,同个字符的不同字体族,不同字体大小,不同字体样式都对应了不同的字形。而由于连写(Ligatures)的存在,多个字符也会存在对应一个字形的情况。

CoreText那些事儿_第1张图片
连字

  • 字形描述集(Glyphs Metris):即字形的各个参数。如下图所示:

连字

CoreText那些事儿_第2张图片
连字

  • 边框(Bounding Box):一个假想的边框,尽可能地容纳整个字形。

  • 基线(Baseline):一条假想的参照线,以此为基础进行字形的渲染。一般来说是一条横线。

  • 基础原点(Origin):基线上最左侧的点。

  • 行间距(Leading):行与行之间的间距。

  • 字间距(Kerning):字与字之间的距离,为了排版的美观,并不是所有的字形之间的距离都是一致的,但是这个基本步影响到我们的文字排版。

  • 上行高度(Ascent)和下行高度(Decent):一个字形最高点和最低点到基线的距离,前者为正数,而后者为负数。当同一行内有不同字体的文字时,就取最大值作为相应的值。这样,行高LineHeight = Ascent + |Descent| + Leading

2.2 CoreText

CoreText提供了一些低级的API用于富文本排版,它的数据源是NSAttributedString。它可以根据NSAttributedString的定义的每个range的subNSAttributedString的样式进行对字符串的渲染。可以这样说,这是一个富文本渲染器。

iOS/OSX中用于描述富文本的类是NSAttributedString,顾名思义,它比NSString多了Attribute的概念。它可以包含很多属性,粗体,斜体,下划线,颜色,背景色等等,每个属性都有其对应的字符区域。在OSX上我们只需解析完毕相应的数据,准备好NSAttributedString即可,底层的绘制完全可以交给相应的控件完成。但是在iOS6.0之前就没有这么方便,想要绘制Attributed String就需要用到CoreText了,iOS6.0以及以后的版本,UILabel和UITextView提供了一个attributedText属性,供我们设置AttributeString,然后UILabel和UITextView会调用CoreText进行排版。

2.2.1 CoreText工作流

CoreText那些事儿_第3张图片
连字

使用CoreText进行NSAttributedString的绘制,最重要的两个概念就是CTFrameSetter和CTFrame。

其中CTFramesetter是由CFAttributedString(NSAttributedString)初始化而来,可以认为它是CTFrame的一个Factory,通过传入CGPath生成相应的CTFrame并使用它进行渲染:直接以CTFrame为参数使用CTFrameDraw绘制或者从CTFrame中获取CTLine进行微调后使用CTLineDraw进行绘制。

一个CTFrame是由一行一行的CLine组成,每个CTLine又会包含若干个CTRun(既字形绘制的最小单元),通过相应的方法可以获取到不同位置的CTRun和CTLine,以实现对不同位置touch事件的响应。CTFrame可以认为是一个整体的画布(Canvas),

2.2.2 Attribute String

使用Attribute String主要有两种途径,一个是使用Foundation框架中的NSAttributeString或NSMutableAttributeString类,另一个则是直接使用较为底层的API创建CFAttributedStringRef变量。我们可以猜测NSAttributeString和NSMutableAttributeString是对底层CFAttributedStringRef的封装。

使用NSAttributeString或NSMutableAttributeString相对简单,如下:

NSString *string = @"这是一张小图";
NSDictionary *attr = @{
                           NSFontAttributeName:[UIFont systemFontOfSize:14],
                           NSForegroundColorAttributeName:[UIColor redColor]
                      };
NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:string attributes:attr];

我们可以看到初始化方法中有一个attributes参数,类型为NSDictionary,也就是说在设置一段文字的排版时,Attribute主要是通过该参数以key-value的形式进行设置。Foundation框架在iOS6.0之后提供NSString类型的key供我们使用,而且6.0之前,则需要使用CFStringRef类型的key,我们需要对其进行bridge转换转为NSString进行使用。本文写作时最高iOS版本为9.3,而且当前绝大多数App已经不支持iOS6以下甚至iOS7以下的设备,所以我们着重看一下Foundation提供的NSString类型的Attribute Key。

NSString类型的Attribute Key有如下可选:

UIKIT_EXTERN NSString * const NSFontAttributeName NS_AVAILABLE(10_0, 6_0);                // 字体,UIFont类型, 默认为 Helvetica(Neue) 12

UIKIT_EXTERN NSString * const NSParagraphStyleAttributeName NS_AVAILABLE(10_0, 6_0);      // 段落样式,值为NSParagraphStyle类型, 默认为defaultParagraphStyle

UIKIT_EXTERN NSString * const NSForegroundColorAttributeName NS_AVAILABLE(10_0, 6_0);     // 前景色(文字颜色), UIColor类型, 默认为黑色

UIKIT_EXTERN NSString * const NSBackgroundColorAttributeName NS_AVAILABLE(10_0, 6_0);     // 背景色, UIColor类型,默认为nil

UIKIT_EXTERN NSString * const NSLigatureAttributeName NS_AVAILABLE(10_0, 6_0);    // 连体属性,取值为NSNumber 对象(整数),0 表示没有连体字符,1 表示使用默认的连体字符

UIKIT_EXTERN NSString * const NSKernAttributeName NS_AVAILABLE(10_0, 6_0);                // 字间距,float的NSNumber装箱

UIKIT_EXTERN NSString * const NSStrikethroughStyleAttributeName NS_AVAILABLE(10_0, 6_0);  // 设置删除线,取值为 NSNumber 对象(整数),枚举常量

UIKIT_EXTERN NSString * const NSUnderlineStyleAttributeName NS_AVAILABLE(10_0, 6_0);      // 设置下划线,取值为 NSNumber 对象(整数),0为没有下划线

UIKIT_EXTERN NSString * const NSStrokeColorAttributeName NS_AVAILABLE(10_0, 6_0);         // 填充部分颜色,不是字体颜色,取值为 UIColor 对象

UIKIT_EXTERN NSString * const NSStrokeWidthAttributeName NS_AVAILABLE(10_0, 6_0);         // 设置笔画宽度,取值为 NSNumber 对象(整数),负值填充效果,正值中空效果

UIKIT_EXTERN NSString * const NSShadowAttributeName NS_AVAILABLE(10_0, 6_0);              // 设置阴影属性,取值为 NSShadow 对象

UIKIT_EXTERN NSString *const NSTextEffectAttributeName NS_AVAILABLE(10_10, 7_0);          // 设置文本特殊效果,取值为 NSString 对象,(图版印刷效果)

UIKIT_EXTERN NSString * const NSAttachmentAttributeName NS_AVAILABLE(10_0, 7_0);          // 文本附件,取值为NSTextAttachment对象,常用于文字图片混排

UIKIT_EXTERN NSString * const NSLinkAttributeName NS_AVAILABLE(10_0, 7_0);                // 设置链接属性,点击后调用浏览器打开指定URL地址

UIKIT_EXTERN NSString * const NSBaselineOffsetAttributeName NS_AVAILABLE(10_0, 7_0);      // 设置基线偏移值,取值为 NSNumber (float),正值上偏,负值下偏

UIKIT_EXTERN NSString * const NSUnderlineColorAttributeName NS_AVAILABLE(10_0, 7_0);      // 下划线颜色

UIKIT_EXTERN NSString * const NSStrikethroughColorAttributeName NS_AVAILABLE(10_0, 7_0);  // 删除线颜色

UIKIT_EXTERN NSString * const NSObliquenessAttributeName NS_AVAILABLE(10_0, 7_0);         // 字形倾斜度,取值为 NSNumber (float),正值右倾,负值左倾

UIKIT_EXTERN NSString * const NSExpansionAttributeName NS_AVAILABLE(10_0, 7_0);           // 文本横向拉伸属性,取值为 NSNumber (float),正值横向拉伸文本,负值横向压缩文本

UIKIT_EXTERN NSString * const NSWritingDirectionAttributeName NS_AVAILABLE(10_6, 7_0);    // 文字书写方向

UIKIT_EXTERN NSString * const NSVerticalGlyphFormAttributeName NS_AVAILABLE(10_7, 6_0);   // 文字排版方向,取值为 NSNumber 对象(整数),0 表示横排文本,1 表示竖排文本(目前iOS总是横排文本)

三、CoreText的使用

对于CoreText使用,对于我们程序员来说,应该更喜欢简单粗暴地直接上代码,如果把代码分开按片段来讲解,反而会使思维有断层。

创建项目以及新建导航结构不是本文重点,先略过了,针对CoreText的使用,我简单的从三个方面来演示:

  1. 简单文本的文字排版,使用CoreText和CoteGraphics API
  2. 图文混编加支持事件响应,使用CoreText和CoteGraphics API
  3. 图文混编加支持事件响应,使用Foundation和UIKit框架提供的高级API

下面开始对上述内容逐一进行演示,注意:前方代码简陋,没有重用,更无设计感,只是尽可能展示用法,当然用法也不可能在一篇文章中穷尽,请举一反三!

3.1 简单文本的文字排版

新建一个类CTBaseView,内容如下(有备注但不多,请自行脑补):

@implementation CTBaseView
- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        self.backgroundColor = SHRGB(0xee, 0xee, 0xee);
    }
    return self;
}

- (void)drawRect:(CGRect)rect
{
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    CGContextSaveGState(ctx);
    
    // 1.垂直翻转画布
    CGContextSetTextMatrix(ctx, CGAffineTransformIdentity);
    CGContextTranslateCTM(ctx, 0, self.height);
    CGContextScaleCTM(ctx, 1.0, -1.0);
    
    // 2.创建path
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, self.bounds);
    
    
    // 3.创建Attribute String
    NSMutableAttributedString *muAttrString = [[NSMutableAttributedString alloc]init];
    
    // 3.1
    NSString *string = @"H";
    NSDictionary *attr = @{
                           NSFontAttributeName:[UIFont systemFontOfSize:20],
                           NSForegroundColorAttributeName:[UIColor redColor]
                           };
    NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:string attributes:attr];
    [muAttrString appendAttributedString:attrString];
    
    // 3.2
    string = @"ello 复仇者联盟!\n";
    attr = @{
             NSFontAttributeName:[UIFont systemFontOfSize:14],
             NSForegroundColorAttributeName:[UIColor blueColor]
             };
    attrString = [[NSAttributedString alloc] initWithString:string attributes:attr];
    [muAttrString appendAttributedString:attrString];
    
    // 3.3
    string = @"我们的主页是:";
    attrString = [[NSAttributedString alloc] initWithString:string];
    [muAttrString appendAttributedString:attrString];
    
    // 3.4
    string = @"http://www.dianping.com\n";
    attr = @{NSBackgroundColorAttributeName:[UIColor yellowColor],
             NSForegroundColorAttributeName:SHRGB(0x72, 0xAC, 0xE3),
             NSUnderlineStyleAttributeName:@(NSUnderlineStyleSingle),
             NSLinkAttributeName:@"http://www.baidu.com"};
    attrString = [[NSAttributedString alloc] initWithString:string attributes:attr];
    [muAttrString appendAttributedString:attrString];
    
    // 3.5
    string = @"《复仇者联盟》(Marvel's The Avengers)是漫威影业出品的一部科幻动作电影,取材自漫威漫画,是漫威电影宇宙的第六部电影,同时也是第一阶段的收官作品。由乔斯·韦登执导,小罗伯特·唐尼、克里斯·埃文斯、克里斯·海姆斯沃斯、马克·鲁法洛、斯嘉丽·约翰逊、杰瑞米·雷纳和汤姆·希德勒斯顿联袂出演。\n影片讲述了神盾局指挥官尼克·弗瑞为了对付《雷神》中被流放的洛基,积极奔走寻找最强者,在神盾局斡旋下将钢铁侠、美国队长、雷神托尔、绿巨人、黑寡妇和鹰眼侠六位超级英雄集结在一起,组成了复仇者联盟,共同携手应对邪神洛基。\n影片于2012年5月5日在中国内地正式上映。";
    NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc]init];
    style.lineSpacing = 5;
    style.paragraphSpacing = 20;
    attr = @{
             NSFontAttributeName:[UIFont systemFontOfSize:14],
             NSForegroundColorAttributeName:[UIColor orangeColor],
             NSParagraphStyleAttributeName:style
             };
    attrString = [[NSAttributedString alloc] initWithString:string attributes:attr];
    [muAttrString appendAttributedString:attrString];
    
    // 4.创建Framesetter和Frame
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)muAttrString);
    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, muAttrString.length), path, NULL);
    
    // 5.打印Frame中line和run信息
    NSArray *lines = (NSArray*)CTFrameGetLines(frame);
    NSInteger lineCount = lines.count;
    CGPoint lineOrigin[lineCount];
    CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigin);
    for (NSInteger i=0; i line:%@ - run:%@ [%@ , %@]", @(i),@(j),@(range.location),@(range.length));
        }
    }
    
    CTFrameDraw(frame, ctx);
    CFRelease(frame);
    CFRelease(framesetter);
    CFRelease(path);
    CGContextRestoreGState(ctx);
}
@end

展示效果如下:

CoreText那些事儿_第4张图片
连字

3.2 图文混编加支持事件响应

新建一个类CTMixView,内容如下(有备注但不多,请自行脑补):

static CGFloat ascentCallback(void *ref)
{
    return [[((__bridge NSDictionary*)ref) objectForKey:@"height"] floatValue];
}

static CGFloat descentCallback(void *ref)
{
    return 0;
}

static CGFloat widthCallback(void *ref)
{
    return [[((__bridge NSDictionary*)ref) objectForKey:@"width"] floatValue];
}

@interface CTMixView ()

@property (nonatomic, strong) NSMutableArray *imageRects;

@end

@implementation CTMixView
- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        self.backgroundColor = SHRGB(0xee, 0xee, 0xee);
        self.imageRects = [NSMutableArray new];
        UIGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTap:)];
        [self addGestureRecognizer:gesture];
        self.userInteractionEnabled = YES;
    }
    return self;
}

- (void)drawRect:(CGRect)rect
{
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    CGContextSaveGState(ctx);
    
    // 垂直翻转画布
    CGContextSetTextMatrix(ctx, CGAffineTransformIdentity);
    CGContextTranslateCTM(ctx, 0, self.height);
    CGContextScaleCTM(ctx, 1.0, -1.0);
    
    // 2.创建path
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, self.bounds);
    
    // 3.创建Attribute String
    NSMutableAttributedString *muAttrString = [[NSMutableAttributedString alloc]init];
    
    // 3.1
    NSString *string = @"这是一张小图";
    NSDictionary *attr = @{
                           NSFontAttributeName:[UIFont systemFontOfSize:14],
                           NSForegroundColorAttributeName:[UIColor redColor]
                           };
    NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:string attributes:attr];
    [muAttrString appendAttributedString:attrString];
    
    // 3.2 添加一张小图
    [self addImageWithWidth:50 height:50 toAttrString:muAttrString];
    
    // 3.3
    string = @",这是一张大图";
    attr = @{
             NSFontAttributeName:[UIFont systemFontOfSize:14],
             NSForegroundColorAttributeName:[UIColor redColor]
             };
    attrString = [[NSAttributedString alloc] initWithString:string attributes:attr];
    [muAttrString appendAttributedString:attrString];
    
    // 3.4 添加一张大图
    [self addImageWithWidth:200 height:200 toAttrString:muAttrString];
    
    // 3.5
    string = @"后面没图了";
    attr = @{
             NSFontAttributeName:[UIFont systemFontOfSize:20],
             NSForegroundColorAttributeName:[UIColor blueColor]
             };
    attrString = [[NSAttributedString alloc] initWithString:string attributes:attr];
    [muAttrString appendAttributedString:attrString];
    
    // 4.创建Framesetter和Frame
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)muAttrString);
    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, muAttrString.length), path, NULL);
    
    
    // 5.获取添加的图片的Rect
    NSArray *lines = (NSArray*)CTFrameGetLines(frame);
    NSInteger lineCount = lines.count;
    CGPoint lineOrigin[lineCount];
    CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigin);
    for (NSInteger i=0; i

展示效果如下:

CoreText那些事儿_第5张图片
连字

3.3 图文混编加支持事件响应(高级接口)

新建一个类CTMixView,内容如下(有备注但不多,请自行脑补):

@interface CTHightLevelMixView ()

@property (nonatomic, strong) NSMutableArray *positons;

@end

@implementation CTHightLevelMixView
- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        self.positons = [NSMutableArray new];
        self.backgroundColor = SHRGB(0xee, 0xee, 0xee);
        
        [self displayContent];
        UIGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTap:)];
        [self addGestureRecognizer:gesture];
        self.userInteractionEnabled = YES;
    }
    return self;
}

- (void)displayContent
{
    NSMutableAttributedString *muAttrString = [[NSMutableAttributedString alloc]init];
    
    NSString *string = @"这是一张小图";
    NSDictionary *attr = @{
                           NSFontAttributeName:[UIFont systemFontOfSize:14],
                           NSForegroundColorAttributeName:[UIColor redColor]
                           };
    NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:string attributes:attr];
    [muAttrString appendAttributedString:attrString];
    
    // 3.2 添加一张小图
    [self addImage:@"50x50" width:50 height:50 toAttrString:muAttrString];
    
    // 3.3
    string = @",这是一张大图";
    attr = @{
             NSFontAttributeName:[UIFont systemFontOfSize:14],
             NSForegroundColorAttributeName:[UIColor redColor]
             };
    attrString = [[NSAttributedString alloc] initWithString:string attributes:attr];
    [muAttrString appendAttributedString:attrString];
    
    // 3.4 添加一张大图
    [self addImage:@"200x200" width:200 height:200 toAttrString:muAttrString];
    
    // 3.5
    string = @"后面没图了";
    attr = @{
             NSFontAttributeName:[UIFont systemFontOfSize:20],
             NSForegroundColorAttributeName:[UIColor blueColor]
             };
    attrString = [[NSAttributedString alloc] initWithString:string attributes:attr];
    [muAttrString appendAttributedString:attrString];
    
    self.attributedText = muAttrString;
}

- (void)addImage:(NSString*)imageName width:(CGFloat)width height:(CGFloat)height toAttrString:(NSMutableAttributedString*)attrString
{
    NSTextAttachment *imageAttachment = [[NSTextAttachment alloc]init];
    imageAttachment.image = [UIImage imageNamed:imageName];
    imageAttachment.bounds = CGRectMake(0, 0, width, height);
    
    NSAttributedString *string = [NSAttributedString attributedStringWithAttachment:imageAttachment];
    NSRange range = NSMakeRange(attrString.length, string.length);
    NSValue *rangeValue = [NSValue valueWithRange:range];
    [self.positons addObject:rangeValue];
    [attrString appendAttributedString:string];
}

- (void)onTap:(UIGestureRecognizer*)gesture
{
    CGPoint point = [gesture locationInView:self];
    [self.positons enumerateObjectsUsingBlock:^(NSValue *value, NSUInteger idx, BOOL * _Nonnull stop) {
        NSRange range = [value rangeValue];
        self.selectedRange = range;
        NSArray *rects = [self selectionRectsForRange:self.selectedTextRange];
        self.selectedRange = NSMakeRange(0, 0);
        for (UITextSelectionRect *textRect in rects) {
            CGRect rect = textRect.rect;
            if (CGRectContainsPoint(rect, point)) {
                NSString *text = idx == 0 ? @"点击了小图" : @"点击了大图";
                UIAlertView *av = [[UIAlertView alloc]initWithTitle:nil message:text delegate:nil cancelButtonTitle:@"确定" otherButtonTitles:nil];
                [av show];
                *stop = YES;
            }
        }
    }];
}
@end

展示效果如下:

CoreText那些事儿_第6张图片
连字

本文Demo下载:Demo源码

你可能感兴趣的:(CoreText那些事儿)