# iOS進(jìn)階 # CoreText使用說明書

1. CoreText框架基礎(chǔ)

CoreText是Mac OS和iOS系統(tǒng)中處理文本的low-level API, 不管是使用OC還是swift, 實(shí)際我們使用CoreText都還是間接或直接使用C語言在寫代碼。CoreText是iOS和Mac OS中文本處理的根基, TextKit和WebKit都是構(gòu)建于其上。

常用類、屬性

CTFrameRef

CTFramesetterRef

CTLineRef

CTRunRef

CTTypesetterRef

CTGlyphInfoRef (NSGlyphInfo)

CTParagraphStyleRef (NSParagraphStyle)

CTFontRef (UIFont)

CFArrayRef (NSArray)

分析:

coreText 屬于怎樣一套API?

字體結(jié)構(gòu):

當(dāng)我們進(jìn)行字體繪制的時(shí)候很重要。

image

CTRun、CTFrame、CTLine

image
- CTFrame可以想象成畫布, 畫布的大小范圍由CGPath決定
- CTFrame由很多CTLine組成, CTLine表示為一行
- CTLine由多個(gè)CTRun組成, CTRun相當(dāng)于一行中的多個(gè)塊, 但是CTRun不需要你自己創(chuàng)建, 由NSAttributedString的屬性決定, 系統(tǒng)自動(dòng)生成。每個(gè)CTRun對(duì)應(yīng)不同屬性。
- CTFramesetter是一個(gè)工廠, 創(chuàng)建CTFrame, 一個(gè)界面上可以有多個(gè)CTFrame
- CTFrame就是一個(gè)基本畫布,然后一行一行繪制。 CoreText會(huì)自動(dòng)根據(jù)傳入的NSAttributedString屬性創(chuàng)建CTRun,包括字體樣式,顏色,間距等

更多詳細(xì)的基礎(chǔ)知識(shí)見末尾參考。

流程

如下圖所示,這就是CoreText的基本處理流程:

image

1、創(chuàng)建AttributedString,定義樣式

2、通過 CFAttributedStringRef 生成 CTFramesetter

3、通過CTFramesetter得到CTFrame

4、繪制 (CTFrameDraw)

5、如果有圖片存在,先在AttributedString 對(duì)應(yīng)位置添加占位符

6、通過回調(diào)函數(shù)確定圖片的寬高(CTRunDelegateCallbacks)

7、遍歷到對(duì)應(yīng)CTRun上、獲取對(duì)應(yīng)CGRect、繪制圖片(CGContextDrawImage)

2. 基本的文本樣式實(shí)操

CoreText是需要自己處理繪制,不像UILabel等最上層的控件 ,所以我們必須在drawRect中繪制,為了更好地使用,我們稍微封裝一下,自定義一個(gè)UIView。

我們?cè)谑褂蒙蠈拥目丶r(shí),坐標(biāo)系的原點(diǎn)在左上角,而底層的Core Graphics的坐標(biāo)系原點(diǎn)則是在左下角,以下是一個(gè)最基本的繪制示例:

- (void)drawRect:(CGRect)rect {
    [super drawRect:rect];

    //step 1:獲取當(dāng)前畫布的上下文
    CGContextRef context = UIGraphicsGetCurrentContext();

    //step 2:
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, self.bounds);

    //step 3:
    NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"xXHhofiyYI這是一段中文,前面是大小寫"];

    //step 4:
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributedString);
    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, [attributedString length]), path, NULL);

    //step 5:
    CTFrameDraw(frame,context);

    //step 6:
    CFRelease(frame);
    CFRelease(path);
    CFRelease(framesetter);
    //使用Create函數(shù)建立的對(duì)象引用,必須要使用CFRelease掉。
}

效果如下:

img

結(jié)果分析:發(fā)現(xiàn)文案是反的。原因就是因?yàn)閏oreText的坐標(biāo)系是和UIKit的坐標(biāo)系不一樣的:

