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详解 --> //www.greatytc.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 = "<UICTFont: 0x7f9e3622b320> font-family: \".SFUI-Regular\"; font-weight: normal; font-style: normal; font-size: 12.00pt";
}{
    CTRunDelegate = "<CTRunDelegate 0x600003aca6a0 [0x7fff8002e7f0]>";
}一个富文本{
}

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

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<UITouch *> *)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<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.touchHightlight = nil;
    [self setNeedsDisplay];
}

// 点击取消
- (void)touchesCancelled:(NSSet<UITouch *> *)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实现图文排布

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

推荐阅读更多精彩内容

  • 今天感恩节哎,感谢一直在我身边的亲朋好友。感恩相遇!感恩不离不弃。 中午开了第一次的党会,身份的转变要...
    迷月闪星情阅读 10,543评论 0 11
  • 彩排完,天已黑
    刘凯书法阅读 4,175评论 1 3
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 123,619评论 2 7