CoreText进阶(四)-文字行数限制和显示更多

本文的主要内容是使用CoreText如何进行行数的限制,以及设置了行数限制末尾的内容被截断了怎么设置截断的标识。此外,还有如何设置自定义的截断标识字符串(比如“显示更多”)、设置自定义截断标识字符串的点击事件等的相关讨论

其它文章:
CoreText入门(一)-文本绘制
CoreText入门(二)-绘制图片
CoreText进阶(三)-事件处理
CoreText进阶(四)-文字行数限制和显示更多
CoreText进阶(五)- 文字排版样式和效果
CoreText进阶(六)-内容大小计算和自动布局
CoreText进阶(七)-添加自定义View和对其

用例和效果

Demo:CoreTextDemo

效果图:

默认的截断标识和自定义的截断标识符效果图

默认的截断标识和自定义的截断标识符效果图

点击查看更多之后的效果图

点击查看更多之后的效果图

为了可以设置显示的行数以及截断的标识字符串,YTDrawView类提供了三个属性,外部可以通过设置参数的方式来设置行数和截断的标识字符串,并且可以设置点击事件

@property (nonatomic, assign) NSInteger numberOfLines; ///< 行数
@property (nonatomic, strong) NSAttributedString *truncationToken;///<截断的标识字符串,默认是"..."
@property (nonatomic, copy) ClickActionHandler truncationActionHandler;///<截断的标识字符串点击事件

使用的示例代码:

    CGRect frame = CGRectMake(0, 100, self.view.bounds.size.width, 100);
    YTDrawView *textDrawView = [[YTDrawView alloc] initWithFrame:frame];
    textDrawView.backgroundColor = [UIColor whiteColor];
    textDrawView.numberOfLines = 3;
    [textDrawView addString:@"这是一个最好的时代,也是一个最坏的时代;这是明智的时代,这是愚昧的时代;这是信任的纪元,这是怀疑的纪元;这是光明的季节,这是黑暗的季节;这是希望的春日,这是失望的冬日;我们面前应有尽有,我们面前一无所有;我们都将直上天堂,我们都将直下地狱。" attributes:self.defaultTextAttributes clickActionHandler:^(id obj) {
    }];
    [self.view addSubview:textDrawView];
    
    NSAttributedString * truncationToken = [[NSAttributedString alloc] initWithString:@"查看更多" attributes:[self truncationTextAttributes]];
    frame = CGRectMake(0, 220, self.view.bounds.size.width, 100);
    textDrawView = [[YTDrawView alloc] initWithFrame:frame];
    textDrawView.backgroundColor = [UIColor whiteColor];
    textDrawView.numberOfLines = 2;
    textDrawView.truncationToken = truncationToken;
    [textDrawView addString:@"这是一个最好的时代,也是一个最坏的时代;这是明智的时代,这是愚昧的时代;这是信任的纪元,这是怀疑的纪元;这是光明的季节,这是黑暗的季节;这是希望的春日,这是失望的冬日;我们面前应有尽有,我们面前一无所有;我们都将直上天堂,我们都将直下地狱。" attributes:self.defaultTextAttributes clickActionHandler:^(id obj) {
    }];
    __weak typeof(textDrawView) weakDrawView = textDrawView;
    textDrawView.truncationActionHandler = ^(id obj) {
        NSLog(@"点击查看更多");
        weakDrawView.numberOfLines = 0;
    };
    [self.view addSubview:textDrawView];

分析

步骤分析

主要的有以下几个步骤:

  • 判断有没有设置行数限制,没有使用默认绘制(CTFrameDraw)即可,有行数限制继续下一步
  • 非最后一行直接绘制即可(使用CTLineDraw,并且需要使用CGContextSetTextPosition方法设置绘制文本的位置)
  • 判断最后一行的显示是否会超出超出,超出执行下一步
  • 把最后一行的显示内容截取,留出显示“...”(这个内容可以自定义为比如上面的“显示更多”)的位置,然后把“...”拼接在被截取的原始内容之后
  • 使用CTLineCreateTruncatedLine创建最后一行显示的内容,返回CTLine对象
  • 如果有设置了truncationActionHandler截断的标识字符串点击事件,需要把位置信息进行保存,用于后面的事件处理

涉及到的API

  • CGContextSetTextPosition在使用CTLineDraw绘制一个CTline之前需要设置CTline绘制的位置,这个值可以使用CTFrameGetLineOrigins方法来获取
  • CTLineDrawCTFrameDraw类似,不过是以行为单位进行绘制,灵活性更高,在有行数显示需要添加特殊的截断标识的场景需要使用这个方法才能满足要求
  • CTLineCreateTruncatedLine创建一个带有特殊截断标识的CTLine对象

实现

截断标识行的实现

数据的处理依然放在YTRichContentData类中进行,calculateTruncatedLinesWithBounds就是以上分析的步骤的代码实现,关键的步骤在代码中都有注释