img

如上圖,CoreText是基于CoreGraphics的,所以坐標(biāo)系原點(diǎn)是左下角,我們需要進(jìn)行翻轉(zhuǎn)。將Y軸從向上轉(zhuǎn)換為向下。

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

翻轉(zhuǎn)后,下面來進(jìn)行一個(gè)最基本的富文本示例:

step 4 添加
    [attr addAttribute:(NSString *)kCTBackgroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(0, 10)];
    [attr addAttribute:(NSString *)kCTFontAttributeName value:(__bridge id _Nonnull)(fontRef) range:NSMakeRange(0, 10)];

效果如下:

img

上面的繪制方式是基于CTFrame繪制,還可以按行和按run繪制:

按CTLine繪制

// 1.獲得CTLine數(shù)組
let lines = CTFrameGetLines(frame)
// 2.獲得行數(shù)
let numberOfLines = CFArrayGetCount(lines)
// 3.獲得每一行的origin, CoreText的origin是在字形的baseLine處的, 請(qǐng)參考字形圖
var lineOrigins = [CGPoint](count: numberOfLines, repeatedValue: CGPointZero)
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &lineOrigins)
// 4.遍歷每一行進(jìn)行繪制
for index in 0..<numberOfLines {
  let origin = lineOrigins[index]
  // 參考: http://swifter.tips/unsafe/
  let line = unsafeBitCast(CFArrayGetValueAtIndex(lines, index), CTLine.self)
  // 設(shè)置每一行的位置
  CGContextSetTextPosition(context, origin.x, origin.y)
  // 開始一行的繪制
  CTLineDraw(line, context)
}

按CTRun繪制

用下面函數(shù)替換CTLineDraw(line, context)這一句就可以了, 效果也如上面。

// 畫一行
func drawLine(line: CTLine, context: CGContext) {
   let runs = CTLineGetGlyphRuns(line) as Array
   runs.forEach { run in
       CTRunDraw(run as! CTRun, context, CFRangeMake(0, 0))
       }
   }
}

3. 圖文混排

CoreText本身是不提供UIImage的繪制,所以UIImage肯定只能通過Core Graphics繪制,但是繪制時(shí)雙必須要知道此繪制單元的長(zhǎng)寬,慶幸的是CoreText繪制的最小單元CTRun提供了CTRunDelegate,也就是當(dāng)設(shè)置了kCTRunDelegateAttributeName過后,CTRun的繪制時(shí)所需的參考(長(zhǎng)寬等)將可從委托中獲取,我們即可通過此方法實(shí)現(xiàn)圖片的繪制。在需要繪制圖片的位置,提前預(yù)留空白占位。
CTRun有幾個(gè)委托用以實(shí)現(xiàn)CTRun的幾個(gè)參數(shù)的獲取。

以下是CTRunDelegateCallbacks的幾個(gè)委托代理 。

typedef struct
{
    CFIndex                         version;
    CTRunDelegateDeallocateCallback dealloc;
    CTRunDelegateGetAscentCallback  getAscent;
    CTRunDelegateGetDescentCallback getDescent;
    CTRunDelegateGetWidthCallback   getWidth;
} CTRunDelegateCallbacks;

以下是一個(gè)最基本的圖片繪制原型:遍歷查詢圖片,即查找含有imgName attribute的CTRun,并繪制。

step 4 前面添加:
    
CTRunDelegateCallbacks imageCallBacks;
imageCallBacks.version = kCTRunDelegateCurrentVersion;
imageCallBacks.dealloc = ImgRunDelegateDeallocCallback;
imageCallBacks.getAscent = ImgRunDelegateGetAscentCallback;
imageCallBacks.getDescent = ImgRunDelegateGetDescentCallback;
imageCallBacks.getWidth = ImgRunDelegateGetWidthCallback;

