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開發進階》,本人花了點時間學習並做了一下整理和改動,希望對學習這方面知識的人有幫助。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM