CoreText与TextKit入门

转眼已经来到2016,昨天刚刚看了罗胖4个小时时间的朋友跨年演讲。感触颇多,2015年时间过得太快,每天都在充斥着新的理念与技术,让人应接不暇。

前段时间抽空回顾了一下CoreText与TextKit相关的内容,期间也重点学习了下YYKit,从中学到不少内容,YYKit框架的代码风格非常好,写得非常干净整洁。与其它的CoreText Engine风格不同,其更加贴近iOS SDK的风格。

接下来还是看看CoreText 与 TextKit的框架吧,其实已经很多人已经写出了很好的博客,这里我总结一下的目的,更多是为了自己以后方便查阅,同时也是对前段时间学习的一个回顾和归纳。

Image

看来TextKit是在CoreText之上构建起来的,但TextKit是在iOS7之后推出来的,应用在UITextView内,使用起来相对简洁,但却没有CoreText灵活。

Core Text

先来看一张图
Image
CTFrame可以理解为一个整体的画布由很多行(CTLine)组成,而每一行又由一个或者多个小方块(CTRun)组成,我们不需要自己创建CTRun,Core Text将根据NSAttributedString的属性来自动创建CTRun。每个CTRun对象对应不同的属性,正因此,你可以自由的控制字体、颜色、字间距等等信息。

CTFramesetter其实就是CTFrame的工厂方法,通过给定的NSAttributedString,生成CTRrame,同时系统自动的创建了CTTypesetter,CTTypesetter就是管理你的字体的类。
这儿有一篇CoreText入门文章,可以帮我们了解这一些。

在使用CoreText的过程中,需要注意如下一些问题:

坐标系

Core Text一开始是供Mac OS使用的,使用数学中原点在左下角的坐标系,所以它在绘制文本的时候都是参照左下角的原点进行绘制的。 所以iOS需要先反转坐标系,代码如下:

    //step 1:
    CGContextRef context = UIGraphicsGetCurrentContext();

    //原点平移至左上角,并翻转Y轴
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    CGContextTranslateCTM(context, 0, self.bounds.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);

图文混排

我们可以在NSMutableAttributedString内插入一个图片相关的NSMutableAttributedString(例如imgAttributedStr),imgAttributedStr中添加kCTRunDelegateAttributeName,以及一个自定义的一个@"imgName",这样在drawRect的时候通过遍历CTLine-->CTRun时,可以找到需要绘制图片的CTRun,这样就可以调用CGContextDrawImage把图片绘制在屏幕上。

    //图片
    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];

响应点击事件

我看到网上的例子是通过添加一个UITapGestureRecognizer,通过点击点CGPoint来查找相应的CTRun,这样就知道点击的具体是什么了,例如链接、图片等。
代码如下:

- (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;
}

TextKit

Text kit是在在iOS7中引入的,相比Core Text,其集成程度更高,可以更加快捷地实现一些简易的文字排版的需求。
先来看一张非常重要的关系图片:
Image

  • 1.Text containers:对应的类NSTextContainer.主要是用于针对哪个区域的文字可以进行排版。一般来说都是矩形区域。它改维护一个数组,该数组定义了一个区域,排版的时候文字不会填充该区域。
  • 2.Layout manager:对应的类NSLayoutManager类。负责对文字进行编辑排版处理--通过将存储在NSTextStorage中的数据转换为可以在视图空间中显示的文本内容,将统一的字符编码映射到对应的字形上,然后将自行排版到NSTextContainer定义的区域中。
  • 3.Text storage:对应着NSTextStorage类。基本存储机制,继承自NSMutableAttributedString,主要用来存储文本的字符和相关属性。当NSTextStorage中的字符或属性发生了改变会通知NSLayoutManager,进而做到文本内容的显示更新。
  • 4.Text View:显示控件的,主要包含UILable、UITextView、UItextField.

如果要改变里面的一些高亮或者排版信息,可以继承NSTextStorageNSLayoutManager,重写里面的replaceCharactersInRange:等相关方法。

如果要识别里面的链接或者电话号码,可以使用NSDataDetector这个类,如果要实现图文混排的话,可以先保留一个预留区域,这个区域不能绘制文本。可以使用textContainer.exclusionPaths这个属性来达到这一目的。

再说到用户交互的话,除了使用UITextView自带的Delegate方法- (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRange:(NSRange)characterRange外,我们也可以像Core Text一样,自己计算点击的内容,如characterIndexForGlyphAtIndex:,
下面截取一下MLLabel里面的代码。

- (MLLink *)linkAtPoint:(CGPoint)location
{
    if (self.links.count<=0||self.text.length == 0||self.textContainer.size.width<=0||self.textContainer.size.height<=0)
    {
        return nil;
    }

    CGPoint textOffset;
    //在执行usedRectForTextContainer之前最好还是执行下glyphRangeForTextContainer relayout
    [self.layoutManager glyphRangeForTextContainer:self.textContainer];
    textOffset = [self textOffsetWithTextSize:[self.layoutManager usedRectForTextContainer:self.textContainer].size];

    //location转换成在textContainer的绘制区域的坐标
    location.x -= textOffset.x;
    location.y -= textOffset.y;

    //获取触摸的字形
    NSUInteger glyphIdx = [self.layoutManager glyphIndexForPoint:location inTextContainer:self.textContainer];

    //apple文档上写有说 如果location的区域没字形,可能返回的是最近的字形index,所以我们再找到这个字形所处于的rect来确认
    CGRect glyphRect = [self.layoutManager boundingRectForGlyphRange:NSMakeRange(glyphIdx, 1)
                                                     inTextContainer:self.textContainer];
    if (!CGRectContainsPoint(glyphRect, location))
        return nil;

    NSUInteger charIndex = [self.layoutManager characterIndexForGlyphAtIndex:glyphIdx];

    //找到了charIndex,然后去寻找是否这个字处于链接内部
    for (MLLink *link in self.links) {
        if (NSLocationInRange(charIndex,link.linkRange)) {
            return link;
        }
    }

    return nil;
}
2016-01-12 15:031391
  • ppp2017-10-07 15:31

    你也关注罗胖