NSString *imgName = @"test.jpg";
CTRunDelegateRef imgRunDelegate = CTRunDelegateCreate(&imageCallBacks, (__bridge void * _Nullable)(imgName));//我們也可以傳入其它參數(shù)
NSMutableAttributedString *imgAttributedStr = [[NSMutableAttributedString alloc]initWithString:@" "];
[imgAttributedStr addAttribute:(NSString *)kCTRunDelegateAttributeName value:(__bridge id)imgRunDelegate range:NSMakeRange(0, 1)];
CFRelease(imgRunDelegate);

#define kImgName @"imgName"
//圖片占位符添加
[imgAttributedStr addAttribute:kImgName value:imgName range:NSMakeRange(0, 1)];
[attributedString insertAttributedString:imgAttributedStr atIndex:30];

step 5 后面添加:

//繪制圖片
CFArrayRef lines = CTFrameGetLines(frame);
CGPoint lineOrigins[CFArrayGetCount(lines)];
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigins);//獲取第行的起始點(diǎn)
for (int i = 0; i < CFArrayGetCount(lines); i++) {
    CTLineRef line = CFArrayGetValueAtIndex(lines, i);
    CGFloat lineAscent;//上緣線
    CGFloat lineDescent;//下緣線
    CGFloat lineLeading;//行間距
    CTLineGetTypographicBounds(line, &lineAscent, &lineDescent, &lineLeading);//獲取此行的字形參數(shù)

    //獲取此行中每個(gè)CTRun
    CFArrayRef runs = CTLineGetGlyphRuns(line);
    for(int j = 0;j< CFArrayGetCount(runs);j++){
        CGFloat runAscent;//此CTRun上緣線
        CGFloat runDescent;//此CTRun下緣線
        CGPoint lineOrigin = lineOrigins[i];//此行起點(diǎn)

        CTRunRef run = CFArrayGetValueAtIndex(runs, j);//獲取此CTRun
        NSDictionary *attributes = (NSDictionary *)CTRunGetAttributes(run);

        CGRect runRect;
        //獲取此CTRun的上緣線,下緣線,并由此獲取CTRun和寬度
        runRect.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &runAscent, &runDescent, NULL);

        //CTRun的X坐標(biāo)
        CGFloat runOrgX = lineOrigin.x + CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL);
        runRect = CGRectMake(runOrgX,lineOrigin.y-runDescent,runRect.size.width,runAscent+runDescent );

        NSString *imgName = [attributes objectForKey:kImgName];
        if (imgName) {
            UIImage *image = [UIImage imageNamed:imgName];
            if(image){
                CGRect imageRect ;
                imageRect.size = image.size;
                imageRect.origin.x = runRect.origin.x + lineOrigin.x;
                imageRect.origin.y = lineOrigin.y;
                CGContextDrawImage(context, imageRect, image.CGImage);
            }
        }
    }
}

代理函數(shù):

#pragma mark - CTRunDelegateCallbacks

void ImgRunDelegateDeallocCallback( void* refCon ){

}

CGFloat ImgRunDelegateGetAscentCallback( void *refCon ){
    NSString *imageName = (__bridge NSString *)refCon;
    return [UIImage imageNamed:imageName].size.height;
}

CGFloat ImgRunDelegateGetDescentCallback(void *refCon){
    return 0;
}

CGFloat ImgRunDelegateGetWidthCallback(void *refCon){
    NSString *imageName = (__bridge NSString *)refCon;
    return [UIImage imageNamed:imageName].size.width;
}

效果如下:

img

基于以上這個(gè)原型,我們可以封裝一個(gè)比較完整的富文本控件,比如定義HTML協(xié)議或者JSON,然后在內(nèi)部進(jìn)行解析,然后根據(jù)類型與相應(yīng)的屬性進(jìn)行繪制。

4. 圖片點(diǎn)擊事件

