CoreText实战讲解,手把手教你实现图文、点击高亮、自定义截断功能

1、CoreText基础知识

CoreText 框架中常用的几个类CTFrame、CTFramesetter、CTLine、CTRun、CTRunDelegateRef、CTFont。

各个类之间的关系如下图所示:其中,CTRun可以理解成相同属性的一个或几个字符的集合。

image.jpeg
image.jpeg

CTFrame 作为一个整体的画布(Canvas),其中由行(CTLine)组成,而每行可以分为一个或多个小方块(CTRun)。 注意:你不需要自己创建CTRun,Core Text将根据NSAttributedString的属性来自动创建CTRun。每个CTRun对象对应不同的属性,正因此,你可以自由的控制字体、颜色、字间距等等信息。 通常处理步聚: 1.使用core text就是先有一个要显示的string,然后定义这个string每个部分的样式->attributedString -> 生成 CTFramesetter -> 得到CTFrame -> 绘制(CTFrameDraw) 其中可以更详细的设置换行方式,对齐方式,绘制区域的大小等。 2.绘制只是显示,点击事件就需要一个判断了。 CTFrame 包含了多个CTLine,并且可以得到各个line的其实位置与大小。判断点击处在不在某个line上。CTLine 又可以判断这个点(相对于ctline的坐标)处的文字范围。然后遍历这个string的所有NSTextCheckingResult,根据result的rang判断点击处在不在这个rang上,从而得到点击的链接与位置。

image.jpeg
image.jpeg

我们主要关注Ascent(上行高度)、Descent(下行高度)、Baseline(基线)、Origin(原点)。

2、使用CoreText绘制文本

接下来我们来实际操练一下,使用CoreText知识绘制一段简单的富文本。

- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    // y坐标轴反转
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    CGContextTranslateCTM(context, 0, self.bounds.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);
    // 文字描述
    NSMutableAttributedString * attributeStr = [[NSMutableAttributedString alloc] initWithString:@"我是一个富文本"];
    [attributeStr addAttributes:@{NSFontAttributeName : [UIFont systemFontOfSize:12], NSForegroundColorAttributeName: [UIColor redColor]} range:NSMakeRange(0, 2)];
    // CTFramesetter
    CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributeStr);
    // 绘制路径,也可以理解成绘制区域
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, self.bounds);
    // CTFrame
    NSInteger length = attributeStr.length;
    CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, length), path, NULL);
    // 绘制
    CTFrameDraw(frame, context);
    
    CFRelease(frame);
    CFRelease(path);
    CFRelease(frameSetter);
}

运行项目可以看到一段文字,当然我们还可以使用一个快捷方式绘制一段文本,如下:这个方法是要有显示的View,并且要能获取到图形绘制上下文对象的。因此也是要卸载drawRect:中

- (void)drawRect:(CGRect)rect {
    [@"我是一段文本" drawInRect:rect withAttributes:@{NSFontAttributeName : [UIFont systemFontOfSize:15], NSForegroundColorAttributeName : [UIColor redColor]}];
}

3、实现图文排布

我们先来看下代码:

- (void)drawRect:(CGRect)rect {
    // Drawing code
    
    CGContextRef context = UIGraphicsGetCurrentContext();

    // 仿射变换CGAffineTransform详解 --> https://www.jianshu.com/p/6c09d138b31d
    // 先把CoreText坐标系设置为原位,相当于设置的平移旋转都无效。这里不写这句也没事
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    // 平移变换,由于默认原点在左下角,x轴正向向右,y轴正向向上,这句代码相当于把x轴上移了self.bounds.size.height的高度
    CGContextTranslateCTM(context, 0, self.bounds.size.height);
    // 缩放,这里将y轴反转了
    CGContextScaleCTM(context, 1.0, -1.0);

    NSMutableAttributedString * attributeStr = [[NSMutableAttributedString alloc] initWithString:@"我是一个富文本"];
    [attributeStr addAttributes:@{NSFontAttributeName : [UIFont systemFontOfSize:12], NSForegroundColorAttributeName: [UIColor redColor]} range:NSMakeRange(0, 2)];
    
    // 占位符代理回调
    CTRunDelegateCallbacks callBacks;
    memset(&callBacks,0,sizeof(CTRunDelegateCallbacks));
    callBacks.version = kCTRunDelegateCurrentVersion;
    callBacks.getAscent = ascentCallBacks;
    callBacks.getDescent = descentCallBacks;
    callBacks.getWidth = widthCallBacks;
    
    NSDictionary *info = @{@"w":@130, @"h":@100};
    // 占位符代理
    CTRunDelegateRef delegate = CTRunDelegateCreate(& callBacks, (__bridge void *)info);
    // https://www.sojson.com/hexadecimal.html
    // 这里其实就是一个空字符串
    unichar placeHolder = 0xFFFC;
    NSString * placeHolderStr = [NSString stringWithCharacters:&placeHolder length:1];
    NSMutableAttributedString * placeHolderAttrStr = [[NSMutableAttributedString alloc] initWithString:placeHolderStr];
    CFAttributedStringSetAttribute((CFMutableAttributedStringRef)placeHolderAttrStr, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);
    CFRelease(delegate);

    [attributeStr insertAttributedString:placeHolderAttrStr atIndex:2];
    
    CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributeStr);
    
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, self.bounds);
    
    NSInteger length = attributeStr.length;
    CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, length), path, NULL);
    
    CTFrameDraw(frame, context);
    
    // 绘制图片
    CFArrayRef lines = CTFrameGetLines(frame);
    CFIndex lineCount = CFArrayGetCount(lines);
    CGPoint origins[lineCount];
    CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins);
    for (int i = 0; i < lineCount; i++) {
        CTLineRef lineRef = CFArrayGetValueAtIndex(lines, i);
        CFArrayRef runs = CTLineGetGlyphRuns(lineRef);
        CFIndex runCount = CFArrayGetCount(runs);
        for (int j = 0; j < runCount; j++) {
            CTRunRef runRef = CFArrayGetValueAtIndex(runs, j);
            NSDictionary *attributes = (id)CTRunGetAttributes(runRef);
            if (!attributes) {
                continue;
            }
            // 附件
            CTRunDelegateRef delegateRef = (__bridge CTRunDelegateRef)[attributes valueForKey:(id)kCTRunDelegateAttributeName];
            if (delegateRef) {
                CGPoint origin = origins[i];
                id info = (id)CTRunDelegateGetRefCon(delegateRef);
                if ([info isKindOfClass:[NSDictionary class]]) {
                    CGFloat w = [info[@"w"] doubleValue];
                    CGFloat h = [info[@"h"] doubleValue];
                    CGFloat offset_x = CTLineGetOffsetForStringIndex(lineRef, CTRunGetStringRange(runRef).location, NULL);
                    CGContextDrawImage(context, CGRectMake(origin.x + offset_x, origin.y, w, h), [UIImage imageNamed:@"mababa"].CGImage);
                }
            }
        }
    }
    
    CFRelease(frame);
    CFRelease(path);
    CFRelease(frameSetter);
}

static CGFloat ascentCallBacks(void * ref)
{
    return [(NSNumber *)[(__bridge NSDictionary *)ref valueForKey:@"h"] floatValue];
}
static CGFloat descentCallBacks(void * ref)
{
    return 0;
}
static CGFloat widthCallBacks(void * ref)
{
    return [(NSNumber *)[(__bridge NSDictionary *)ref valueForKey:@"w"] floatValue];
}

【图文】效果图:

image.jpeg

代码比较长,接下来我一段段解读。

富文本占位符的设置

CTRunDelegateCallbacks callBacks;
    memset(&callBacks,0,sizeof(CTRunDelegateCallbacks));
    callBacks.version = kCTRunDelegateCurrentVersion;
    callBacks.getAscent = ascentCallBacks;
    callBacks.getDescent = descentCallBacks;
    callBacks.getWidth = widthCallBacks;
    
    NSDictionary *info = @{@"w":@130, @"h":@100};
    
    CTRunDelegateRef delegate = CTRunDelegateCreate(& callBacks, (__bridge void *)info);
    // https://www.sojson.com/hexadecimal.html
    // 这里其实就是一个空字符串
    unichar placeHolder = 0xFFFC;
    NSString * placeHolderStr = [NSString stringWithCharacters:&placeHolder length:1];
    NSMutableAttributedString * placeHolderAttrStr = [[NSMutableAttributedString alloc] initWithString:placeHolderStr];
    CFAttributedStringSetAttribute((CFMutableAttributedStringRef)placeHolderAttrStr, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate);
    CFRelease(delegate);

    [attributeStr insertAttributedString:placeHolderAttrStr atIndex:2];