- (void)calculateTruncatedLinesWithBounds:(CGRect)bounds {
    
    // 清除旧的数据
    [self.truncations removeAllObjects];
    
    // 获取最终需要绘制的文本行数
    CFIndex numberOfLinesToDraw = [self numberOfLinesToDrawWithCTFrame:self.ctFrame];
    if (numberOfLinesToDraw <= 0) {
        self.drawMode = YTDrawModeFrame;
    } else {
        self.drawMode = YTDrawModeLines;
        NSArray *lines = (NSArray *)CTFrameGetLines(self.ctFrame);
        
        CGPoint lineOrigins[numberOfLinesToDraw];
        CTFrameGetLineOrigins(self.ctFrame, CFRangeMake(0, numberOfLinesToDraw), lineOrigins);
        
        for (int lineIndex = 0; lineIndex < numberOfLinesToDraw; lineIndex ++) {
 
            CTLineRef line = (__bridge CTLineRef)(lines[lineIndex]);
            CFRange range = CTLineGetStringRange(line);
            // 判断最后一行是否需要显示【截断标识字符串(...)】
            if ( lineIndex == numberOfLinesToDraw - 1
                && range.location + range.length < [self attributeStringToDraw].length) {
                
                // 创建【截断标识字符串(...)】
                NSAttributedString *tokenString = nil;
                if (_truncationToken) {
                    tokenString = _truncationToken;
                } else {
                    NSUInteger truncationAttributePosition = range.location + range.length - 1;
                    
                    NSDictionary *tokenAttributes = [[self attributeStringToDraw] attributesAtIndex:truncationAttributePosition
                                                                                          effectiveRange:NULL];
                    tokenString = [[NSAttributedString alloc] initWithString:@"\u2026" attributes:tokenAttributes];
                }
                
                // 计算【截断标识字符串(...)】的长度
                CGSize tokenSize = [tokenString boundingRectWithSize:CGSizeMake(MAXFLOAT, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin context:NULL].size;
                CGFloat tokenWidth = tokenSize.width;
                CTLineRef truncationTokenLine = CTLineCreateWithAttributedString((CFAttributedStringRef)tokenString);
                
                // 根据【截断标识字符串(...)】的长度,计算【需要截断字符串】的最后一个字符的位置,把该位置之后的字符从【需要截断字符串】中移除,留出【截断标识字符串(...)】的位置
                CFIndex truncationEndIndex = CTLineGetStringIndexForPosition(line, CGPointMake(bounds.size.width - tokenWidth, 0));
                CGFloat length = range.location + range.length - truncationEndIndex;
                
                // 把【截断标识字符串(...)】添加到【需要截断字符串】后面
                NSMutableAttributedString *truncationString = [[[self attributeStringToDraw] attributedSubstringFromRange:NSMakeRange(range.location, range.length)] mutableCopy];
                if (length < truncationString.length) {
                    [truncationString deleteCharactersInRange:NSMakeRange(truncationString.length - length, length)];
                    [truncationString appendAttributedString:tokenString];
                }
                
                // 使用`CTLineCreateTruncatedLine`方法创建含有【截断标识字符串(...)】的`CTLine`对象
                CTLineRef truncationLine = CTLineCreateWithAttributedString((CFAttributedStringRef)truncationString);
                CTLineTruncationType truncationType = kCTLineTruncationEnd;
                CTLineRef lastLine = CTLineCreateTruncatedLine(truncationLine, bounds.size.width, truncationType, truncationTokenLine);
                
                // 添加truncation的位置信息
                NSArray *runs = (NSArray *)CTLineGetGlyphRuns(line);
                if (runs.count > 0 && self.truncationActionHandler) {
                    CTRunRef run = (__bridge CTRunRef)runs.lastObject;
                    
                    CGFloat ascent;
                    CGFloat desent;
                    // 可以直接从metaData获取到图片的宽度和高度信息
                    CGFloat width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &desent, NULL);
                    CGFloat height = ascent + desent;
                    
                    YTTruncationItem* truncationItem = [YTTruncationItem new];
                    CGRect truncationFrame = CGRectMake(width - tokenWidth,
                                                        bounds.size.height - lineOrigins[lineIndex].y - height,
                                                        tokenSize.width,
                                                        tokenSize.height);
                    [truncationItem addFrame:truncationFrame];
                    truncationItem.clickActionHandler = self.truncationActionHandler;
                    [self.truncations addObject:truncationItem];
                }
                
                
                YTCTLine *ytLine = [YTCTLine new];
                ytLine.ctLine = lastLine;
                ytLine.position = CGPointMake(lineOrigins[lineIndex].x, lineOrigins[lineIndex].y);
                [self.linesToDraw addObject:ytLine];
                
                CFRelease(truncationTokenLine);
                CFRelease(truncationLine);
                
            } else {
                YTCTLine *ytLine = [YTCTLine new];
                ytLine.ctLine = line;
                ytLine.position = CGPointMake(lineOrigins[lineIndex].x, lineOrigins[lineIndex].y);
                [self.linesToDraw addObject:ytLine];
            }
        }
    }
}

“显示更多”事件处理

点击了“显示更多”,调用的是YTDrawView类的setNumberOfLines方法,方法的处理很简单只是更新YTRichContentData类的numberOfLines属性,调用setNeedsDisplay方法请求YTDrawView类进行重新绘制

- (void)setNumberOfLines:(NSInteger)numberOfLines {
    self.data.numberOfLines = numberOfLines;
    [self setNeedsDisplay];
}

YTDrawView类会调用drawRect方法,drawRect方法会重新处理数据和进行绘制

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

推荐阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    阳明先生_X自主阅读 15,988评论 3 119
  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,416评论 8 265
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,111评论 1 32
  • 首先分析一件事,锦绣未央里李常茹未婚先孕这件事在那个时代为世人所诟病吗?不会,北魏是由鲜卑族拓跋部建立的,属于游牧...
    辉的阅读 769评论 9 3
  • 镜子照出许多小与大 而梦却在掩饰 虚构 一生都在跋涉 虚无境 欲在阻隔 白雾蒹葭 知道我是什么 脚踏一半现实和幻相...
    朴二雄阅读 190评论 2 1