CoreText就是將內(nèi)容繪制到畫布上,自然沒有事件處理,我們要實(shí)現(xiàn)圖片與鏈接的點(diǎn)擊效果就需要使用觸摸事件了。當(dāng)點(diǎn)擊的位置在圖片的Rect中,那我們做相應(yīng)的操作即可,所以基本步驟如下:

記錄所有圖片所在畫布中作為一個(gè)CTRun的位置 -> 獲取每個(gè)圖片所在畫布中所占的Rect矩形區(qū)域 -> 當(dāng)點(diǎn)擊事件發(fā)生時(shí),判斷點(diǎn)擊的點(diǎn)是否在某個(gè)需要處理的圖片Rect內(nèi)。

這里為了演示的簡(jiǎn)單,我們直接在drawRect中記錄圖片的相應(yīng)坐標(biāo),但是一般我們會(huì)在CTRichView渲染之前對(duì)數(shù)據(jù)進(jìn)行相應(yīng)的處理,比如處理傳入的樣式數(shù)據(jù)、記錄圖片與鏈接等信息。

用于記錄圖片信息類

@interface CTImageData : NSObject
@property (nonatomic,strong) NSString *imgHolder;
@property (nonatomic,strong) NSURL *imgPath;
@property (nonatomic) NSInteger idx;
@property (nonatomic) CGRect imageRect;
@end

//記錄圖片信息

//以下操作僅僅是演示示例,實(shí)戰(zhàn)時(shí)請(qǐng)?jiān)阡秩局疤幚頂?shù)據(jù),做到最佳實(shí)踐。  
if(!_imageDataArray){
    _imageDataArray = [[NSMutableArray alloc]init];
}
BOOL imgExist = NO;
for (CTImageData *ctImageData in _imageDataArray) {
    if (ctImageData.idx == idx) {
        imgExist = YES;
        break;
    }
}
if(!imgExist){
    CTImageData *ctImageData = [[CTImageData alloc]init];
    ctImageData.imgHolder = imgName;
    ctImageData.imageRect = imageRect;
    ctImageData.idx = idx;
    [_imageDataArray addObject:ctImageData];
}

- (void)setupEvents{
    UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(userTapGestureDetected:)];

    [self addGestureRecognizer:tapRecognizer];

    self.userInteractionEnabled = YES;
}

- (void)userTapGestureDetected:(UIGestureRecognizer *)recognizer{
    CGPoint point = [recognizer locationInView:self];
    //先判斷是否是點(diǎn)擊的圖片Rect
    for(CTImageData *imageData in _imageDataArray){
        CGRect imageRect = imageData.imageRect;
        CGFloat imageOriginY = self.bounds.size.height - imageRect.origin.y - imageRect.size.height;
        CGRect rect = CGRectMake(imageRect.origin.x,imageOriginY, imageRect.size.width, imageRect.size.height);
        if(CGRectContainsPoint(rect, point)){
            NSLog(@"tap image handle");
            return;
        }
    }

    //再判斷鏈接
}

5. 鏈接點(diǎn)擊事件

記錄鏈接信息類

@interface CTLinkData : NSObject
@property (nonatomic ,strong) NSString *text;
@property (nonatomic ,strong) NSString *url;
@property (nonatomic ,assign) NSRange range;
@end

記錄鏈接信息

if(!_linkDataArray){
    _linkDataArray = [[NSMutableArray alloc]init];
}
CTLinkData *ctLinkData = [[CTLinkData alloc]init];
ctLinkData.text = [attributedString.string substringWithRange:linkRange];
ctLinkData.url = @"http://www.baidu.com";
ctLinkData.range = linkRange;
[_linkDataArray addObject:ctLinkData];

處理鏈接事件

- (void)userTapGestureDetected:(UIGestureRecognizer *)recognizer{
    CGPoint point = [recognizer locationInView:self];
    //先判斷是否是點(diǎn)擊的圖片Rect
    //......
    //再判斷鏈接
    CFIndex idx = [self touchPointOffset:point];
    if (idx != -1) {
        for(CTLinkData *linkData in _linkDataArray){
            if (NSLocationInRange(idx, linkData.range)) {
                NSLog(@"tap link handle,url:%@",linkData.url);
                break;
            }
        }
    }
}

