# iOS进阶 # CoreText使用说明书

1. CoreText框架基础

CoreText是Mac OS和iOS系统中处理文本的low-level API, 不管是使用OC还是swift, 实际我们使用CoreText都还是间接或直接使用C语言在写代码。CoreText是iOS和Mac OS中文本处理的根基, TextKit和WebKit都是构建于其上。

常用类、属性

CTFrameRef

CTFramesetterRef

CTLineRef

CTRunRef

CTTypesetterRef

CTGlyphInfoRef (NSGlyphInfo)

CTParagraphStyleRef (NSParagraphStyle)

CTFontRef (UIFont)

CFArrayRef (NSArray)

分析:

coreText 属于怎样一套API?

字体结构:

当我们进行字体绘制的时候很重要。

image

CTRun、CTFrame、CTLine

image
- CTFrame可以想象成画布, 画布的大小范围由CGPath决定
- CTFrame由很多CTLine组成, CTLine表示为一行
- CTLine由多个CTRun组成, CTRun相当于一行中的多个块, 但是CTRun不需要你自己创建, 由NSAttributedString的属性决定, 系统自动生成。每个CTRun对应不同属性。
- CTFramesetter是一个工厂, 创建CTFrame, 一个界面上可以有多个CTFrame
- CTFrame就是一个基本画布,然后一行一行绘制。 CoreText会自动根据传入的NSAttributedString属性创建CTRun,包括字体样式,颜色,间距等

更多详细的基础知识见末尾参考。

流程

如下图所示,这就是CoreText的基本处理流程:

image

1、创建AttributedString,定义样式

2、通过 CFAttributedStringRef 生成 CTFramesetter

3、通过CTFramesetter得到CTFrame

4、绘制 (CTFrameDraw)

5、如果有图片存在,先在AttributedString 对应位置添加占位符

6、通过回调函数确定图片的宽高(CTRunDelegateCallbacks)

7、遍历到对应CTRun上、获取对应CGRect、绘制图片(CGContextDrawImage)

2. 基本的文本样式实操

CoreText是需要自己处理绘制,不像UILabel等最上层的控件 ,所以我们必须在drawRect中绘制,为了更好地使用,我们稍微封装一下,自定义一个UIView。

我们在使用上层的控件时,坐标系的原点在左上角,而底层的Core Graphics的坐标系原点则是在左下角,以下是一个最基本的绘制示例:

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

    //step 1:获取当前画布的上下文
    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函数建立的对象引用,必须要使用CFRelease掉。
}

效果如下:

img

结果分析:发现文案是反的。原因就是因为coreText的坐标系是和UIKit的坐标系不一样的:

img

如上图,CoreText是基于CoreGraphics的,所以坐标系原点是左下角,我们需要进行翻转。将Y轴从向上转换为向下。

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

翻转后,下面来进行一个最基本的富文本示例:

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数组
let lines = CTFrameGetLines(frame)
// 2.获得行数
let numberOfLines = CFArrayGetCount(lines)
// 3.获得每一行的origin, CoreText的origin是在字形的baseLine处的, 请参考字形图
var lineOrigins = [CGPoint](count: numberOfLines, repeatedValue: CGPointZero)
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &lineOrigins)
// 4.遍历每一行进行绘制
for index in 0..<numberOfLines {
  let origin = lineOrigins[index]
  // 参考: http://swifter.tips/unsafe/
  let line = unsafeBitCast(CFArrayGetValueAtIndex(lines, index), CTLine.self)
  // 设置每一行的位置
  CGContextSetTextPosition(context, origin.x, origin.y)
  // 开始一行的绘制
  CTLineDraw(line, context)
}

按CTRun绘制

用下面函数替换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绘制,但是绘制时双必须要知道此绘制单元的长宽,庆幸的是CoreText绘制的最小单元CTRun提供了CTRunDelegate,也就是当设置了kCTRunDelegateAttributeName过后,CTRun的绘制时所需的参考(长宽等)将可从委托中获取,我们即可通过此方法实现图片的绘制。在需要绘制图片的位置,提前预留空白占位。
CTRun有几个委托用以实现CTRun的几个参数的获取。

以下是CTRunDelegateCallbacks的几个委托代理 。

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

以下是一个最基本的图片绘制原型:遍历查询图片,即查找含有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));//我们也可以传入其它参数
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);//获取第行的起始点
for (int i = 0; i < CFArrayGetCount(lines); i++) {
    CTLineRef line = CFArrayGetValueAtIndex(lines, i);
    CGFloat lineAscent;//上缘线
    CGFloat lineDescent;//下缘线
    CGFloat lineLeading;//行间距
    CTLineGetTypographicBounds(line, &lineAscent, &lineDescent, &lineLeading);//获取此行的字形参数

    //获取此行中每个CTRun
    CFArrayRef runs = CTLineGetGlyphRuns(line);
    for(int j = 0;j< CFArrayGetCount(runs);j++){
        CGFloat runAscent;//此CTRun上缘线
        CGFloat runDescent;//此CTRun下缘线
        CGPoint lineOrigin = lineOrigins[i];//此行起点

        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坐标
        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);
            }
        }
    }
}

代理函数:

#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

基于以上这个原型,我们可以封装一个比较完整的富文本控件,比如定义HTML协议或者JSON,然后在内部进行解析,然后根据类型与相应的属性进行绘制。

4. 图片点击事件

CoreText就是将内容绘制到画布上,自然没有事件处理,我们要实现图片与链接的点击效果就需要使用触摸事件了。当点击的位置在图片的Rect中,那我们做相应的操作即可,所以基本步骤如下:

记录所有图片所在画布中作为一个CTRun的位置 -> 获取每个图片所在画布中所占的Rect矩形区域 -> 当点击事件发生时,判断点击的点是否在某个需要处理的图片Rect内。

这里为了演示的简单,我们直接在drawRect中记录图片的相应坐标,但是一般我们会在CTRichView渲染之前对数据进行相应的处理,比如处理传入的样式数据、记录图片与链接等信息。

用于记录图片信息类

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

//记录图片信息

//以下操作仅仅是演示示例,实战时请在渲染之前处理数据,做到最佳实践。  
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];
    //先判断是否是点击的图片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. 链接点击事件

记录链接信息类

@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];
    //先判断是否是点击的图片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;
            }
        }
    }
}

根据点击点获取字符串偏移

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

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

    //获取每行起点
    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相对于view的坐标转换为相对于该行的坐标
            CGPoint linePoint = CGPointMake(point.x-lineRect.origin.x, point.y-lineRect.origin.y);
            //根据当前行的坐标获取相对整个CoreText串的偏移
            idx = CTLineGetStringIndexForPosition(line, linePoint);
        }
    }
    return idx;
}

6、微博类型富文本实现异步绘制

当我们涉及到图文混排时候的高度计算

http://www.jianshu.com/p/a7f55e456539

YYLable

1、解析@、超链接、图片、表情

2、逐行逐Run异步绘制,

3、点击高亮背景绘制

3、点击效果、事件

参考

iOS富文本

Text Programming Guide for iOS

CoreText基础概念

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容