zoukankan      html  css  js  c++  java
  • iOS:基于CoreText的排版引擎

    一、CoreText的简介

    CoreText是用于处理文字和字体的底层技术。它直接和Core Graphics(又被称为Quartz)打交道。Quartz是一个2D图形渲染引擎,能够处理OSX和iOS中图形显示问题。Quartz能够直接处理字体(font)和字形(glyphs),将文字渲染到界面上,它是基础库中唯一能够处理字形的模块。因此CoreText为了排版,需要将显示的文字内容、位置、字体、字形直接传递给Quartz。与其他UI组件相比,由于CoreText直接和Quartz来交互,所以它具有更高效的排版功能。

    下面是CoreText的架构图,可以看到,CoreText处在非常底层的位置,上层的UI控件(包含UILable、UITextField及UITextView)和UIWebView都是基于CoreText来实现的。

    UIWebview也是处理复杂的文字排版的备选方案。对于排版,基于CoreText和基于UIWebView相比,具有以下不同点:

    • CoreText占用内存更少,渲染速度更快,UIWebView占用内存多,渲染速度慢。
    • CoreText在渲染界面前就可以精确地获得显示内容的高度(只要有了CTFrame即可),而UIWebView只有渲染出内容后,才能获得内容的高度(而且还需要通过JavaScript代码来获取)。
    • CoreText的CTFrame可以在后台线程渲染,UIWebView的内容只能在主线程(UI线程)渲染。
    • 基于CoreText可以做更好的原生交互效果,交互效果可以更细腻。而UIWebView的交互效果都是利用JavaScript来实现的,在交互效果上会有一些卡顿情况存在。例如,在UIWebView下,一个简单的按钮按下操作,都无法做出原生按钮的即时和细腻的按下效果。

    当然,基于CoreText的排版方案也有那么一些劣势:

    • CoreText渲染出来的内容不能像UIWebView那样方便的支付内容的复制。
    • 基于CoreText来排版需要自己处理很多复杂逻辑,例如需要自己处理图片和文字混排相关的逻辑,也需要自己实现链接点击操作的支持。
    1、图文混排
    CTFrameRef  textFrame     // coreText 的 frame
    CTLineRef   line          // coreText 的 line
    CTRunRef    run           // line  中的部分文字
    
    2、相关方法:
    CFArrayRef CTFrameGetLines(CTFrameRef frame) //获取包含CTLineRef的数组
    void CTFrameGetLineOrigins(CTFrameRef frame,CFRange range,CGPoint origins[])//获取所有CTLineRef的原点
    CFRange CTLineGetStringRange(CTLineRef line) //获取line中文字在整段文字中的Range
    CFArrayRef CTLineGetGlyphRuns(CTLineRef line)//获取line中包含所有run的数组
    CFRange CTRunGetStringRange(CTRunRef run)//获取run在整段文字中的Range
    CFIndex CTLineGetStringIndexForPosition(CTLineRef line,CGPoint position)//获取点击处position文字在整段文字中的index
    CGFloat CTLineGetOffsetForStringIndex(CTLineRef line,CFIndex charIndex,CGFloat* secondaryOffset)//获取整段文字中charIndex位置的字符相对line的原点的x值

    二、基于CoreText的基础排版引擎

    简单实现步骤:

    a.自定义View,重写drawRect方法,后面的操作均在其中进行

    b.得到当前绘图上下问文,用于后续将内容绘制在画布上

    c.将坐标系翻转

    d.创建绘制的区域,写入要绘制的内容

    示例1:不带图片的排版引擎,只是显示文本内容,而且不设置文字的属性信息

    自定义的CTDispalyView.m

    //  CTDispalyView.m
    //  CoreTextDemo
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    
    #import "CTDispalyView.h"
    
    //导入CoreText系统框架
    #import <CoreText/CoreText.h>
    
    @implementation CTDispalyView
    
    //重写drawRect方法
    - (void)drawRect:(CGRect)rect {
        
        [super drawRect:rect];
     
        //1.获取当前绘图上下文
        CGContextRef context = UIGraphicsGetCurrentContext();
        
        //2.旋转坐坐标系(默认和UIKit坐标是相反的)
        CGContextSetTextMatrix(context, CGAffineTransformIdentity);
        CGContextTranslateCTM(context, 0, self.bounds.size.height);
        CGContextScaleCTM(context, 1.0, -1.0);
        
        //3.创建绘制局域
        CGMutablePathRef path = CGPathCreateMutable();
        CGPathAddRect(path, NULL, self.bounds);
        
        //4.设置绘制内容
        NSAttributedString *attString = [[NSAttributedString alloc] initWithString:
                                         @"CoreText是用于处理文字和字体的底层技术。"
                                         "它直接和Core Graphics(又被称为Quartz)打交道。"
                                         "Quartz是一个2D图形渲染引擎,能够处理OSX和iOS中图形显示问题。"
                                         "Quartz能够直接处理字体(font)和字形(glyphs),将文字渲染到界面上,它是基础库中唯一能够处理字形的模块。"
                                         "因此CoreText为了排版,需要将显示的文字内容、位置、字体、字形直接传递给Quartz。"
                                         "与其他UI组件相比,由于CoreText直接和Quartz来交互,所以它具有更高效的排版功能。"];
        CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString);
        CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, [attString length]), path, NULL);
        
        //5.开始绘制
        CTFrameDraw(frame, context);
        
        //6.释放资源
        CFRelease(frame);
        CFRelease(path);
        CFRelease(framesetter);
    }
    @end
    View Code

    在ViewController.m实现显示

    //  ViewController.m
    //  CoreTextDemo
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    
    #import "ViewController.h"
    #import "CTDispalyView.h"
    
    @interface ViewController ()
    @end
    
    @implementation ViewController
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.view.backgroundColor = [UIColor whiteColor];
        
        //显示内容
        CTDispalyView *dispaleView = [[CTDispalyView alloc] initWithFrame:CGRectMake(0, 0, 300, 200)];
        dispaleView.center = self.view.center;
        dispaleView.backgroundColor = [UIColor whiteColor];
        [self.view addSubview:dispaleView];
    }
    @end
    View Code

    演示结果截图

    三、基于CoreText的基本封装

    发现,虽然上面效果确实达到了我们的要求,但是,很有局限性,因为它仅仅是展示了CoreText排版的基本功能而已。要制作一个比较完善的排版引擎,我们不能简单的将所有的代码都放到CTDisplayView的drawRect方法中。根据设计模式的“单一功能原则”,我们应该把功能拆分,把不同的功能都放到各自不同的类里面进行。

    对于一个复杂的排版引擎来说,可以将功能拆分为以下几个类来完成:

    1、一个显示用的类,仅仅负责显示内容,不负责排版

    2、一个模型类,用于承载显示所需要的所有数据

    3、一个排版类,用于实现文字内容的排版

    4、一个配置类,用于实现一些排版时的可配置项

    例如定义的4个类分别为:

    CTFrameParserConfig类:用于配置绘制的参数,例如文字颜色、大小、行间距等

    CTFrameParser类:用于生成最后绘制界面需要的CTFrameRef实例

    CoreTextData类:用于保存由CTFrameParser类生成的CTFrameRef实例,以及CTFrameRef实际绘制需要的高度

    CTDisplayView类:持有CoreTextData类实例,负责将CFFrameRef绘制在界面上。

    关于这4个类的关键代码如下:

    CTFrameParserConfig

    //  CTFrameParserConfig.h
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import <Foundation/Foundation.h>
    
    @interface CTFrameParserConfig : NSObject
    
    //配置属性
    @property (nonatomic ,assign)CGFloat width;
    @property (nonatomic, assign)CGFloat fontSize;
    @property (nonatomic, assign)CGFloat lineSpace;
    @property (nonatomic, strong)UIColor *textColor;
    
    @end
    View Code
    //  CTFrameParserConfig.m
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import "CTFrameParserConfig.h"
    
    @implementation CTFrameParserConfig
    
    //初始化
    -(instancetype)init{
        self = [super init];
        if (self) {
            _width = 200.f;
            _fontSize = 16.0f;
            _lineSpace = 8.0f;
            _textColor = RGB(108, 108, 108);
        }
        return self;
    }
    
    @end
    View Code

    CTFrameParser

    //  CTFrameParser.h
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import <Foundation/Foundation.h>
    #import "CoreTextData.h"
    
    @class CTFrameParserConfig;
    @interface CTFrameParser : NSObject
    
    /**
     *  给内容设置配置信息
     *
     *  @param content 内容
     *  @param config  配置信息
     *
     */
    +(CoreTextData *)parseContent:(NSString *)content config:(CTFrameParserConfig *)config;
    
    @end
    View Code
    //  CTFrameParser.m
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import "CTFrameParser.h"
    #import "CTFrameParserConfig.h"
    #import "CoreTextData.h"
    
    @implementation CTFrameParser
    
    //给内容设置配置信息
    +(CoreTextData *)parseContent:(NSString *)content config:(CTFrameParserConfig *)config{
        
        NSDictionary *attributes = [self attributesWithConfig:config];
        NSAttributedString *contextString = [[NSAttributedString alloc] initWithString:content attributes:attributes];
        
        //创建CTFrameStterRef实例
        CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)contextString);
        
        //获得要绘制的区域的高度
        CGSize restrictSize = CGSizeMake(config.width, CGFLOAT_MAX);
        CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, 0), nil, restrictSize, nil);
        CGFloat textHeight = coreTextSize.height;
        
        //生成CTFrameRef实例
        CTFrameRef frame = [self createFrameWithFramesetter:framesetter config:config height:textHeight];
        
        //将生成好的CTFrameRef实例和计算好的绘制高度保存到CoreTextData实例中,最后返回CoreTextData实例
        CoreTextData *data = [[CoreTextData alloc] init];
        data.ctFrame = frame;
        data.height = textHeight;
        
        //释放内存
        CFRelease(framesetter);
        CFRelease(frame);
        
        return data;
    }
    
    
    //配置信息格式化
    +(NSDictionary *)attributesWithConfig:(CTFrameParserConfig *)config{
        
        CGFloat fontSize = config.fontSize;
        CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
        CGFloat lineSpcing = config.lineSpace;
        const CFIndex kNumberOfSettings = 3;
        CTParagraphStyleSetting theSettings[kNumberOfSettings] = {
            {kCTParagraphStyleSpecifierLineSpacingAdjustment,sizeof(CGFloat),&lineSpcing},
            {kCTParagraphStyleSpecifierMaximumLineSpacing,sizeof(CGFloat),&lineSpcing},
            {kCTParagraphStyleSpecifierMinimumLineSpacing,sizeof(CGFloat),&lineSpcing},
        };
        
        CTParagraphStyleRef theParagraphRef = CTParagraphStyleCreate(theSettings, kNumberOfSettings);
        UIColor *textColor = config.textColor;
       
        NSMutableDictionary *dict = [NSMutableDictionary dictionary];
        dict[(id)kCTForegroundColorAttributeName] = (id)textColor.CGColor;
        dict[(id)kCTFontAttributeName] = (__bridge id)fontRef;
        dict[(id)kCTParagraphStyleAttributeName] = (__bridge id)theParagraphRef;
        
        CFRelease(fontRef);
        CFRelease(theParagraphRef);
        return dict;
    }
    
    //创建CTFrameRef绘制路径实例
    +(CTFrameRef)createFrameWithFramesetter:(CTFramesetterRef)framesetter config:(CTFrameParserConfig *)config height:(CGFloat)height{
        
        CGMutablePathRef path = CGPathCreateMutable();
        CGPathAddRect(path, NULL, CGRectMake(0, 0, config.width, height));
        
        CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
        CFRelease(path);
        return frame;
    }
    
    @end
    View Code

    CoreTextData

    //  CoreTextData.h
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import <Foundation/Foundation.h>
    
    @interface CoreTextData : NSObject
    
    @property (assign,nonatomic)CTFrameRef ctFrame;
    @property (assign,nonatomic)CGFloat height;
    
    @end
    View Code
    //  CoreTextData.m
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import "CoreTextData.h"
    
    @implementation CoreTextData
    
    //CoreFoundation不支持ARC,需要手动去管理内存的释放
    -(void)setCtFrame:(CTFrameRef)ctFrame{
        if (_ctFrame != ctFrame) {
            if (_ctFrame !=nil) {
                CFRelease(_ctFrame);
            }
        }
        CFRetain(ctFrame);
        _ctFrame = ctFrame;
    }
    
    -(void)dealloc{
        if (_ctFrame != nil) {
            CFRelease(_ctFrame);
            _ctFrame = nil;
        }
    }
    
    @end
    View Code

    CTDisplayView

    //  CTDispalyView.h
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import <UIKit/UIKit.h>
    #import "CoreTextData.h"
    
    @interface CTDispalyView : UIView
    @property(strong,nonatomic)CoreTextData *data;
    @end
    View Code
    //  CTDispalyView.m
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import "CTDispalyView.h"
    
    //导入CoreText系统框架
    #import <CoreText/CoreText.h>
    
    @implementation CTDispalyView
    
    //重写drawRect方法
    - (void)drawRect:(CGRect)rect {
        
        [super drawRect:rect];
     
        //1.获取当前绘图上下文
        CGContextRef context = UIGraphicsGetCurrentContext();
        
        //2.旋转坐坐标系(默认和UIKit坐标是相反的)
        CGContextSetTextMatrix(context, CGAffineTransformIdentity);
        CGContextTranslateCTM(context, 0, self.bounds.size.height);
        CGContextScaleCTM(context, 1.0, -1.0);
        
       //3.绘制内容
        if (self.data) {
            CTFrameDraw(self.data.ctFrame, context);
        }
    }
    
    @end
    View Code

    除了这4个类外,在代码中还创建了基本的宏定义和分类Category,分别是CoreTextDemo.pch、UIView+Frame.h(快速访问view的尺寸)

    CoreTextDemo.pch

    //  CoreTextDemo.pch
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #ifndef CoreTextDemo_pch
    #define CoreTextDemo_pch
    
    
    #ifdef DEBUG    
    #define debugLog(...) NSLog(__VA_ARGS__)
    #define debugMethod() NSLog(@"%s",__func__)
    #else
    #define debugLog(...)
    #define debugMethod()
    #endif
    
    #define RGB(R,G,B) [UIColor colorWithRed:R/255.0 green:G/255.0 blue:B/255.0 alpha:1.0]
    
    #import <Foundation/Foundation.h>
    #import "UIView+Frame.h"
    #import <CoreText/CoreText.h>
    
    
    #endif
    View Code

    UIView+Frame.h

    //  UIView+Frame.h
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import <UIKit/UIKit.h>
    #import <Foundation/Foundation.h>
    
    @interface UIView (Frame)
    
    -(CGFloat)x;
    -(void)setX:(CGFloat)x;
    
    -(CGFloat)y;
    -(void)setY:(CGFloat)y;
    
    -(CGFloat)height;
    -(void)setHeight:(CGFloat)height;
    
    -(CGFloat)width;
    -(void)setWidth:(CGFloat)width;
    
    @end
    View Code
    //  UIView+Frame.m
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import "UIView+Frame.h"
    
    @implementation UIView (Frame)
    
    -(CGFloat)x{
        return self.frame.origin.x;
    }
    -(void)setX:(CGFloat)x{
        self.frame = CGRectMake(x, self.y, self.width, self.height);
    }
    
    -(CGFloat)y{
        return self.frame.origin.y;
    }
    -(void)setY:(CGFloat)y{
        self.frame = CGRectMake(self.x, y, self.width, self.height);
    }
    
    -(CGFloat)height{
        return self.frame.size.height;
    }
    -(void)setHeight:(CGFloat)height{
        self.frame = CGRectMake(self.x, self.y, self.width, height);
    }
    
    -(CGFloat)width{
        return self.frame.size.width;
    }
    -(void)setWidth:(CGFloat)width{
        self.frame = CGRectMake(self.x, self.y, width, self.height);
    }
    
    
    @end
    View Code

    示例2:不带图片的排版引擎,只是显示文本内容,设置文字的一些简单的属性信息

    //  ViewController.m
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import "ViewController.h"
    #import "CTDispalyView.h"
    #import "CTFrameParserConfig.h"
    #import "CoreTextData.h"
    #import "CTFrameParser.h"
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.view.backgroundColor = [UIColor whiteColor];
        
        //创建画布
        CTDispalyView *dispaleView = [[CTDispalyView alloc] initWithFrame:CGRectMake(0, 0, 300, 200)];
        dispaleView.center = CGPointMake(self.view.center.x, self.view.center.y-100);
        dispaleView.backgroundColor = [UIColor whiteColor];
        [self.view addSubview:dispaleView];
        
        //设置配置信息
        CTFrameParserConfig *config = [[CTFrameParserConfig alloc] init];
        config.textColor = [UIColor redColor];
        config.width = dispaleView.width;
        
        //设置内容
        CoreTextData *data = [CTFrameParser parseContent:@"CoreText是用于处理文字和字体的底层技术。"
                                                                   "它直接和Core Graphics(又被称为Quartz)打交道。"
                                                                   "Quartz是一个2D图形渲染引擎,能够处理OSX和iOS中图形显示问题。"
                                                                   "Quartz能够直接处理字体(font)和字形(glyphs),将文字渲染到界面上,它是基础库中唯一能够处理字形的模块。"
                                                                   "因此CoreText为了排版,需要将显示的文字内容、位置、字体、字形直接传递给Quartz。"
                                                                   "与其他UI组件相比,由于CoreText直接和Quartz来交互,所以它具有更高效的排版功能。" config:config];
        dispaleView.data = data;
        dispaleView.height = data.height;
        dispaleView.backgroundColor = [UIColor yellowColor];
    }
    
    @end
    View Code

    演示结果截图

    好了,效果确实是实现了,现在来看看本框架的UML示意图,这4个类的关系是这样的:

    1、CTFrameParser通过CTFrameParserConfig实例来生成CoreTextData实例;

    2、CTDisplayView通过持有CoreTextData实例来获取绘制所需要的所有信息;

    3、ViewController类通过配置CTFrameParserConfig实例,进而获得生成的CoreTextData实例,最后将其赋值给CTDisplayView成员,达到将指定内容显示在界面的效果。

    四、定制排版文件格式

    对于上面的例子,我们给CTFrameParser增加了一个将NSString转换为CoreTextData的方法。但是这样的实现方式有很多的局限性,因为整个内容虽然可以定制字体大小、颜色、行高等信息,但是却不能支持定制内容中某一个部分。例如,如果我们只想让内容的某几个字显示成红色并将字体变大,而让其他的文字显示成黑色而且字体不变,那么就办不到了。

    解决办法:让CTFrameParser支持接受NSAttributeString作为参数,然后在ViewController中设置我们想要的NSAttributeString信息。

    更改后的CTFrameParser

    //  CTFrameParser.h
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import <Foundation/Foundation.h>
    #import "CoreTextData.h"
    
    @class CTFrameParserConfig;
    @interface CTFrameParser : NSObject
    
    /**
     *  给内容设置配置信息
     *
     *  @param content 内容
     *  @param config  配置信息
     *
     */
    +(CoreTextData *)parseAttributedContent:(NSAttributedString *)content config:(CTFrameParserConfig *)config;
    
    /**
     *  配置信息格式化
     *
     *  @param config 配置信息
     */
    +(NSDictionary *)attributesWithConfig:(CTFrameParserConfig *)config;
    
    @end
    View Code
    //  CTFrameParser.m
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import "CTFrameParser.h"
    #import "CTFrameParserConfig.h"
    #import "CoreTextData.h"
    
    @implementation CTFrameParser
    
    //给内容设置配置信息
    +(CoreTextData *)parseAttributedContent:(NSAttributedString *)content config:(CTFrameParserConfig *)config{
        
        //创建CTFrameStterRef实例
        CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)content);
        
        //获得要绘制的区域的高度
        CGSize restrictSize = CGSizeMake(config.width, CGFLOAT_MAX);
        CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, 0), nil, restrictSize, nil);
        CGFloat textHeight = coreTextSize.height;
        
        //生成CTFrameRef实例
        CTFrameRef frame = [self createFrameWithFramesetter:framesetter config:config height:textHeight];
        
        //将生成好的CTFrameRef实例和计算好的绘制高度保存到CoreTextData实例中,最后返回CoreTextData实例
        CoreTextData *data = [[CoreTextData alloc] init];
        data.ctFrame = frame;
        data.height = textHeight;
        
        //释放内存
        CFRelease(framesetter);
        CFRelease(frame);
        
        return data;
    }
    
    
    //配置信息格式化
    +(NSDictionary *)attributesWithConfig:(CTFrameParserConfig *)config{
        
        CGFloat fontSize = config.fontSize;
        CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
        CGFloat lineSpcing = config.lineSpace;
        const CFIndex kNumberOfSettings = 3;
        CTParagraphStyleSetting theSettings[kNumberOfSettings] = {
            {kCTParagraphStyleSpecifierLineSpacingAdjustment,sizeof(CGFloat),&lineSpcing},
            {kCTParagraphStyleSpecifierMaximumLineSpacing,sizeof(CGFloat),&lineSpcing},
            {kCTParagraphStyleSpecifierMinimumLineSpacing,sizeof(CGFloat),&lineSpcing},
        };
        
        CTParagraphStyleRef theParagraphRef = CTParagraphStyleCreate(theSettings, kNumberOfSettings);
        UIColor *textColor = config.textColor;
       
        NSMutableDictionary *dict = [NSMutableDictionary dictionary];
        dict[(id)kCTForegroundColorAttributeName] = (id)textColor.CGColor;
        dict[(id)kCTFontAttributeName] = (__bridge id)fontRef;
        dict[(id)kCTParagraphStyleAttributeName] = (__bridge id)theParagraphRef;
        
        CFRelease(fontRef);
        CFRelease(theParagraphRef);
        return dict;
    }
    
    //创建CTFrameRef绘制路径实例
    +(CTFrameRef)createFrameWithFramesetter:(CTFramesetterRef)framesetter config:(CTFrameParserConfig *)config height:(CGFloat)height{
        
        CGMutablePathRef path = CGPathCreateMutable();
        CGPathAddRect(path, NULL, CGRectMake(0, 0, config.width, height));
        
        CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
        CFRelease(path);
        return frame;
    }
    
    @end
    View Code

    示例3:不带图片的排版引擎,只是显示文本内容,通过富文本更改文字的一些简单的属性信息

    //  ViewController.m
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import "ViewController.h"
    #import "CTDispalyView.h"
    #import "CTFrameParserConfig.h"
    #import "CoreTextData.h"
    #import "CTFrameParser.h"
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.view.backgroundColor = [UIColor whiteColor];
        
        //创建画布
        CTDispalyView *dispaleView = [[CTDispalyView alloc] initWithFrame:CGRectMake(0, 0, 300, 200)];
        dispaleView.center = CGPointMake(self.view.center.x, self.view.center.y-100);
        dispaleView.backgroundColor = [UIColor whiteColor];
        [self.view addSubview:dispaleView];
        
        //设置配置信息
        CTFrameParserConfig *config = [[CTFrameParserConfig alloc] init];
        config.textColor = [UIColor blackColor];
        config.width = dispaleView.width;
        
        //内容
        NSString *content =
                            @"CoreText是用于处理文字和字体的底层技术。"
                            "它直接和Core Graphics(又被称为Quartz)打交道。"
                            "Quartz是一个2D图形渲染引擎,能够处理OSX和iOS中图形显示问题。"
                            "Quartz能够直接处理字体(font)和字形(glyphs),将文字渲染到界面上,它是基础库中唯一能够处理字形的模块。"
                            "因此CoreText为了排版,需要将显示的文字内容、位置、字体、字形直接传递给Quartz。"
                            "与其他UI组件相比,由于CoreText直接和Quartz来交互,所以它具有更高效的排版功能。";
        
        //设置富文本
        NSDictionary *attr = [CTFrameParser attributesWithConfig:config];
        NSMutableAttributedString *attributeString = [[NSMutableAttributedString alloc] initWithString:content attributes:attr];
        [attributeString addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:26] range:NSMakeRange(0, 15)];
        [attributeString addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(0, 15)];
        
        //创建绘制数据实例
        CoreTextData *data = [CTFrameParser parseAttributedContent:attributeString config:config];
        dispaleView.data = data;
        dispaleView.height = data.height;
        dispaleView.backgroundColor = [UIColor yellowColor];
    }
    
    @end
    View Code

    演示结果截图

    更进一步,实际工作中,我们更希望通过一个排版文件,来设置需要排版的文字的内容、颜色、字体大小等信息。我们规定排版的模板文件为JSON格式。排版格式示例文件如下:

    [
     {
       "color":"blue",
       "content":"CoreText是用于处理文字和字体的底层技术。",
       "size":16,
       "type":"txt"
     },
     {
     "color":"red",
     "content":"它直接和Core Graphics(又被称为Quartz)打交道。",
     "size":22,
     "type":"txt"
     },
     {
     "color":"black",
     "content":"Quartz是一个2D图形渲染引擎,能够处理OSX和iOS中图形显示问题。",
     "size":16,
     "type":"txt"
     },
     {
     "color":"blue",
     "content":"Quartz能够直接处理字体(font)和字形(glyphs),将文字渲染到界面上,它是基础库中唯一能够处理字形的模块。",
     "size":16,
     "type":"txt"
     },
     {
     "color":"default",
     "content":"因此CoreText为了排版,需要将显示的文字内容、位置、字体、字形直接传递给Quartz。与其他UI组件相比,由于CoreText直接和Quartz来交互,所以它具有更高效的排版功能。",
     "type":"txt"
     }
    ]
    View Code

    通过苹果提供的NSJSONSeriallization类,我们可以将上面的模板文件转换成NSArray数组,每一个数组元素是一个Dictionary,代表一段相同设置的文字。为了简单,我们配置文件只支持配置颜色和字号,但是以后可以根据同样的思想,很方便地增加其他配置信息。

    现在修改CTFrameParser类,增加如下的这些方法,让其可以从如上格式的模板文件中生成CoreTextData。最终实现代码如下:

    更改后的CTFrameParser:

    //  CTFrameParser.h
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import <Foundation/Foundation.h>
    #import "CoreTextData.h"
    
    @class CTFrameParserConfig;
    @interface CTFrameParser : NSObject
    
    /**
     *  给内容设置配置信息
     *
     *  @param content 内容
     *  @param config  配置信息
     *
     */
    +(CoreTextData *)parseAttributedContent:(NSAttributedString *)content config:(CTFrameParserConfig *)config;
    
    
    /**
     *  给内容设置配置信息
     *
     *  @param path   模板文件路径
     *  @param config 配置信息
     *
     */
    +(CoreTextData *)parseTemplateFile:(NSString *)path config:(CTFrameParserConfig *)config;
    
    @end
    View Code
    //  CTFrameParser.m
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import "CTFrameParser.h"
    #import "CTFrameParserConfig.h"
    #import "CoreTextData.h"
    
    @implementation CTFrameParser
    
    //方法一:用于提供对外的接口,调用方法二实现从一个JSON的模板文件中读取内容,然后调用方法五生成的CoreTextData
    +(CoreTextData *)parseTemplateFile:(NSString *)path config:(CTFrameParserConfig *)config{
        
        NSAttributedString *content = [self loadTemplateFile:path config:config];
        return [self parseAttributedContent:content config:config];
    }
    
    //方法二:读取JSON文件内容,并且调用方法三获得从NSDcitionay到NSAttributedString的转换结果
    +(NSAttributedString *)loadTemplateFile:(NSString *)path config:(CTFrameParserConfig *)config{
        NSData *data = [NSData dataWithContentsOfFile:path];
        NSMutableAttributedString *result = [[NSMutableAttributedString alloc] init];
        if (data) {
            NSArray *array = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
            if ([array isKindOfClass:[NSArray class]]) {
                for (NSDictionary *dict in array) {
                    NSString *type = dict[@"type"];
                    if ([type isEqualToString:@"txt"]) {
                        NSAttributedString *as = [self parseAttributeContentFromNSDictionary:dict config:config];
                        [result appendAttributedString:as];
                    }
                }
            }
        }
        return  result;
    }
    
    //方法三:将NSDcitionay内容转换为NSAttributedString
    +(NSAttributedString *)parseAttributeContentFromNSDictionary:(NSDictionary*)dict config:(CTFrameParserConfig *)config{
        
        NSMutableDictionary *attributes = [NSMutableDictionary dictionaryWithDictionary:[self attributesWithConfig:config]];
        
        //设置颜色
        UIColor *color = [self colorFromTemplate:dict[@"color"]];
        if (color) {
            attributes[(id)kCTForegroundColorAttributeName] = (id)color.CGColor;
        }
        
        //设置字号
        CGFloat fontSize = [dict[@"size"] floatValue];
        if (fontSize>0) {
            CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
            attributes[(id)kCTFontAttributeName] = (__bridge id)fontRef;
            CFRelease(fontRef);
        }
        
        NSString *content = dict[@"content"];
        return [[NSAttributedString alloc] initWithString:content attributes:attributes];
    }
    
    //方法四:提供将NSString转换为UIColor的功能
    +(UIColor *)colorFromTemplate:(NSString *)name{
        
        if ([name isEqualToString:@"blue"]) {
            return [UIColor blueColor];
        }else if ([name isEqualToString:@"red"]){
            return [UIColor redColor];
        }else if ([name isEqualToString:@"black"]){
            return [UIColor blackColor];
        }else{
            return nil;
        }
    }
    
    //方法五:接受一个NSAttributedString和一个Config参数,将NSAttributedString转换成CoreTextData返回
    +(CoreTextData *)parseAttributedContent:(NSAttributedString *)content config:(CTFrameParserConfig *)config{
        
        //创建CTFrameStterRef实例
        CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)content);
        
        //获得要绘制的区域的高度
        CGSize restrictSize = CGSizeMake(config.width, CGFLOAT_MAX);
        CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, 0), nil, restrictSize, nil);
        CGFloat textHeight = coreTextSize.height;
        
        //生成CTFrameRef实例
        CTFrameRef frame = [self createFrameWithFramesetter:framesetter config:config height:textHeight];
        
        //将生成好的CTFrameRef实例和计算好的绘制高度保存到CoreTextData实例中,最后返回CoreTextData实例
        CoreTextData *data = [[CoreTextData alloc] init];
        data.ctFrame = frame;
        data.height = textHeight;
        
        //释放内存
        CFRelease(framesetter);
        CFRelease(frame);
        
        return data;
    }
    
    //方法六:方法五的一个辅助函数,供方法五调用
    +(CTFrameRef)createFrameWithFramesetter:(CTFramesetterRef)framesetter config:(CTFrameParserConfig *)config height:(CGFloat)height{
        
        CGMutablePathRef path = CGPathCreateMutable();
        CGPathAddRect(path, NULL, CGRectMake(0, 0, config.width, height));
        
        CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
        CFRelease(path);
        return frame;
    }
    
    @end
    View Code

    示例4:不带图片的排版引擎,只是显示文本内容,通过排版文件格式更改文字的一些简单的属性信息

    //
    //  ViewController.m
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import "ViewController.h"
    #import "CTDispalyView.h"
    #import "CTFrameParserConfig.h"
    #import "CoreTextData.h"
    #import "CTFrameParser.h"
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.view.backgroundColor = [UIColor whiteColor];
        
        //创建画布
        CTDispalyView *dispaleView = [[CTDispalyView alloc] initWithFrame:CGRectMake(0, 0, 300, 200)];
        dispaleView.center = CGPointMake(self.view.center.x, self.view.center.y-100);
        dispaleView.backgroundColor = [UIColor whiteColor];
        [self.view addSubview:dispaleView];
        
        //设置配置信息
        CTFrameParserConfig *config = [[CTFrameParserConfig alloc] init];
        config.width = dispaleView.width;
        
    
        //获取模板文件
        NSString *path = [[NSBundle mainBundle] pathForResource:@"JsonTemplate" ofType:@"json"];
        
        
        //创建绘制数据实例
        CoreTextData *data = [CTFrameParser parseTemplateFile:path config:config];
        dispaleView.data = data;
        dispaleView.height = data.height;
        dispaleView.backgroundColor = [UIColor yellowColor];
    }
    
    @end
    View Code

    演示结果截图

    可以看到,通过一个简单的模板文件,我们可以很方便地定义排版的配置信息了。

    五、支持图文混排的排版引擎

    在上面的示例中,我们在设置模板文件的时候,就专门在模板文件里面预留了一个名为type的字段,用于表示内容的类型。之前的type的值都是txt,这次,我们增加一个img的值,用于表示图片。同时给img类型的内容还需要配置3个属性如下:

    1、width:用于设置图片显示的宽度

    2、height:用于设置图片显示的高度

    3、name:用于设置图片的资源名

    也即文件格式如下:

     

    在改造代码之前,先来了解一下CTFrame内部的CTLine和CTRun。

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

    CTLine和CTRun示意图如下:

    示意图解释:

    可以看到,第一行的CTLine是由两个CTRun构成的,第一个CTRun为红色大字号的左边部分,第二个CTRun为右边黑色小字号部分。

    虽然我们不用管理CTRun的创建过程,但是我们可以设置某一个具体的CTRun的CTRunDelegate来指定该文本在绘制时的高度、宽度、排列对齐方式等信息。

    对于图片的排版,其实,CoreText本质上是不支持的,但是,可以在显示文本的地方,用一个特殊的空白字符代替,同时设置该字体的CTRunDelegate信息为要显示的图片的宽度和高度信息,这样最后生成的CTFrame实例,就会在绘制时将图片的位置预留出来。以后,在CTDisplayView的drawRect方法中使CGContextDrawImage方法直接绘制出来就行了。

    改造模板解析类,要做的工作有:

    • 增加一个CoreTextImageData类,寄存图片信息
    • 改造CTFrameParser的parserTemplateFile:(NSString *)path config:(CTFrameParserConfig *)config方法,使其支持type为omg的节点解析。并且对type为omg的节点,设置其CTRunDelegate信息,使其在绘制时,为图片预留相应的空白位置。
    • 改造CoreTextData类,增加图片相关的信息,并且增加计算图片绘制局域的逻辑。
    • 改造CTDisplayView类,增加绘制图片的相关的逻辑。

    具体的改造如下:

    新添加CoreTextImageData类:

    //  CoreTextImageData.h
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/26.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import <Foundation/Foundation.h>
    
    @interface CoreTextImageData : NSObject
    
    //图片资源名称
    @property (copy,nonatomic)NSString *name;
    //图片位置的起始点
    @property (assign,nonatomic)CGFloat position;
    //图片的尺寸
    @property (assign,nonatomic)CGRect imagePostion;
    
    @end
    View Code
    //  CoreTextImageData.m
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/26.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import "CoreTextImageData.h"
    
    @implementation CoreTextImageData
    
    @end
    View Code

    修改CTFrameParser解析类:

    //  CTFrameParser.h
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import <Foundation/Foundation.h>
    #import "CoreTextData.h"
    
    @class CTFrameParserConfig;
    @interface CTFrameParser : NSObject
    
    /**
     *  配置信息格式化
     *
     *  @param config 配置信息
     */
    +(NSDictionary *)attributesWithConfig:(CTFrameParserConfig *)config;
    
    
    /**
     *  给内容设置配置信息
     *
     *  @param content 内容
     *  @param config  配置信息
     */
    +(CoreTextData *)parseAttributedContent:(NSAttributedString *)content config:(CTFrameParserConfig *)config;
    
    /**
     *  给内容设置配置信息
     *
     *  @param path   模板文件路径
     *  @param config 配置信息
     */
    +(CoreTextData *)parseTemplateFile:(NSString *)path config:(CTFrameParserConfig *)config;
    
    @end
    View Code
    //  CTFrameParser.m
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import "CTFrameParser.h"
    #import "CTFrameParserConfig.h"
    #import "CoreTextData.h"
    #import "CoreTextImageData.h"
    
    @implementation CTFrameParser
    
    
    //配置信息格式化
    +(NSDictionary *)attributesWithConfig:(CTFrameParserConfig *)config{
        
        CGFloat fontSize = config.fontSize;
        CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
        CGFloat lineSpcing = config.lineSpace;
        const CFIndex kNumberOfSettings = 3;
        CTParagraphStyleSetting theSettings[kNumberOfSettings] = {
            {kCTParagraphStyleSpecifierLineSpacingAdjustment,sizeof(CGFloat),&lineSpcing},
            {kCTParagraphStyleSpecifierMaximumLineSpacing,sizeof(CGFloat),&lineSpcing},
            {kCTParagraphStyleSpecifierMinimumLineSpacing,sizeof(CGFloat),&lineSpcing},
        };
        
        CTParagraphStyleRef theParagraphRef = CTParagraphStyleCreate(theSettings, kNumberOfSettings);
        UIColor *textColor = config.textColor;
       
        NSMutableDictionary *dict = [NSMutableDictionary dictionary];
        dict[(id)kCTForegroundColorAttributeName] = (id)textColor.CGColor;
        dict[(id)kCTFontAttributeName] = (__bridge id)fontRef;
        dict[(id)kCTParagraphStyleAttributeName] = (__bridge id)theParagraphRef;
        
        CFRelease(fontRef);
        CFRelease(theParagraphRef);
        return dict;
    }
    
    
    
    #pragma mark - 新增的方法
    
    //方法一:用于提供对外的接口,调用方法二实现从一个JSON的模板文件中读取内容,然后调用方法五生成的CoreTextData
    +(CoreTextData *)parseTemplateFile:(NSString *)path config:(CTFrameParserConfig *)config{
        
        NSMutableArray *imageArray = [NSMutableArray array];
        NSAttributedString *content = [self loadTemplateFile:path config:config imageArray:imageArray];
        CoreTextData *data = [self parseAttributedContent:content config:config];
        data.imageArray = imageArray;
        
        return data;
    }
    
    //方法二:读取JSON文件内容,并且调用方法三获得从NSDcitionay到NSAttributedString的转换结果
    +(NSAttributedString *)loadTemplateFile:(NSString *)path config:(CTFrameParserConfig *)config imageArray:(NSMutableArray *)imageArray{
        NSData *data = [NSData dataWithContentsOfFile:path];
        NSMutableAttributedString *result = [[NSMutableAttributedString alloc] init];
        if (data) {
            
            NSArray *array = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
            
            if ([array isKindOfClass:[NSArray class]]) {
                for (NSDictionary *dict in array) {
                    
                    NSString *type = dict[@"type"];
                    
                    if ([type isEqualToString:@"txt"]) {
                        
                        NSAttributedString *as = [self parseAttributeContentFromNSDictionary:dict config:config];
                        [result appendAttributedString:as];
                        
                    }else if ([type isEqualToString:@"img"]){
                        
                        //创建CoreTextImageData,保存图片到imageArray数组中
                        CoreTextImageData *imageData = [[CoreTextImageData alloc] init];
                        imageData.name = dict[@"name"];
                        imageData.position = [result length];
                        [imageArray addObject:imageData];
                        
                        //创建空白占位符,并且设置它的CTRunDelegate信息
                        NSAttributedString *as = [self parseImageDataFromNSDictionary:dict config:config];
                        [result appendAttributedString:as];
                    }
                }
            }
        }
        return  result;
    }
    
    //方法三:将NSDcitionay内容转换为NSAttributedString
    +(NSAttributedString *)parseAttributeContentFromNSDictionary:(NSDictionary*)dict config:(CTFrameParserConfig *)config{
        
        NSMutableDictionary *attributes = [NSMutableDictionary dictionaryWithDictionary:[self attributesWithConfig:config]];
        
        //设置颜色
        UIColor *color = [self colorFromTemplate:dict[@"color"]];
        if (color) {
            attributes[(id)kCTForegroundColorAttributeName] = (id)color.CGColor;
        }
        
        //设置字号
        CGFloat fontSize = [dict[@"size"] floatValue];
        if (fontSize>0) {
            CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
            attributes[(id)kCTFontAttributeName] = (__bridge id)fontRef;
            CFRelease(fontRef);
        }
        
        NSString *content = dict[@"content"];
        return [[NSAttributedString alloc] initWithString:content attributes:attributes];
    }
    
    //方法四:提供将NSString转换为UIColor的功能
    +(UIColor *)colorFromTemplate:(NSString *)name{
        
        if ([name isEqualToString:@"blue"]) {
            return [UIColor blueColor];
        }else if ([name isEqualToString:@"red"]){
            return [UIColor redColor];
        }else if ([name isEqualToString:@"black"]){
            return [UIColor blackColor];
        }else{
            return nil;
        }
    }
    
    //方法五:接受一个NSAttributedString和一个Config参数,将NSAttributedString转换成CoreTextData返回
    +(CoreTextData *)parseAttributedContent:(NSAttributedString *)content config:(CTFrameParserConfig *)config{
        
        //创建CTFrameStterRef实例
        CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)content);
        
        //获得要绘制的区域的高度
        CGSize restrictSize = CGSizeMake(config.width, CGFLOAT_MAX);
        CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, 0), nil, restrictSize, nil);
        CGFloat textHeight = coreTextSize.height;
        
        //生成CTFrameRef实例
        CTFrameRef frame = [self createFrameWithFramesetter:framesetter config:config height:textHeight];
        
        //将生成好的CTFrameRef实例和计算好的绘制高度保存到CoreTextData实例中,最后返回CoreTextData实例
        CoreTextData *data = [[CoreTextData alloc] init];
        data.ctFrame = frame;
        data.height = textHeight;
        
        //释放内存
        CFRelease(framesetter);
        CFRelease(frame);
        
        return data;
    }
    
    //方法六:方法五的一个辅助函数,供方法五调用
    +(CTFrameRef)createFrameWithFramesetter:(CTFramesetterRef)framesetter config:(CTFrameParserConfig *)config height:(CGFloat)height{
        
        CGMutablePathRef path = CGPathCreateMutable();
        CGPathAddRect(path, NULL, CGRectMake(0, 0, config.width, height));
        
        CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
        CFRelease(path);
        return frame;
    }
    
    #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];
    }
    +(NSAttributedString *)parseImageDataFromNSDictionary:(NSDictionary *)dict config:(CTFrameParserConfig *)config{
        
        CTRunDelegateCallbacks callbacks;
        memset(&callbacks, 0, sizeof(CTRunDelegateCallbacks));
        callbacks.version = kCTRunDelegateVersion1;
        callbacks.getAscent = ascentCallback;
        callbacks.getDescent = descentCallback;
        callbacks.getWidth = widthCallback;
        CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (__bridge void *)dict);
        
        //使用0xFFFC作为空白占位符
        unichar objectReplacementChar = 0xFFFC;
        NSString *content = [NSString stringWithCharacters:&objectReplacementChar length:1];
        NSDictionary *attributes = [self attributesWithConfig:config];
        NSMutableAttributedString *space = [[NSMutableAttributedString alloc] initWithString:content attributes:attributes];
        CFAttributedStringSetAttribute((CFMutableAttributedStringRef)space, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);
        CFRelease(delegate);
        return space;
    }
    
    @end
    View Code

    改造CoreTextData类:

    //  CoreTextData.h
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import <Foundation/Foundation.h>
    
    @interface CoreTextData : NSObject
    
    @property (assign,nonatomic)CTFrameRef ctFrame;
    @property (assign,nonatomic)CGFloat height;
    
    //新增加的成员
    @property (strong,nonatomic)NSArray *imageArray;
    
    @end
    View Code
    //  CoreTextData.m
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import "CoreTextData.h"
    #import "CoreTextImageData.h"
    
    @implementation CoreTextData
    
    //CoreFoundation不支持ARC,需要手动去管理内存的释放
    -(void)setCtFrame:(CTFrameRef)ctFrame{
        if (_ctFrame != ctFrame) {
            if (_ctFrame !=nil) {
                CFRelease(_ctFrame);
            }
        }
        CFRetain(ctFrame);
        _ctFrame = ctFrame;
    }
    
    -(void)dealloc{
        if (_ctFrame != nil) {
            CFRelease(_ctFrame);
            _ctFrame = nil;
        }
    }
    
    -(void)setImageArray:(NSArray *)imageArray{
        _imageArray = imageArray;
        [self fillImagePosition];
        
    }
    //填充图片
    -(void)fillImagePosition{
        if (self.imageArray.count==0) {
            return;
        }
        NSArray *lines = (NSArray *)CTFrameGetLines(self.ctFrame);
        NSInteger lineCount = [lines count];
        CGPoint lineOrigins[lineCount];
        CTFrameGetLineOrigins(self.ctFrame, CFRangeMake(0, 0), lineOrigins);
        
        int imgIndex = 0;
        CoreTextImageData *imageData = self.imageArray[0];
        for (int i=0; i<lineCount; i++) {
            if (imageData==nil) {
                break;
            }
            CTLineRef line = (__bridge CTLineRef)lines[i];
            NSArray *runObjArray = (NSArray *)CTLineGetGlyphRuns(line);
            for (id runObj in runObjArray) {
                CTRunRef run = (__bridge CTRunRef)runObj;
                NSDictionary *runAttributes = (NSDictionary *)CTRunGetAttributes(run);
                CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[runAttributes valueForKey:(id)kCTRunDelegateAttributeName];
                if (delegate == nil) {
                    continue;
                }
                
                NSDictionary *metaDic = CTRunDelegateGetRefCon(delegate);
                if (![metaDic isKindOfClass:[NSDictionary class]]) {
                    continue;
                }
                
                CGRect runBounds;
                CGFloat ascent;
                CGFloat descent;
                runBounds.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
                runBounds.size.height = ascent + descent;
                
                CGFloat x0ffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
                runBounds.origin.x = lineOrigins[i].x + x0ffset;
                runBounds.origin.y = lineOrigins[i].y;
                runBounds.origin.y -= descent;
                
                CGPathRef pathRef = CTFrameGetPath(self.ctFrame);
                CGRect colRect = CGPathGetBoundingBox(pathRef);
                CGRect delegateBounds = CGRectOffset(runBounds, colRect.origin.x, colRect.origin.y);
                
                imageData.imagePostion = delegateBounds;
                imgIndex ++;
                if (imgIndex == self.imageArray.count) {
                    imageData = nil;
                    break;
                }else{
                    imageData = self.imageArray[imgIndex];
                }
            }
        }
    }
    
    @end
    View Code

    改造CTDisplayView类:

    //  CTDispalyView.h
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import <UIKit/UIKit.h>
    #import "CoreTextData.h"
    
    @interface CTDispalyView : UIView
    @property(strong,nonatomic)CoreTextData *data;
    @end
    View Code
    //  CTDispalyView.m
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import "CTDispalyView.h"
    #import "CoreTextImageData.h"
    
    //导入CoreText系统框架
    #import <CoreText/CoreText.h>
    
    @implementation CTDispalyView
    
    //重写drawRect方法
    - (void)drawRect:(CGRect)rect {
        
        [super drawRect:rect];
     
        //1.获取当前绘图上下文
        CGContextRef context = UIGraphicsGetCurrentContext();
        
        //2.旋转坐坐标系(默认和UIKit坐标是相反的)
        CGContextSetTextMatrix(context, CGAffineTransformIdentity);
        CGContextTranslateCTM(context, 0, self.bounds.size.height);
        CGContextScaleCTM(context, 1.0, -1.0);
        
        if (self.data) {
            
            CTFrameDraw(self.data.ctFrame, context);
            for (CoreTextImageData *imageData in self.data.imageArray) {
                
                UIImage *image = [UIImage imageNamed:imageData.name];
                CGContextDrawImage(context, imageData.imagePostion, image.CGImage);
            }
        }
    }
    
    @end
    View Code

    示例5:带图片的排版引擎,显示文本内容和图片,通过排版文件格式更改文字的一些简单的属性信息

    //  ViewController.m
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import "ViewController.h"
    #import "CTDispalyView.h"
    #import "CTFrameParserConfig.h"
    #import "CoreTextData.h"
    #import "CTFrameParser.h"
    
    @interface ViewController ()
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.view.backgroundColor = [UIColor whiteColor];
        
        //创建画布
        CTDispalyView *dispaleView = [[CTDispalyView alloc] initWithFrame:self.view.bounds];
        dispaleView.backgroundColor = [UIColor whiteColor];
        [self.view addSubview:dispaleView];
        
        //设置配置信息
        CTFrameParserConfig *config = [[CTFrameParserConfig alloc] init];
        config.width = dispaleView.width;
        
    
        //获取模板文件
        NSString *path = [[NSBundle mainBundle] pathForResource:@"JsonTemplate" ofType:@"json"];
        
        //创建绘制数据实例
        CoreTextData *data = [CTFrameParser parseTemplateFile:path config:config];
        dispaleView.data = data;
        dispaleView.height = data.height;
        dispaleView.backgroundColor = [UIColor yellowColor];
    }
    
    @end
    View Code

    测试效果图如下:

    六、添加对图片的点击支持

    实现方式

    为了实现对图片的点击支持,我们需要给CTDisplayView类增加用户点击操作的检测函数,在检测函数中,判断当前用户点击的局域是否在图片上,如果在图片上,则触发点击图片的逻辑。拼过提供的UITapGestureRecognizer可以很好地满足我们的要求,所以我们这里用它来检测用户的点击操作。

    这里我们实现的是点击图片后,显示图片。实际开发中,可以根据业务需求去调整点击后的效果。

    CTDisplayView类实现如下,增加点击手势:

    //  CTDispalyView.m
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import "CTDispalyView.h"
    #import "CoreTextImageData.h"
    
    //导入CoreText系统框架
    #import <CoreText/CoreText.h>
    
    @interface CTDispalyView ()<UIGestureRecognizerDelegate>
    @property (strong,nonatomic)UIImageView *tapImgeView;
    @property (strong,nonatomic)UIView *coverView;
    @end
    
    @implementation CTDispalyView
    
    //初始化方法
    -(instancetype)initWithFrame:(CGRect)frame{
        self = [super initWithFrame:frame];
        if (self) {
            [self setupEvents];
        }
        return self;
    }
    
    //添加点击手势
    -(void)setupEvents{
        
        UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(userTapGestureDetected:)];
        tapRecognizer.delegate = self;
        [self addGestureRecognizer:tapRecognizer];
        self.userInteractionEnabled = YES;
    }
    
    
    //增加UITapGestureRecognizer的回调函数
    -(void)userTapGestureDetected:(UITapGestureRecognizer *)recognizer{
        
        CGPoint point = [recognizer locationInView:self];
        for (CoreTextImageData *imagData in self.data.imageArray) {
            
            //翻转坐标系,因为ImageData中的坐标是CoreText的坐标系
            CGRect imageRect = imagData.imagePostion;
            CGPoint imagePosition = imageRect.origin;
            imagePosition.y = self.bounds.size.height - imageRect.origin.y - imageRect.size.height;
            CGRect rect = CGRectMake(imagePosition.x, imagePosition.y, imageRect.size.width, imageRect.size.height);
            
            //检测点击位置Point是否在rect之内
            if (CGRectContainsPoint(rect, point)) {
                
                //在这里处理点击后的逻辑
                [self showTapImage:[UIImage imageNamed:imagData.name]];
                break;
            }
        }
    }
    
    //显示图片
    -(void)showTapImage:(UIImage *)tapImage{
        
        UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
        
        //图片
        _tapImgeView = [[UIImageView alloc] initWithImage:tapImage];
        _tapImgeView.frame = CGRectMake(0, 0, 300, 200);
        _tapImgeView.center = keyWindow.center;
        
        
        //蒙版
        _coverView = [[UIView alloc] initWithFrame:keyWindow.bounds];
        [_coverView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(cancel)]];
        _coverView.backgroundColor = [UIColor colorWithRed:0/255.0 green:0/255.0 blue:0/255.0 alpha:0.6];
        _coverView.userInteractionEnabled = YES;
        
        [keyWindow addSubview:_coverView];
        [keyWindow addSubview:_tapImgeView];
    }
    
    -(void)cancel{
        [_tapImgeView removeFromSuperview];
        [_coverView removeFromSuperview];
    }
    
    
    //重写drawRect方法
    - (void)drawRect:(CGRect)rect {
        
        [super drawRect:rect];
     
        //1.获取当前绘图上下文
        CGContextRef context = UIGraphicsGetCurrentContext();
        
        //2.旋转坐坐标系(默认和UIKit坐标是相反的)
        CGContextSetTextMatrix(context, CGAffineTransformIdentity);
        CGContextTranslateCTM(context, 0, self.bounds.size.height);
        CGContextScaleCTM(context, 1.0, -1.0);
        
        if (self.data) {
            
            CTFrameDraw(self.data.ctFrame, context);
            for (CoreTextImageData *imageData in self.data.imageArray) {
                
                UIImage *image = [UIImage imageNamed:imageData.name];
                CGContextDrawImage(context, imageData.imagePostion, image.CGImage);
            }
        }
    }
    
    @end
    View Code

    点击图片演示截图:

     

    七、添加对链接的点击支持

    实现方式:需要修改模板文件,增加一个名为”link”的类型,用于表示链接内容。格式如下:

    首先增加一个CoreTextLinkData类,用于记录解析JSON文件时的链接信息:

    CoreTextLinkData

    //  CoreTextLinkData.h
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/26.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import <Foundation/Foundation.h>
    
    @interface CoreTextLinkData : NSObject
    
    @property (copy, nonatomic)NSString *title;
    @property (copy, nonatomic)NSString *url;
    @property (assign, nonatomic)NSRange range;
    
    @end
    View Code
    //  CoreTextLinkData.m
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/26.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import "CoreTextLinkData.h"
    
    @implementation CoreTextLinkData
    
    @end
    View Code

    接着增加一个工具类CoreTextUtils类,用于检测链接是否被点击:

    CoreTextUtils:

    //  CoreTextUtils.h
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/26.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import <Foundation/Foundation.h>
    #import "CoreTextLinkData.h"
    #import "CoreTextData.h"
    
    @interface CoreTextUtils : NSObject
    
    /**
     *  检测点击位置是否在链接上
     *
     *  @param view  点击区域
     *  @param point 点击坐标
     *  @param data  数据源
     */
    +(CoreTextLinkData *)touchLinkInView:(UIView *)view atPoint:(CGPoint)point data:(CoreTextData *)data;
    
    
    @end
    View Code
    //  CoreTextUtils.m
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/26.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import "CoreTextUtils.h"
    
    @implementation CoreTextUtils
    
    //检测点击位置是否在链接上
    +(CoreTextLinkData *)touchLinkInView:(UIView *)view atPoint:(CGPoint)point data:(CoreTextData *)data{
        
        CTFrameRef textFrame = data.ctFrame;
        CFArrayRef lines = CTFrameGetLines(textFrame);
        if (!lines) return nil;
        CFIndex count = CFArrayGetCount(lines);
        CoreTextLinkData *foundLink = nil;
        
        //获得每一行的origin坐标
        CGPoint origins[count];
        CTFrameGetLineOrigins(textFrame, CFRangeMake(0, 0), origins);
        
        //翻转坐标系
        CGAffineTransform tranform = CGAffineTransformMakeTranslation(0, view.bounds.size.height);
        tranform = CGAffineTransformScale(tranform, 1.f, -1.f);
        for (int i=0; i<count; i++) {
            CGPoint linePoint = origins[i];
            CTLineRef line = CFArrayGetValueAtIndex(lines, i);
            
            //获取每一行的CGRect信息
            CGRect flippedRect = [self getLineBounds:line point:linePoint];
            CGRect rect = CGRectApplyAffineTransform(flippedRect, tranform);
            
            if (CGRectContainsPoint(rect, point)) {
                //将点击的坐标转换成相对于当前行的坐标
                CGPoint relativePoint = CGPointMake(point.x-CGRectGetMinX(rect), point.y-CGRectGetMinY(rect));
                
                //获得当前点击坐标对应的字符串偏移
                CFIndex idx = CTLineGetStringIndexForPosition(line, relativePoint);
                
                //判断这个偏移是否在我们的链接列表中
                foundLink = [self linkAtIndex:idx linkArray:data.linkArray];
                
                return foundLink;
            }
        }
        return nil;
    }
    
    //获取每一行的CGRect信息
    +(CGRect)getLineBounds:(CTLineRef)line point:(CGPoint)point{
        CGFloat ascent = 0.0f;
        CGFloat descent = 0.0f;
        CGFloat leading = 0.0f;
        CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
        CGFloat height = ascent + descent;
        return CGRectMake(point.x, point.y, width, height);
    }
    
    //判断这个偏移是否在我们的链接列表中
    +(CoreTextLinkData *)linkAtIndex:(CFIndex)i linkArray:(NSArray *)linkArray{
        
        CoreTextLinkData *link = nil;
        for (CoreTextLinkData *data in linkArray) {
            if (NSLocationInRange(i, data.range)) {
                link = data;
                break;
            }
        }
        return link;
    }
    
    @end
    View Code

    然后依次改造CTFrameParser类,CoreTextData类,CTDisplayView类

    CTFrameParser:

    //  CTFrameParser.h
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import <Foundation/Foundation.h>
    #import "CoreTextData.h"
    
    @class CTFrameParserConfig;
    @interface CTFrameParser : NSObject
    
    /**
     *  给内容设置配置信息
     *
     *  @param content 内容
     *  @param config  配置信息
     *
     */
    +(CoreTextData *)parseContent:(NSString *)content config:(CTFrameParserConfig *)config;
    
    /**
     *  配置信息格式化
     *
     *  @param config 配置信息
     */
    +(NSDictionary *)attributesWithConfig:(CTFrameParserConfig *)config;
    
    
    //=======================================================================================================//
    
    
    /**
     *  给内容设置配置信息
     *
     *  @param content 内容
     *  @param config  配置信息
     */
    +(CoreTextData *)parseAttributedContent:(NSAttributedString *)content config:(CTFrameParserConfig *)config;
    
    /**
     *  给内容设置配置信息
     *
     *  @param path   模板文件路径
     *  @param config 配置信息
     */
    +(CoreTextData *)parseTemplateFile:(NSString *)path config:(CTFrameParserConfig *)config;
    
    @end
    View Code
    //  CTFrameParser.m
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import "CTFrameParser.h"
    #import "CTFrameParserConfig.h"
    #import "CoreTextData.h"
    #import "CoreTextImageData.h"
    #import "CoreTextLinkData.h"
    
    @implementation CTFrameParser
    
    //给内容设置配置信息
    +(CoreTextData *)parseContent:(NSString *)content config:(CTFrameParserConfig *)config{
        
        NSDictionary *attributes = [self attributesWithConfig:config];
        NSAttributedString *contextString = [[NSAttributedString alloc] initWithString:content attributes:attributes];
        
        //创建CTFrameStterRef实例
        CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)contextString);
        
        //获得要绘制的区域的高度
        CGSize restrictSize = CGSizeMake(config.width, CGFLOAT_MAX);
        CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, 0), nil, restrictSize, nil);
        CGFloat textHeight = coreTextSize.height;
        
        //生成CTFrameRef实例
        CTFrameRef frame = [self createFrameWithFramesetter:framesetter config:config height:textHeight];
        
        //将生成好的CTFrameRef实例和计算好的绘制高度保存到CoreTextData实例中,最后返回CoreTextData实例
        CoreTextData *data = [[CoreTextData alloc] init];
        data.ctFrame = frame;
        data.height = textHeight;
        
        //释放内存
        CFRelease(framesetter);
        CFRelease(frame);
        
        return data;
    }
    
    //配置信息格式化
    +(NSDictionary *)attributesWithConfig:(CTFrameParserConfig *)config{
        
        CGFloat fontSize = config.fontSize;
        CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
        CGFloat lineSpcing = config.lineSpace;
        const CFIndex kNumberOfSettings = 3;
        CTParagraphStyleSetting theSettings[kNumberOfSettings] = {
            {kCTParagraphStyleSpecifierLineSpacingAdjustment,sizeof(CGFloat),&lineSpcing},
            {kCTParagraphStyleSpecifierMaximumLineSpacing,sizeof(CGFloat),&lineSpcing},
            {kCTParagraphStyleSpecifierMinimumLineSpacing,sizeof(CGFloat),&lineSpcing},
        };
        
        CTParagraphStyleRef theParagraphRef = CTParagraphStyleCreate(theSettings, kNumberOfSettings);
        UIColor *textColor = config.textColor;
       
        NSMutableDictionary *dict = [NSMutableDictionary dictionary];
        dict[(id)kCTForegroundColorAttributeName] = (id)textColor.CGColor;
        dict[(id)kCTFontAttributeName] = (__bridge id)fontRef;
        dict[(id)kCTParagraphStyleAttributeName] = (__bridge id)theParagraphRef;
        
        CFRelease(fontRef);
        CFRelease(theParagraphRef);
        return dict;
    }
    
    
    
    #pragma mark - 新增的方法
    
    //方法一:用于提供对外的接口,调用方法二实现从一个JSON的模板文件中读取内容,然后调用方法五生成的CoreTextData
    +(CoreTextData *)parseTemplateFile:(NSString *)path config:(CTFrameParserConfig *)config{
        
        NSMutableArray *imageArray = [NSMutableArray array];
        NSMutableArray *linkArray  = [NSMutableArray array];
        NSAttributedString *content = [self loadTemplateFile:path config:config imageArray:imageArray linkArray:linkArray];
        CoreTextData *data = [self parseAttributedContent:content config:config];
        data.imageArray = imageArray;
        data.linkArray = linkArray;
        return data;
    }
    
    //方法二:读取JSON文件内容,并且调用方法三获得从NSDcitionay到NSAttributedString的转换结果
    +(NSAttributedString *)loadTemplateFile:(NSString *)path config:(CTFrameParserConfig *)config
                                 imageArray:(NSMutableArray *)imageArray
                                 linkArray:(NSMutableArray *)linkArray{
        NSData *data = [NSData dataWithContentsOfFile:path];
        NSMutableAttributedString *result = [[NSMutableAttributedString alloc] init];
        if (data) {
            
            NSArray *array = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
            
            if ([array isKindOfClass:[NSArray class]]) {
                for (NSDictionary *dict in array) {
                    
                    NSString *type = dict[@"type"];
                    
                    if ([type isEqualToString:@"txt"]) {
                        
                        NSAttributedString *as = [self parseAttributeContentFromNSDictionary:dict config:config];
                        [result appendAttributedString:as];
                        
                    }else if ([type isEqualToString:@"img"]){
                        
                        //创建CoreTextImageData,保存图片到imageArray数组中
                        CoreTextImageData *imageData = [[CoreTextImageData alloc] init];
                        imageData.name = dict[@"name"];
                        imageData.position = [result length];
                        [imageArray addObject:imageData];
                        
                        //创建空白占位符,并且设置它的CTRunDelegate信息
                        NSAttributedString *as = [self parseImageDataFromNSDictionary:dict config:config];
                        [result appendAttributedString:as];
                    }
                    else if ([type isEqualToString:@"link"]){
                        
                        NSUInteger startPos = result.length;
                        NSAttributedString *as = [self parseAttributeContentFromNSDictionary:dict config:config];
                        [result appendAttributedString:as];
                        
                        //创建CoreTextLinkData
                        NSUInteger length = result.length - startPos;
                        NSRange linkRange = NSMakeRange(startPos, length);
                        CoreTextLinkData *linkData = [[CoreTextLinkData alloc] init];
                        linkData.title = dict[@"content"];
                        linkData.url   = dict[@"url"];
                        linkData.range = linkRange;
                        [linkArray addObject:linkData];
                    }
                }
            }
        }
        return  result;
    }
    
    //方法三:将NSDcitionay内容转换为NSAttributedString
    +(NSAttributedString *)parseAttributeContentFromNSDictionary:(NSDictionary*)dict config:(CTFrameParserConfig *)config{
        
        NSMutableDictionary *attributes = [NSMutableDictionary dictionaryWithDictionary:[self attributesWithConfig:config]];
        
        //设置颜色
        UIColor *color = [self colorFromTemplate:dict[@"color"]];
        if (color) {
            attributes[(id)kCTForegroundColorAttributeName] = (id)color.CGColor;
        }
        
        //设置字号
        CGFloat fontSize = [dict[@"size"] floatValue];
        if (fontSize>0) {
            CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);
            attributes[(id)kCTFontAttributeName] = (__bridge id)fontRef;
            CFRelease(fontRef);
        }
        
        NSString *content = dict[@"content"];
        return [[NSAttributedString alloc] initWithString:content attributes:attributes];
    }
    
    //方法四:提供将NSString转换为UIColor的功能
    +(UIColor *)colorFromTemplate:(NSString *)name{
        
        if ([name isEqualToString:@"blue"]) {
            return [UIColor blueColor];
        }else if ([name isEqualToString:@"red"]){
            return [UIColor redColor];
        }else if ([name isEqualToString:@"black"]){
            return [UIColor blackColor];
        }else{
            return nil;
        }
    }
    
    //方法五:接受一个NSAttributedString和一个Config参数,将NSAttributedString转换成CoreTextData返回
    +(CoreTextData *)parseAttributedContent:(NSAttributedString *)content config:(CTFrameParserConfig *)config{
        
        //创建CTFrameStterRef实例
        CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)content);
        
        //获得要绘制的区域的高度
        CGSize restrictSize = CGSizeMake(config.width, CGFLOAT_MAX);
        CGSize coreTextSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, 0), nil, restrictSize, nil);
        CGFloat textHeight = coreTextSize.height;
        
        //生成CTFrameRef实例
        CTFrameRef frame = [self createFrameWithFramesetter:framesetter config:config height:textHeight];
        
        //将生成好的CTFrameRef实例和计算好的绘制高度保存到CoreTextData实例中,最后返回CoreTextData实例
        CoreTextData *data = [[CoreTextData alloc] init];
        data.ctFrame = frame;
        data.height = textHeight;
        
        //释放内存
        CFRelease(framesetter);
        CFRelease(frame);
        
        return data;
    }
    
    //方法六:方法五的一个辅助函数,供方法五调用
    +(CTFrameRef)createFrameWithFramesetter:(CTFramesetterRef)framesetter config:(CTFrameParserConfig *)config height:(CGFloat)height{
        
        CGMutablePathRef path = CGPathCreateMutable();
        CGPathAddRect(path, NULL, CGRectMake(0, 0, config.width, height));
        
        CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
        CFRelease(path);
        return frame;
    }
    
    #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];
    }
    +(NSAttributedString *)parseImageDataFromNSDictionary:(NSDictionary *)dict config:(CTFrameParserConfig *)config{
        
        CTRunDelegateCallbacks callbacks;
        memset(&callbacks, 0, sizeof(CTRunDelegateCallbacks));
        callbacks.version = kCTRunDelegateVersion1;
        callbacks.getAscent = ascentCallback;
        callbacks.getDescent = descentCallback;
        callbacks.getWidth = widthCallback;
        CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, (__bridge void *)dict);
        
        //使用0xFFFC作为空白占位符
        unichar objectReplacementChar = 0xFFFC;
        NSString *content = [NSString stringWithCharacters:&objectReplacementChar length:1];
        NSDictionary *attributes = [self attributesWithConfig:config];
        NSMutableAttributedString *space = [[NSMutableAttributedString alloc] initWithString:content attributes:attributes];
        CFAttributedStringSetAttribute((CFMutableAttributedStringRef)space, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);
        CFRelease(delegate);
        return space;
    }
    
    @end
    View Code

    CoreTextData:

    //
    //  CoreTextData.h
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import <Foundation/Foundation.h>
    
    @interface CoreTextData : NSObject
    
    @property (assign,nonatomic)CTFrameRef ctFrame;
    @property (assign,nonatomic)CGFloat height;
    
    //新增加的成员
    @property (strong,nonatomic)NSArray *imageArray;
    @property (strong,nonatomic)NSArray *linkArray;
    
    @end
    View Code
    //
    //  CoreTextData.m
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import "CoreTextData.h"
    #import "CoreTextImageData.h"
    
    @implementation CoreTextData
    
    //CoreFoundation不支持ARC,需要手动去管理内存的释放
    -(void)setCtFrame:(CTFrameRef)ctFrame{
        if (_ctFrame != ctFrame) {
            if (_ctFrame !=nil) {
                CFRelease(_ctFrame);
            }
        }
        CFRetain(ctFrame);
        _ctFrame = ctFrame;
    }
    
    -(void)dealloc{
        if (_ctFrame != nil) {
            CFRelease(_ctFrame);
            _ctFrame = nil;
        }
    }
    
    -(void)setImageArray:(NSArray *)imageArray{
        _imageArray = imageArray;
        [self fillImagePosition];
        
    }
    //填充图片
    -(void)fillImagePosition{
        if (self.imageArray.count==0) {
            return;
        }
        NSArray *lines = (NSArray *)CTFrameGetLines(self.ctFrame);
        NSInteger lineCount = [lines count];
        CGPoint lineOrigins[lineCount];
        CTFrameGetLineOrigins(self.ctFrame, CFRangeMake(0, 0), lineOrigins);
        
        int imgIndex = 0;
        CoreTextImageData *imageData = self.imageArray[0];
        for (int i=0; i<lineCount; i++) {
            if (imageData==nil) {
                break;
            }
            CTLineRef line = (__bridge CTLineRef)lines[i];
            NSArray *runObjArray = (NSArray *)CTLineGetGlyphRuns(line);
            for (id runObj in runObjArray) {
                CTRunRef run = (__bridge CTRunRef)runObj;
                NSDictionary *runAttributes = (NSDictionary *)CTRunGetAttributes(run);
                CTRunDelegateRef delegate = (__bridge CTRunDelegateRef)[runAttributes valueForKey:(id)kCTRunDelegateAttributeName];
                if (delegate == nil) {
                    continue;
                }
                
                NSDictionary *metaDic = CTRunDelegateGetRefCon(delegate);
                if (![metaDic isKindOfClass:[NSDictionary class]]) {
                    continue;
                }
                
                CGRect runBounds;
                CGFloat ascent;
                CGFloat descent;
                runBounds.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, NULL);
                runBounds.size.height = ascent + descent;
                
                CGFloat x0ffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
                runBounds.origin.x = lineOrigins[i].x + x0ffset;
                runBounds.origin.y = lineOrigins[i].y;
                runBounds.origin.y -= descent;
                
                CGPathRef pathRef = CTFrameGetPath(self.ctFrame);
                CGRect colRect = CGPathGetBoundingBox(pathRef);
                CGRect delegateBounds = CGRectOffset(runBounds, colRect.origin.x, colRect.origin.y);
                
                imageData.imagePostion = delegateBounds;
                imgIndex ++;
                if (imgIndex == self.imageArray.count) {
                    imageData = nil;
                    break;
                }else{
                    imageData = self.imageArray[imgIndex];
                }
            }
        }
    }
    
    @end
    View Code

    CTDisplayView

    //
    //  CTDispalyView.h
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import <UIKit/UIKit.h>
    #import "CoreTextData.h"
    
    @interface CTDispalyView : UIView
    @property(strong,nonatomic)CoreTextData *data;
    @end
    View Code
    //
    //  CTDispalyView.m
    //  CoreTextDemo
    //
    //  Created by 夏远全 on 16/12/25.
    //  Copyright © 2016年 广州市东德网络科技有限公司. All rights reserved.
    //
    
    #import "CTDispalyView.h"
    #import "CoreTextImageData.h"
    #import "CoreTextLinkData.h"
    #import "CoreTextUtils.h"
    
    //导入CoreText系统框架
    #import <CoreText/CoreText.h>
    
    @interface CTDispalyView ()<UIGestureRecognizerDelegate>
    @property (strong,nonatomic)UIImageView *tapImgeView;
    @property (strong,nonatomic)UIView *coverView;
    @property (strong,nonatomic)UIWebView *webView;
    @end
    
    @implementation CTDispalyView
    
    //初始化方法
    -(instancetype)initWithFrame:(CGRect)frame{
        self = [super initWithFrame:frame];
        if (self) {
            [self setupEvents];
        }
        return self;
    }
    
    //添加点击手势
    -(void)setupEvents{
        
        UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(userTapGestureDetected:)];
        tapRecognizer.delegate = self;
        [self addGestureRecognizer:tapRecognizer];
        self.userInteractionEnabled = YES;
    }
    
    
    //增加UITapGestureRecognizer的回调函数
    -(void)userTapGestureDetected:(UITapGestureRecognizer *)recognizer{
        
        CGPoint point = [recognizer locationInView:self];
        
        //点击图片
        for (CoreTextImageData *imagData in self.data.imageArray) {
            
            //翻转坐标系,因为ImageData中的坐标是CoreText的坐标系
            CGRect imageRect = imagData.imagePostion;
            CGPoint imagePosition = imageRect.origin;
            imagePosition.y = self.bounds.size.height - imageRect.origin.y - imageRect.size.height;
            CGRect rect = CGRectMake(imagePosition.x, imagePosition.y, imageRect.size.width, imageRect.size.height);
            
            //检测点击图片的位置Point是否在rect之内
            if (CGRectContainsPoint(rect, point)) {
                
                //在这里处理点击后的逻辑
                [self showTapImage:[UIImage imageNamed:imagData.name]];
                break;
            }
        }
        
        //点击链接
        CoreTextLinkData *linkData = [CoreTextUtils touchLinkInView:self atPoint:point data:self.data];
        if (linkData) {
            [self showTapLink:linkData.url];
            return;
        }
    }
    
    //显示图片
    -(void)showTapImage:(UIImage *)tapImage{
        
        UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
        
        //图片
        _tapImgeView = [[UIImageView alloc] initWithImage:tapImage];
        _tapImgeView.frame = CGRectMake(0, 0, 300, 200);
        _tapImgeView.center = keyWindow.center;
        
        
        //蒙版
        _coverView = [[UIView alloc] initWithFrame:keyWindow.bounds];
        [_coverView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(cancel)]];
        _coverView.backgroundColor = [UIColor colorWithRed:0/255.0 green:0/255.0 blue:0/255.0 alpha:0.6];
        _coverView.userInteractionEnabled = YES;
        
        [keyWindow addSubview:_coverView];
        [keyWindow addSubview:_tapImgeView];
    }
    
    -(void)cancel{
        [_tapImgeView removeFromSuperview];
        [_coverView removeFromSuperview];
    }
    
    //显示链接网页
    -(void)showTapLink:(NSString *)urlStr{
        
        UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
        
        //网页
        _webView = [[UIWebView alloc] initWithFrame:CGRectMake(0, 0, 300, 400)];
        _webView.center = keyWindow.center;
        [_webView setScalesPageToFit:YES];
        NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlStr]];
        [_webView loadRequest:request];
        
        //蒙版
        _coverView = [[UIView alloc] initWithFrame:keyWindow.bounds];
        [_coverView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(hide)]];
        _coverView.backgroundColor = [UIColor colorWithRed:0/255.0 green:0/255.0 blue:0/255.0 alpha:0.6];
        _coverView.userInteractionEnabled = YES;
        
        [keyWindow addSubview:_coverView];
        [keyWindow addSubview:_webView];
    }
    -(void)hide{
        [_webView removeFromSuperview];
        [_coverView removeFromSuperview];
    }
    
    
    //重写drawRect方法
    - (void)drawRect:(CGRect)rect {
        
        [super drawRect:rect];
     
        //1.获取当前绘图上下文
        CGContextRef context = UIGraphicsGetCurrentContext();
        
        //2.旋转坐坐标系(默认和UIKit坐标是相反的)
        CGContextSetTextMatrix(context, CGAffineTransformIdentity);
        CGContextTranslateCTM(context, 0, self.bounds.size.height);
        CGContextScaleCTM(context, 1.0, -1.0);
        
        if (self.data) {
            
            CTFrameDraw(self.data.ctFrame, context);
            for (CoreTextImageData *imageData in self.data.imageArray) {
                
                UIImage *image = [UIImage imageNamed:imageData.name];
                CGContextDrawImage(context, imageData.imagePostion, image.CGImage);
            }
        }
    }
    
    @end
    View Code

    测试截图:

    源码链接https://github.com/xiayuanquan/CoreTextKit.git

    本博文摘自唐巧《iOS开发进阶》,本人花了点时间学习并做了一下整理和改动,希望对学习这方面知识的人有帮助。

  • 相关阅读:
    purple-class2-默认选项切换
    purple-accessData
    “/wechat”应用程序中的服务器错误。
    GDI+ 中发生一般性错误。
    ylbtech-Unitity-CS:Indexers
    ylbtech-Unitity-CS:Hello world
    ylbtech-Unitity-CS:Generics
    ylbtech-Unitity-CS:Delegates
    ZooKeeper目录
    Zookeeper常用命令 (转)
  • 原文地址:https://www.cnblogs.com/XYQ-208910/p/6222931.html
Copyright © 2011-2022 走看看