根據(jù)點(diǎn)擊點(diǎn)獲取字符串偏移

- (CFIndex)touchPointOffset:(CGPoint)point{
    //獲取所有行
    CFArrayRef lines = CTFrameGetLines(_ctFrame);

    if(lines == nil){
        return -1;
    }
    CFIndex count = CFArrayGetCount(lines);

    //獲取每行起點(diǎn)
    CGPoint origins[count];
    CTFrameGetLineOrigins(_ctFrame, CFRangeMake(0, 0), origins);


    //Flip
    CGAffineTransform transform =  CGAffineTransformMakeTranslation(0, self.bounds.size.height);
    transform = CGAffineTransformScale(transform, 1.f, -1.f);

    CFIndex idx = -1;
    for (int i = 0; i< count; i++) {
        CGPoint lineOrigin = origins[i];
        CTLineRef line = CFArrayGetValueAtIndex(lines, i);

        //獲取每一行Rect
        CGFloat ascent = 0.0f;
        CGFloat descent = 0.0f;
        CGFloat leading = 0.0f;
        CGFloat width = (CGFloat)CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
        CGRect lineRect = CGRectMake(lineOrigin.x, lineOrigin.y - descent, width, ascent + descent);

        lineRect = CGRectApplyAffineTransform(lineRect, transform);

        if(CGRectContainsPoint(lineRect,point)){
            //將point相對(duì)于view的坐標(biāo)轉(zhuǎn)換為相對(duì)于該行的坐標(biāo)
            CGPoint linePoint = CGPointMake(point.x-lineRect.origin.x, point.y-lineRect.origin.y);
            //根據(jù)當(dāng)前行的坐標(biāo)獲取相對(duì)整個(gè)CoreText串的偏移
            idx = CTLineGetStringIndexForPosition(line, linePoint);
        }
    }
    return idx;
}

6、微博類型富文本實(shí)現(xiàn)異步繪制

當(dāng)我們涉及到圖文混排時(shí)候的高度計(jì)算

http://www.itdecent.cn/p/a7f55e456539

YYLable

1、解析@、超鏈接、圖片、表情

2、逐行逐Run異步繪制,

3、點(diǎn)擊高亮背景繪制

3、點(diǎn)擊效果、事件

參考

iOS富文本

Text Programming Guide for iOS

CoreText基礎(chǔ)概念

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1、通過CocoaPods安裝項(xiàng)目名稱項(xiàng)目信息 AFNetworking網(wǎng)絡(luò)請(qǐng)求組件 FMDB本地?cái)?shù)據(jù)庫組件 SD...
    陽明AI閱讀 16,203評(píng)論 3 119
  • CoreText是iOS/OSX中文本顯示的一個(gè)底層框架,它是用C語言寫成的,有快速簡(jiǎn)單的優(yōu)勢(shì)。iOS中的Text...
    小貓仔閱讀 5,124評(píng)論 2 9
  • 蘋果文檔 https://developer.apple.com/documentation/coretext C...
    陽明AI閱讀 524評(píng)論 0 4
  • 系列文章: CoreText實(shí)現(xiàn)圖文混排 CoreText實(shí)現(xiàn)圖文混排之點(diǎn)擊事件 CoreText實(shí)現(xiàn)圖文混排之文...
    老司機(jī)Wicky閱讀 40,615評(píng)論 221 432
  • 日本有過一個(gè)調(diào)查,你覺得十年后自己會(huì)是什么樣?這個(gè)社會(huì)會(huì)是什么樣?超過70%的被采訪者認(rèn)為十年后的自己會(huì)比現(xiàn)在好,...
    三體合一閱讀 375評(píng)論 0 1

友情鏈接更多精彩內(nèi)容