图文排布其实就是在一段文字中插入一个占位符CTRunDelegateRef,这个占位符的时候和NSAttributeString的其他属性使用是一样的,通过设置AttributeString的kCTRunDelegateAttributeName来设置进去。而CTRunDelegateRef需要传入一个callbacks,用于告诉Coretext框架我们需要的这个占位符上行高度(ascent),下行高度(descent)以及宽度是多少。其中ascent和descent就是控制图片垂直排布的关键,关键代码如下所示:

_width = self.size.width;
    CGFloat height = self.size.height;
    if (self.verticalTextAlignment == FFVerticalTextAlignmentTop) { // 与文字顶部对齐
        _ascent = self.font.ascender;
        _descent = height - self.font.ascender;
    } else if (self.verticalTextAlignment == FFVerticalTextAlignmentCenter) { // 与文字垂直居中对齐
        CGFloat fontHeight = self.font.ascender - self.font.descender;
        CGFloat yOffset = self.font.ascender - fontHeight * 0.5;
        _ascent = height * 0.5 + yOffset;
        _descent = height - _ascent;
    } else { // 与文字底部对齐
        _ascent = height;
        _descent = 0;
    }

这个占位符是由AttributeString表示的,需要传入一个字符串,文中的写法是传入了一个十六进制的0xFFFC,其实通过打印placeHolderStr可以知道,他就是一个空字符串,但是我们在初始化的时候设置了他的长度为1,也就是一个length=1的空字符串。其实我们传入一个" "也是完全没问题的。

unichar placeHolder = 0xFFFC;
NSString * placeHolderStr = [NSString stringWithCharacters:&placeHolder length:1];

通过这个带有占位符的Attributestring创建的CTFrameRef,我们通过便利它的CTLineRef,再遍历每个CTLineRef中的CTRunRef,就能获取到那个占位符。

不理解的可以往上翻看下CTFrameRef、CTLineRef、CTRunRef之间的关系图。

打印这个AttributeString可以清楚的看到文本的run是怎么分布的。

2021-04-14 18:01:09.107880+0800 FFText-oc[31622:331009] --->

我是{
    NSColor = "UIExtendedSRGBColorSpace 1 0 0 1";
    NSFont = " font-family: \".SFUI-Regular\"; font-weight: normal; font-style: normal; font-size: 12.00pt";
}{
    CTRunDelegate = "";
}一个富文本{
}

这里提一下获取每行原点的函数。

CGPoint origins[lineCount]; 
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins);

这个函数就是获取每行原点的方法,这里的原点和我们平时布局的原点是不一样的,在CoreText中,这个origin是和baseline在同一水平线上,origin上部分是ascent,也就是所谓的上行高度,下面是descent,也就是下行高度。而不同的文字在上行高度和下行高度上是不一样的,他们有各自的排版显示。我上面的例子中,图片显示正确正是因为我们设置了占位符的ascent为图片的高度,descent为0,因此origin就在和文字底部对齐的地方,所以才能显示正常。如果需要实现图片在文字中的垂直布局,那么需要注意修正这个origin。

image.jpeg
CGFloat w = [info[@"w"] doubleValue];
                    CGFloat h = [info[@"h"] doubleValue];
                    CGFloat offset_x = CTLineGetOffsetForStringIndex(lineRef, CTRunGetStringRange(runRef).location, NULL);
                    CGContextDrawImage(context, CGRectMake(origin.x + offset_x, origin.y, w, h), [UIImage imageNamed:@"mababa"].CGImage);

一旦我们取到delegate和图片的rect后,直接通过函数CGContextDrawImage绘制图片即可。这样简单的图文排布就实现了。

4、在文本中插入自定义视图

插入自定义视图原理和插入上一段插入图片的原理是一样的,我们只需要修改下最后绘图的代码实现:

UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
[button setBackgroundColor:[UIColor redColor]];
[button setTitle:@"我是按钮" forState:UIControlStateNormal];
button.frame = CGRectMake(origin.x + offset_x, rect.size.height - origin.y - h, w, h);
[self addSubview:button];

