前言
自己如何實現(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;
一層層查看
// 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
傳遞給了_innerContainer
的maximumNumberOfRows
,繼續(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,并顯示截斷樣式。