iOS | CoreText簡單實現卷展Label視圖

開發背景

  • 公司最近項目中有一個如下圖的需求,在Github找了好久沒有發現類似的Demo,于是思考了幾天,成功實現了這種效果。
需求效果:點擊`更多`文本跳轉到其他頁面
  • 然后我經過改造,實現了類似于可折疊的Label效果,并支持轉屏自動繪制。如下圖所示:
最終效果 - 豎屏
最終效果 - 橫屏

實現過程

  • 主要思路是利用CoreText系統庫進行的富文本繪制;

  • 在最后一行時,通過不斷讓index減1,然后獲得subAttrText,再加上...更多文本進行計算文本所占的行數lines,直到行數為1行;

  • 最后進行drawAttrText的繪制,并回調返回計算后的totalHeight,讓控制器更新heightConstraint約束;

1.drawRect
-(void)drawRect:(CGRect)rect{
    [super drawRect:rect];
    
    if (!_attributedText) {
        return;
    }
    
    [self drawTextWithCompletion:^(CGFloat height, NSAttributedString *drawAttributedText) {
        [self addSubview:self.contentView];
        self.contentView.frame = CGRectMake(0, 0, self.bounds.size.width, height);
        self.contentView.attributedText = drawAttributedText;
        // 回調
        _action ? _action(HYExpandableLabelActionDidLoad, @(height)) : nil;
    }];
}
2.指定行數繪制
  • CGPathRef 繪制區域
CGRect rect = CGRectMake(0, 0, self.bounds.size.width, .size.height);
CGPathRef path = CGPathCreateWithRect(rect, nil);
  • CTFrameRef
CTFramesetterRef setter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)_attributedText);
CTFrameRef ctFrame = CTFramesetterCreateFrame(setter, CFRangeMake(0, _attributedText.length), path, NULL);
  • CTLines
NSArray *lines = (NSArray*)CTFrameGetLines(ctFrame);
  • CTLine Origins 每一行的繪制原點
CGPoint ctOriginPoints[lines.count];
CTFrameGetLineOrigins(ctFrame, CFRangeMake(0, 0), ctOriginPoints);
  • 計算繪制文本drawAttrText和總高度
NSMutableAttributedString *drawAttributedText = [NSMutableAttributedString new];
    
    for (int i=0; i<lines.count; i++) {
        if (lines.count > _maximumLines && i == _maximumLines) {
            break;
        }
        CTLineRef line = (__bridge CTLineRef)lines[i];
        
        CGPoint lineOrigin = ctOriginPoints[i];
        
        CFRange range = CTLineGetStringRange(line);
        NSAttributedString *subAttr = [_attributedText attributedSubstringFromRange:NSMakeRange(range.location, range.length)];

        if (lines.count > _maximumLines && i == _maximumLines - 1) {
            // 最后一行特殊處理
        }
        else{
            [drawAttributedText appendAttributedString:subAttr];
            
            totalHeight += [self heightForCTLine:line];
        }
    }
  • 最后一行的處理
NSMutableAttributedString *drawAttr = [[NSMutableAttributedString alloc] initWithAttributedString:subAttr];

for (int j=0; j<drawAttr.length; j++) {
    NSMutableAttributedString *lastLineAttr = [[NSMutableAttributedString alloc] initWithAttributedString:[drawAttr attributedSubstringFromRange:NSMakeRange(0, drawAttr.length-j)]];
    
    [lastLineAttr appendAttributedString:self.clickAttributedText];
    
    NSInteger number = [self numberOfLinesForAttributtedText:lastLineAttr withOriginPoint:lineOrigin];
    // 當滿足為一行時,break
    if (number == 1) {
        [drawAttributedText appendAttributedString:lastLineAttr];
        
        CTLineRef moreLine = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)self.clickAttributedText);
        CGSize moreSize = CTLineGetBoundsWithOptions(moreLine, 0).size;
        // 點擊區域
        self.clickArea = CGRectMake(self.bounds.size.width-moreSize.width, totalHeight, moreSize.width, moreSize.height);
        
        totalHeight += [self heightForCTLine:line];
        break;
    }
}
  • 回調
completion(totalHeight, drawAttributedText);
3.全部文本繪制
  • 全部文本繪制(即展開文本)繪制相對來說較為簡單,只需將clickAttrText追加到attrText后,然后繪制

  • CTFrameRef、CTLines等的獲取和上面一樣,下面主要說下totalHeight計算及clickArea的計算

for (int i=0; i<lines.count; i++) {
        CTLineRef line = (__bridge CTLineRef)lines[i];
        totalHeight += [self heightForCTLine:line];
        // 繪制最后一行時
        if (i == lines.count - 1) {
            CTLineRef moreLine = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)self.clickAttributedText);
            
// 計算`收起`文本的origin.x值
            NSArray *runs = (NSArray*)CTLineGetGlyphRuns(line);
            CGFloat w = 0;
            for (int i=0; i<runs.count; i++) {
                if (i == runs.count - 1) {
                    break;
                }
                CTRunRef run = (__bridge CTRunRef)runs[i];
                w += CTRunGetTypographicBounds(run, CFRangeMake(0, 0), NULL, NULL, NULL);
            }
            
            CGSize moreSize = CTLineGetBoundsWithOptions(moreLine, 0).size;
            CGFloat h = moreSize.height + lines.count * _lineHeightErrorDimension;
            self.clickArea = CGRectMake(w, totalHeight - h, moreSize.width, h);
        }
    }

用法

@property (weak, nonatomic) IBOutlet HYExpandableLabel *expandableLabel;
// 高度值約束
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *expandableLabelHeightCons;

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _expandableLabel.attributedText = [[NSAttributedString alloc] initWithString:@"測試文本??"  attributes: @{ NSFontAttributeName:[UIFont systemFontOfSize:14] }];
        
    __block typeof(self)weakSelf = self;
    _expandableLabel.action = ^(HYExpandableLabelActionType type, id info) {
        if (type == HYExpandableLabelActionDidLoad) {
            NSLog(@"_expandableLabel Did Calculated");
// 更新布局
            weakSelf.expandableLabelHeightCons.constant = [info floatValue];
            [weakSelf.view layoutIfNeeded];
        }
    };
}

Github

https://github.com/BackWorld/HYExpandableLabel

如果對你有幫助,別忘了加個關注 或 點個哦??

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容