(自定义视图)效果图:

image.jpeg

5、链接、点击高亮

实现点击高亮的关键就是得到点击的文字区域的point和设置点击高亮的文字的rect是否在同一块区域内。

先看下代码实现:

- (void)drawRect:(CGRect)rect {
    // Drawing code
    
    CGContextRef context = UIGraphicsGetCurrentContext();

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

    NSMutableAttributedString * attributeStr = [[NSMutableAttributedString alloc] initWithString:@"我是一个富文本富文本富文本富文本富文本富文本"];
    [attributeStr addAttributes:@{NSFontAttributeName : [UIFont systemFontOfSize:12], NSForegroundColorAttributeName: [UIColor redColor]} range:NSMakeRange(0, 2)];
    
    // 设置点击高亮的文本
    HightlightAction *action = [HightlightAction new];
    action.backgroundColor = [UIColor lightGrayColor];
    action.range = NSMakeRange(4, 10);
    [attributeStr addAttribute:kHighlightAttributeName value:action range:action.range];
    // 点击能高亮的区域设置下划线
    [attributeStr addAttribute:NSUnderlineStyleAttributeName value:@1 range:action.range];
    
    // 如果发现当前正点击在高亮区域上,那么在这段文字下设置背景色
    if (self.touchHightlight) {
        [attributeStr addAttribute:NSBackgroundColorAttributeName value:self.touchHightlight.backgroundColor range:self.touchHightlight.range];
    }
    
    CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributeStr);
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, self.bounds);
    NSInteger length = attributeStr.length;
    CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, length), path, NULL);
    
    
    CFArrayRef lines = CTFrameGetLines(frame);
    CFIndex lineCount = CFArrayGetCount(lines);
    CGPoint origins[lineCount];
    CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins);
    for (int i = 0; i < lineCount; i++) {
        CTLineRef lineRef = CFArrayGetValueAtIndex(lines, i);
        CFArrayRef runs = CTLineGetGlyphRuns(lineRef);
        CFIndex runCount = CFArrayGetCount(runs);
        for (int j = 0; j < runCount; j++) {
            CTRunRef runRef = CFArrayGetValueAtIndex(runs, j);
            NSDictionary *attributes = (id)CTRunGetAttributes(runRef);
            if (!attributes) {
                continue;
            }
            
            // 取出run中的高亮模型,计算它的rect
            HightlightAction *action = attributes[kHighlightAttributeName];
            if (!action) {
                continue;
            }
            
            CGPoint origin = origins[i];
            CGFloat offset_x = CTLineGetOffsetForStringIndex(lineRef, CTRunGetStringRange(runRef).location, NULL);
            CGFloat width = CTRunGetTypographicBounds(runRef, CFRangeMake(0, 0), NULL, NULL, NULL);
            
            CGFloat ascent; // 上行高
            CGFloat descent; // 下行高
            CGFloat leading; // 行间距
            CTLineGetTypographicBounds(lineRef, &ascent, &descent, &leading);
            
            // 字高
            CGFloat glyphHeight = ascent + ABS(descent);
            CGFloat lineHeight = glyphHeight + leading;
            
            // 需要高亮的区域
            CGRect hightlightRect = CGRectMake(origin.x + offset_x, rect.size.height - origin.y - lineHeight, width, lineHeight);
            action.hightlightRect = hightlightRect;
            self.action = action;
        }
    }
    
    CTFrameDraw(frame, context);
    CFRelease(frame);
    CFRelease(path);
    CFRelease(frameSetter);
}

// 开始点击
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = touches.anyObject;
    CGPoint p = [touch locationInView:touch.view];
    if (CGRectContainsPoint(self.action.hightlightRect, p)) {
        // 保存当前选中的高亮模型
        self.touchHightlight = self.action;
        [self setNeedsDisplay];
    }
}

// 点击结束
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    self.touchHightlight = nil;
    [self setNeedsDisplay];
}

// 点击取消
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    self.touchHightlight = nil;
    [self setNeedsDisplay];
}

代码运行后,可以看到已经实现基本的点击文字高亮效果了。

image.jpeg

可以看到,实现原理其实就是通过找到line中所有run里面有对应高亮标识的run。然后获取它的rect就可以了。

