CoreText進階(四)-文字行數限制和顯示更多

本文的主要內容是使用CoreText如何進行行數的限制,以及設置了行數限制末尾的內容被截斷了怎麼設置截斷的標識。此外,還有如何設置自定義的截斷標識字符串(好比「顯示更多」)、設置自定義截斷標識字符串的點擊事件等的相關討論git

其它文章:
CoreText入門(一)-文本繪製
CoreText入門(二)-繪製圖片
CoreText進階(三)-事件處理
CoreText進階(四)-文字行數限制和顯示更多
CoreText進階(五)- 文字排版樣式和效果
CoreText進階(六)-內容大小計算和自動佈局
CoreText進階(七)-添加自定義View和對其app

用例和效果

Demo:CoreTextDemo佈局

效果圖:atom

默認的截斷標識和自定義的截斷標識符效果圖
默認的截斷標識和自定義的截斷標識符效果圖 .net

點擊查看更多以後的效果圖code

點擊查看更多以後的效果圖orm

爲了能夠設置顯示的行數以及截斷的標識字符串,YTDrawView類提供了三個屬性,外部能夠經過設置參數的方式來設置行數和截斷的標識字符串,而且能夠設置點擊事件對象

@property (nonatomic, assign) NSInteger numberOfLines; ///< 行數
@property (nonatomic, strong) NSAttributedString *truncationToken;///<截斷的標識字符串,默認是"..."
@property (nonatomic, copy) ClickActionHandler truncationActionHandler;///<截斷的標識字符串點擊事件

使用的示例代碼:blog

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];

分析

步驟分析

主要的有如下幾個步驟:token

  • 判斷有沒有設置行數限制,沒有使用默認繪製(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];
}
相關文章
相關標籤/搜索