YYLabel是如何實現(xiàn)numberOfLines(固定行數(shù))的

前言

自己如何實現(xiàn)一個像UILabel這樣的系統(tǒng)控件?要仿寫一個系統(tǒng)的控件,需要先了解系統(tǒng)控件的內(nèi)部實現(xiàn)機制,了解其優(yōu)缺點,最終寫出自己的,包括系統(tǒng)控件的優(yōu)點,又加入自己認為更加方便、高效的設(shè)計的控件。
先拋出一個疑問,UILabel是如何實現(xiàn)固定行數(shù)的?

YYLabel為例,先從 YYLabel設(shè)置numberOfLines的使用說起,在創(chuàng)建YYLabel之后,設(shè)置

label.numberOfLines = 4;
CF29A19C5FAF6529CDC597900203D762.png

一層層查看

// numberOfLines Setter
- (void)setNumberOfLines:(NSUInteger)numberOfLines {
    if (_numberOfLines == numberOfLines) return;
    _numberOfLines = numberOfLines;
    _innerContainer.maximumNumberOfRows = numberOfLines;
    if (_innerText.length && !_ignoreCommonProperties) {
        if (_displaysAsynchronously && _clearContentsBeforeAsynchronouslyDisplay) {
            [self _clearContents];
        }
        [self _setLayoutNeedUpdate];
        [self _endTouch];
        [self invalidateIntrinsicContentSize];
    }
}

又將numberOfLines傳遞給了_innerContainermaximumNumberOfRows,繼續(xù)進入YYTextContainer這個類。

下面是作者的注釋

 The YYTextContainer class defines a region in which text is laid out.
 YYTextLayout class uses one or more YYTextContainer objects to generate layouts.

YYTextContainer 類定義了一個文本布局及顯示的區(qū)域
YYTextLayout 類使用一個或多個YYTextContainer對象去生成布局
 
 A YYTextContainer defines rectangular regions (`size` and `insets`) or 
 nonrectangular shapes (`path`), and you can define exclusion paths inside the 
 text container's bounding rectangle so that text flows around the exclusion 
 path as it is laid out.

YYTextContainer 定義了一個矩形(包括size和insets)或者是一個非矩形的區(qū)域
你可以在這個矩形或者區(qū)域內(nèi)再定義一個隔開的區(qū)域,讓這個矩形或者區(qū)域內(nèi)的內(nèi)容環(huán)繞這個隔開的區(qū)域。

 All methods in this class is thread-safe.
 所有的方法都是線程安全的
 Example:
 
     ┌─────────────────────────────┐  <------- container
     │                             │
     │    asdfasdfasdfasdfasdfa   <------------ container insets
     │    asdfasdfa   asdfasdfa    │
     │    asdfas         asdasd    │
     │    asdfa        <----------------------- container exclusion path
     │    asdfas         adfasd    │
     │    asdfasdfa   asdfasdfa    │
     │    asdfasdfasdfasdfasdfa    │
     │                             │
     └─────────────────────────────┘

接著走

- (void)setMaximumNumberOfRows:(NSUInteger)maximumNumberOfRows {
    Setter(_maximumNumberOfRows = maximumNumberOfRows);
}

此時YYTextContainer持有這個外部的行數(shù),實際上這個YYTextContainer是定義在YYTextLayout文件中的,說明兩個類的相關(guān)性很大,此時再了解一下YYTextLayout這個類。

 YYTextLayout class is a readonly class stores text layout result.
 All the property in this class is readonly, and should not be changed.
 The methods in this class is thread-safe (except some of the draw methods).
// 中文注釋
YYTextLayout 是一個只讀的、存儲Layout布局結(jié)果的類
所有的屬性都是只讀的,所有方法都是線程安全的(除了一些繪制方法)。

 example: (layout with a circle exclusion path)
 
     ┌──────────────────────────┐  <------ container
     │ [--------Line0--------]  │  <- Row0
     │ [--------Line1--------]  │  <- Row1
     │ [-Line2-]     [-Line3-]  │  <- Row2
     │ [-Line4]       [Line5-]  │  <- Row3
     │ [-Line6-]     [-Line7-]  │  <- Row4
     │ [--------Line8--------]  │  <- Row5
     │ [--------Line9--------]  │  <- Row6
     └──────────────────────────┘

YYTextContainer內(nèi)部,并沒有過多的處理,最多的就是Setter和Getter了。那么上面的maximumNumberOfRows接下來又會怎么處理?
YYTextLayout的初始化,需要YYTextContainer,最終maximumNumberOfRows傳遞給了YYTextLayout

+ (YYTextLayout *)layoutWithContainer:(YYTextContainer *)container text:(NSAttributedString *)text range:(NSRange)range {
...
maximumNumberOfRows = container.maximumNumberOfRows;
}

在對單行文本計算完高度之后,根據(jù)條件計算文本區(qū)域

// textBoundingRect 文本的frame, rect是單行文本的frame, CGRectUnion是返回包含兩個矩形的最小矩形
if (maximumNumberOfRows == 0 || rowIdx < maximumNumberOfRows) {
    // 得出的textBoundingRect是在不限制行高,或者當前行數(shù)小于總行數(shù)時的矩形區(qū)域,也就是文本的寬高組成的矩形區(qū)域。
    textBoundingRect = CGRectUnion(textBoundingRect, rect);
}

這里引入一個概念,文本的實際行數(shù)。
CoreText庫中,提供了計算當前給定size的文本行數(shù)的方法。

demo代碼如下:

NSString *src = [NSString stringWithString:@"樣本文字。 "];    
NSMutableAttributedString * mabstring = [[NSMutableAttributedString alloc]initWithString:src];
long slen = [mabstring length];

// 將屬性字串放到frame當中
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)mabstring);
    
CGMutablePathRef Path = CGPathCreateMutable();
CGPathAddRect(Path, NULL ,CGRectMake(10 , 10 ,self.bounds.size.width-20 , self.bounds.size.height-20));
    
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), Path, NULL);

// 得到字串在frame中被自動分成了多少個行。
CFArrayRef rows = CTFrameGetLines(frame);
// 實際行數(shù)
int rowcount = CFArrayGetCount(rows);     

在回到YYTextLayout中判斷條件如下

if (maximumNumberOfRows > 0) {
// 如果實際行數(shù)大于給定行數(shù) 
            if (rowCount > maximumNumberOfRows) {
                needTruncation = YES;// 顯示省略號
                rowCount = maximumNumberOfRows;// 更新行數(shù)為指定的最大行數(shù)
                do {
                     // 從需要被渲染的行數(shù)組中移除顯示不下的內(nèi)容
                    YYTextLine *line = lines.lastObject;
                    if (!line) break;
                    if (line.row < rowCount) break;
                    [lines removeLastObject];
                } while (1);
            }
}

至此,YYLabel的行數(shù)numberOfLines實現(xiàn)的方式就說清楚了。
可以簡單概括為:
在自己實現(xiàn)一個繼承自UIView的Label控件時,要實現(xiàn)numberOfLines屬性,有如下步驟
①根據(jù)給定區(qū)域計算文本的實際行數(shù)
②當實際行數(shù)大于給定行數(shù)時從總的lines數(shù)組中移除不需要被渲染的line,并顯示截斷樣式。

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

推薦閱讀更多精彩內(nèi)容