CGPoint origin = origins[i];
            CGFloat offset_x = CTLineGetOffsetForStringIndex(lineRef, CTRunGetStringRange(runRef).location, NULL);
            CGFloat width = CTRunGetTypographicBounds(runRef, CFRangeMake(0, 0), NULL, NULL, NULL);
            
            CGFloat ascent; // 上行高
            CGFloat descent; // 下行高
            CGFloat leading; // 行间距
            CTLineGetTypographicBounds(lineRef, &ascent, &descent, &leading);
            
            // 字高
            CGFloat glyphHeight = ascent + ABS(descent);
            CGFloat lineHeight = glyphHeight + leading;
            
            // 需要高亮的区域
            CGRect hightlightRect = CGRectMake(origin.x + offset_x, rect.size.height - origin.y - lineHeight, width, lineHeight);

上面这段文字,我们打印这一行所有的runs,可以知道run的数量是4个,分别是“我是”、“一个”、“富文本富文本富文本富”、“文本富文本富文本”。每个run中的文字都是相同的attribute属性。那么思考下,如果我们在上诉的高亮区域中间插入了一个不同属性的文字呢。

来试着加入下面这段代码,然后再运行上面的代码看点击高亮是否还运行正常。

NSMutableAttributedString * attributeStr = [[NSMutableAttributedString alloc] initWithString:@"我是一个富文本富文本富文本富文本富文本富文本"];[attributeStr addAttributes:@{NSFontAttributeName : [UIFont systemFontOfSize:12], NSForegroundColorAttributeName: [UIColor redColor]} range:NSMakeRange(0, 2)];
// 让第六个字符字体变大
[attributeStr addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:20] range:NSMakeRange(13, 1)];
// 剩下代码
...

运行后会发现点击失效了!这是怎么回事?

其实不没有失效,点击最后一个大号字体的富还是能高亮的。

image.jpeg

那么造成这种现象的原因是什么呢,我们不妨先打印下计算得到的hightlightRect,发现宽度只有20了,并且x也不对。

(origin = (x = 156.50400000000002, y = -2.8800000000000026), size = (width = 20.379999999999999, height = 21.880000000000003))

然后再数一下此时一共有几个run了,发现是5个,我们新设置的font属性把高亮属性给截断了,导致for循环中取到了两次包含kHighlightAttributeName的run,第二次就是那个大字号的“富”。所以导致最终计算得到的宽度为这个富字的宽度,并且x为第二个run的x坐标,点击其他文字没出现高亮的效果。

解决办法也简单,就是累加这两端run的宽度即可,x的值用第一个run的x坐标。

CGFloat hightlightX = 0;
    CGFloat hightlightWidth = 0;
    int effectRunCount = 0;
    
    for (int i = 0; i < lineCount; i++) {
        CTLineRef lineRef = CFArrayGetValueAtIndex(lines, i);
        CFArrayRef runs = CTLineGetGlyphRuns(lineRef);
        CFIndex runCount = CFArrayGetCount(runs);
        NSLog(@"runcount ---> %ld",runCount);
        for (int j = 0; j < runCount; j++) {
            CTRunRef runRef = CFArrayGetValueAtIndex(runs, j);
            NSDictionary *attributes = (id)CTRunGetAttributes(runRef);
            if (!attributes) {
                continue;
            }
            
            HightlightAction *action = attributes[kHighlightAttributeName];
            if (!action) {
                
                hightlightWidth = 0;
                
                continue;
            }
            
            effectRunCount += 1;
            
            CGPoint origin = origins[i];
            CGFloat offset_x = CTLineGetOffsetForStringIndex(lineRef, CTRunGetStringRange(runRef).location, NULL);
            CGFloat width = CTRunGetTypographicBounds(runRef, CFRangeMake(0, 0), NULL, NULL, NULL);
            
            if (effectRunCount == 1) {
                hightlightX = origin.x + offset_x;
            }
            
            hightlightWidth += width;
            
            CGFloat ascent; // 上行高
            CGFloat descent; // 下行高
            CGFloat leading; // 行间距
            CTLineGetTypographicBounds(lineRef, &ascent, &descent, &leading);
            
            // 字高
            CGFloat glyphHeight = ascent + ABS(descent);
            CGFloat lineHeight = glyphHeight + leading;
            
            // 需要高亮的区域
            CGRect hightlightRect = CGRectMake(hightlightX, rect.size.height - origin.y - lineHeight, hightlightWidth, lineHeight);
            action.hightlightRect = hightlightRect;
            self.action = action;
        }
    }

