本文的主要内容是使用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
方法来获取 -
CTLineDraw
和CTFrameDraw
类似,不过是以行为单位进行绘制,灵活性更高,在有行数显示需要添加特殊的截断标识的场景需要使用这个方法才能满足要求 -
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];
}