CoreText 学习笔记(上)

作者: ChinaChong | 来源:发表于2018-12-03 10:17 被阅读36次

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

    CoreText是相对来说非常底层的框架,在日常的iOS开发过程中遇到诸如大量文本排版、图文混合排版或者文本链接点击等情况,选择用CoreText去做框架底层还是相当优选的。

    这些内容在唐巧的博客中都详细的给出了,有兴趣的朋友可以去唐巧的博客里好好学习一下。我这里要写的是,在学习唐巧关于CoreText的文章时遇到的几个问题,结合原作者的文章,做个自我学习总结。

    唐巧关于CoreText的介绍是循序渐进的,先介绍的是纯文本的排版,我也从这开始,从不一样的角度去看 CoreText 纯文本排版。

    CoreText 纯文本排版

    坐标系

    在使用CoreText时需要注意坐标系的不同,在CoreText下坐标系的原点为视图的左下角,x轴向右为正方向,y轴向上为正方向。而我们平时的UIKit坐标系原点则是视图的左上角,x轴向右为正方向,y轴向下为正方向。如图所示:


    所以在确定绘制位置时,要注意坐标系的转换,比如下面这个黑色圆点的位置,在两个坐标系中是不一样的

    CoreText使用的整体流程

    首先,使用CoreText绘制纯文本是在UIView中,整个调用流程的入口是UIView的 drawRect 方法,每次创建一个新的UIView系统都会给你预先写好的那部分代码

    /*
    // Only override drawRect: if you perform custom drawing.
    // An empty implementation adversely affects performance during animation.
    - (void)drawRect:(CGRect)rect {
        // Drawing code
    }
    */
    

    接下来就是在 drawRect 方法中实现绘制的代码了,总体流程结构如图:

    图里面总结了基于 CoreText 的排版引擎原文中的架构,下面描述一下具体思路:

    • CoreText排版的入口是 drawRect 方法,所有绘制的代码都要从这里开始

    • 首先第一步要在 drawRect 方法中获取绘制上下文

      CGContextRef context = UIGraphicsGetCurrentContext();
      
    • 第二步要反转坐标系

      CGContextSetTextMatrix(context, CGAffineTransformIdentity);
      CGContextTranslateCTM(context, 0, self.bounds.size.height);
      CGContextScaleCTM(context, 1.0, -1.0);
      
      • CGContextSetTextMatrix(context, CGAffineTransformIdentity);是初始化文本矩阵 Text Matrix,在绘制之前一定记得初始化文本矩阵 Text Matrix,否则,结果将是不可预测的,就像使用非初始化内存一样

      • CGContextTranslateCTM(context, 0, self.bounds.size.height);向上平移一个View高度

      • CGContextScaleCTM(context, 1.0, -1.0);将CoreText坐标系的 y轴 反转

    • 第三步要在绘制之前要计算出绘制区域的总高度,计算高度可以在下一步创建CTFrame时根据其参数 CTFramesetter 获得

    • 最后第四步要调用 CTFrameDraw() 函数进行绘制,完整的函数描述为 CTFrameDraw(CTFrameRef _Nonnull frame, CGContextRef _Nonnull context),共需要两个参数:CTFrameCGContextCGContext 是前面第一步获取过的参数,下一步重点要说的就是最重要的参数 CTFrame

    创建CTFrame

    创建 CTFrame 需要两个参数:CTFramesetterCGMutablePath

    创建 CTFramesetter 需要富文本字符串(NSAttributedString),这个富文本字符串可以根据我们的需求自行创建所需的文本(NSString)和样式(attributes字典)。

    NSDictionary *attributes = @{属性字典};
    
    NSAttributedString *content = [[NSAttributedString alloc] initWithString:@"要显示的文本" attributes:attributes];
    
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)content);
    

    这样 CTFramesetter 就创建好了,接下来要用 CTFramesetter 计算出整个绘制区域的高度:

    // 获得要绘制的区域的高度
    CGSize restrictSize = CGSizeMake(自定义的宽度, CGFLOAT_MAX);
    CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0,0), nil, restrictSize, nil);
    CGFloat textHeight = coreTextSize.height;
    

    绘制区域的总高度就是 textHeight

    接下来创建 CGMutablePath,创建 CGMutablePath需要两个参数:自定的宽度和计算好的高度

    CGMutablePathRef path = CGPathCreateMutable();
    
    CGPathAddRect(path, NULL, CGRectMake(0, 0, 自定宽度, textHeight));
    

    CGMutablePath也有了,现在可以回头创建 CTFrame

    CTFrameRef ctFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
    

    有了 CTFrame 后即可以进行 CoreText使用的整体流程 中的第四步:调用 CTFrameDraw() 函数进行绘制。至此绘制纯文本的架构思路全部介绍完。

    CTFrameDraw(ctFrame, context);
    

    问题:反转坐标系为什么要向上平移一个View高度?

    我画了几张示意图,来说说为什么要平移。

    • 黄色坐标表示CoreText坐标系
    • 红色坐标表示UIKit坐标系
    • 灰色区域是手机屏幕
    • 蓝色区域是自定义的View,就是我们用来绘制的View
    • 文本 Hello World! 所在的白色区域正是绘制区域
    首先不反转坐标系的时候,绘制出来的图像是倒转的。

    然后调用 CGContextSetTextMatrix(context, CGAffineTransformIdentity);初始化文本矩阵,并且调用CGContextScaleCTM(context, 1.0, -1.0);将CoreText坐标系的 y轴 反转,会得到下面的图像

    可以看到,其实在反转CoreText坐标系的 y轴 后,图像刚刚好被弄到View外面了,也就是说黑色虚线位置就是View,蓝色区域的实际图像我们是看不到的,所以我们一定要把蓝色区域向上平移一整个View的高度,才会回到原位,如下图



    牛刀小试

    接下来根据上面的思路写一个小 demo,算是练练手。写这个demo暂不考虑代码结构的优化,优化的代码结构在基于 CoreText 的排版引擎可以找到。完全是为了快速记忆刚刚提到的那些逻辑,用最简单的方式全部回顾一遍。

    创建一个继承自UIView的类,用于绘制,取名 GCDisplayView,源代码如下:

    头文件

    //
    //  GCDisplayView.h
    //
    //  Created by 崇 on 2018.
    //  Copyright © 2018 崇. All rights reserved.
    //
    
    #import <UIKit/UIKit.h>
    
    @interface GCDisplayView : UIView
    
    @property (nonatomic, assign) CGFloat textHeight;
    
    @end
    
    

    实现文件

    //
    //  GCDisplayView.m
    //
    //  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;
    
    @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);
        // 释放
        CFRelease(self.ctFrame);
        CFRelease(self.framesetter);
    }
    
    - (void)createCTFrame {
        
        /*
         创建 CTFrame 需要两个参数:CTFramesetter 和 CGMutablePath。
         
         先创建 CTFramesetter,利用 CTFramesetter 计算出绘制区域高度后再创建 CGMutablePath。
         
         创建 CTFramesetter 需要先创建 NSAttributedString。
         */
        
        NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] init];
        
        NSMutableAttributedString *aStr1 = [[NSMutableAttributedString alloc] initWithString:@"创建 CTFrame 需要两个参数:CTFramesetter 和 CGMutablePath。\n" attributes:@{(id)kCTForegroundColorAttributeName : [UIColor redColor]}];
        
        NSMutableAttributedString *aStr2 = [[NSMutableAttributedString alloc] initWithString:@"先创建 CTFramesetter,利用 CTFramesetter 计算出绘制区域高度后再创建 CGMutablePath。\n" attributes:@{(id)kCTForegroundColorAttributeName : [UIColor blueColor]}];
        
        NSMutableAttributedString *aStr3 = [[NSMutableAttributedString alloc] initWithString:@"创建 CTFramesetter 需要先创建NSAttributedString." attributes:@{(id)kCTForegroundColorAttributeName : [UIColor blackColor]}];
        
        [attributedString appendAttributedString:aStr1];
        [attributedString appendAttributedString:aStr2];
        [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);
        
        CFRelease(path);
    }
    
    @end
    
    

    在ViewController的StoryBoard中拖入一个UIView,让它继承自 GCDisplayView

    把StoryBoard中的这个View拖入到ViewController中作为属性,设置它的高度

    //
    //  ViewController.m
    //  CoreTextPureText
    //
    //  Created by 崇 on 2018/11/8.
    //  Copyright © 2018 崇. All rights reserved.
    //
    
    #import "ViewController.h"
    #import "GCDisplayView.h"
    
    @interface ViewController ()
    @property (weak, nonatomic) IBOutlet GCDisplayView *disView;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // 设置高度
        CGRect frame = CGRectMake(self.disView.frame.origin.x, self.disView.frame.origin.y, self.disView.frame.size.width, self.disView.textHeight);
        self.disView.frame = frame;
    }
    
    
    - (void)didReceiveMemoryWarning {
        [super didReceiveMemoryWarning];
        // Dispose of any resources that can be recreated.
    }
    
    
    @end
    
    

    运行结果如下图





    总结

    如果没有看过唐巧的原文而是先看我这篇文章,那你肯定会迷糊,因为我去掉了很多的细节,写的都是自己学习过后的心得,这些细节我要是搬过来就有点不摇碧莲了,如果想要了解还是请移步 ==> 基于 CoreText 的排版引擎

    唐巧的原文中将纯文本绘制一直写到支持富文本,而且做了很优雅的架构设计,将数据源就是源字符串和字体相关设置都做成JSON格式的文件,方便批量操作。

    基于 CoreText 的排版引擎中写了几个辅助类,主要就是把我写的 demo 中的 - (void)createCTFrame 方法提出去分别实现。其实CoreText绘制只需要有一个CTFrame就足够了,这个CTFrame可以在本类中实现和保存,也可以像唐巧一样提炼出去,做更好的架构。CTFrame谁都不依赖(比如:drawRect 方法或者 context绘制上下文),而我们需要设置的所有文本的属性又都会包含在CTFrame中,所以CTFrame完全可以拿出去,会显得更加灵活。

    另外就是要说一下 drawRect 方法,当时在看 基于 CoreText 的排版引擎的时候就有疑问,那就是代码的执行顺序。由于没怎么用过 drawRect 所以去查了一下。它的调用时机很晚,对于本类而言 drawRect 的调用在初始化完成以后,对于使用这个View的controller而言 drawRect 在viewDidLoad之后,快要显示的时候才会调用。所以你大可以放心把 ctFrame 拿出去做各种设置, drawRect 方法不太可能会比你的方法先执行。如果有对 drawRect 执行顺序感兴趣的朋友,可以到网上搜一搜,一大把有关的文章。

    后面还会继续介绍CoreText图文混排。

    Coming Soon ~~~

    相关文章

      网友评论

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

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