这样修改后就可以正常显示高亮效果了。

image.jpeg

6、自定义截断符

要实现自定义截断符,我们需要转换思路,仔细看上面的几段代码,绘制最后都是调用函数CTFrameDraw来实现的。但要实现类似于文字结尾加...或者说自定义的其他东西,那么需要我们按行来绘制,通过调用函数CTLineDraw。

还是先来看下代码。

- (void)drawRect:(CGRect)rect {
    // Drawing code
    
    CGContextRef context = UIGraphicsGetCurrentContext();

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

    NSMutableAttributedString * attributeStr = [[NSMutableAttributedString alloc] initWithString:@"我是一个富文本我是一个富文本我是一个富文本我是一个富文本我是一个富文本我是一个富文本我是一个富文本我是一个富文本我是一个富文本我是一个富文本我是一个富文本我是一个富文本我是一个富文本我是一个富文本我是一个富文本我是一个富文本我是一个富文本我是一个富文本我是一个富文本"];
    [attributeStr addAttributes:@{NSFontAttributeName : [UIFont systemFontOfSize:12], NSForegroundColorAttributeName: [UIColor redColor]} range:NSMakeRange(0, 2)];
    CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributeStr);
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, self.bounds);
    NSInteger length = attributeStr.length;
    CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, length), path, NULL);

    CFArrayRef lines = CTFrameGetLines(frame);
    CFIndex lineCount = CFArrayGetCount(lines);
    CGPoint origins[lineCount];
    CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins);
    
    // 限制展示1行
    NSInteger numberOfLines = 1;
    
    NSInteger count = numberOfLines == 0 ? lineCount : numberOfLines;
    if (count > lineCount) count = lineCount;
    // 判断是否需要展示截断符,小于总行数都需要展示截断符。
    BOOL needShowTruncation = count < lineCount;
    
    for (int i = 0; i < count; i++) {
        CTLineRef lineRef = CFArrayGetValueAtIndex(lines, i);
        CFRange range = CTLineGetStringRange(lineRef);
        CGPoint origin = origins[i];
        // 一旦我们通过CTLineDraw绘制文字后,那么需要我们自己来设置行的位置,否则都位于最底下显示。
        CGContextSetTextPosition(context, origin.x, origin.y);
        
        if (i == 0 && needShowTruncation) {
            NSMutableAttributedString *drawLineString = [[NSMutableAttributedString alloc] initWithAttributedString:[attributeStr attributedSubstringFromRange:NSMakeRange(range.location, range.length)]];
            
            NSAttributedString *truncationTokenText = [[NSAttributedString alloc] initWithString:@"点我展开" attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:15], NSForegroundColorAttributeName: [UIColor brownColor]}];
            // 这两行可以控制截断符的位置是在开头,还是在中间,还是在结尾
            CTLineTruncationType type = kCTLineTruncationEnd;
            [drawLineString appendAttributedString:truncationTokenText];
            
            CTLineRef drawLineRef = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)drawLineString);
            CTLineRef tokenLineRef = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)truncationTokenText);
            // 创建带有截断符的行
            CTLineRef truncationLineRef = CTLineCreateTruncatedLine(drawLineRef, rect.size.width, type, tokenLineRef);
            CFRelease(drawLineRef);
            CFRelease(tokenLineRef);
            // 绘制
            CTLineDraw(truncationLineRef, context);
        } else {
            CTLineDraw(lineRef, context);
        }
    }
    
    CFRelease(frame);
    CFRelease(path);
    CFRelease(frameSetter);
    
}

运行效果如下。

image.jpeg

我们还可以实现类似点击“更多”展开所有文字的效果,实现逻辑和文字点击高亮的原理是一致的,这里就不展开说明了。

7、拓展

  • 异步渲染

  • 长按选择文本

  • 模糊约束


感兴趣的可以看下我对Coretext的封装实现 FFText

参考博文:

Core Text专题

CoreText入门-进阶

CoreText系统框架

排版的概念

CoreText基本用法

CoreText实现图文排布

你可能感兴趣的:(CoreText实战讲解,手把手教你实现图文、点击高亮、自定义截断功能)