前言
自己如何实现一个像UILabel这样的系统控件?要仿写一个系统的控件,需要先了解系统控件的内部实现机制,了解其优缺点,最终写出自己的,包括系统控件的优点,又加入自己认为更加方便、高效的设计的控件。
先抛出一个疑问,UILabel
是如何实现固定行数的?
以YYLabel
为例,先从 YYLabel
设置numberOfLines
的使用说起,在创建YYLabel
之后,设置
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
,继续进入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 类定义了一个文本布局及显示的区域
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)或者是一个非矩形的区域
你可以在这个矩形或者区域内再定义一个隔开的区域,让这个矩形或者区域内的内容环绕这个隔开的区域。
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
持有这个外部的行数,实际上这个YYTextContainer
是定义在YYTextLayout
文件中的,说明两个类的相关性很大,此时再了解一下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布局结果的类
所有的属性都是只读的,所有方法都是线程安全的(除了一些绘制方法)。
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
内部,并没有过多的处理,最多的就是Setter和Getter了。那么上面的maximumNumberOfRows
接下来又会怎么处理?
YYTextLayout
的初始化,需要YYTextContainer
,最终maximumNumberOfRows
传递给了YYTextLayout
。
+ (YYTextLayout *)layoutWithContainer:(YYTextContainer *)container text:(NSAttributedString *)text range:(NSRange)range {
...
maximumNumberOfRows = container.maximumNumberOfRows;
}
在对单行文本计算完高度之后,根据条件计算文本区域
// textBoundingRect 文本的frame, rect是单行文本的frame, CGRectUnion是返回包含两个矩形的最小矩形
if (maximumNumberOfRows == 0 || rowIdx < maximumNumberOfRows) {
// 得出的textBoundingRect是在不限制行高,或者当前行数小于总行数时的矩形区域,也就是文本的宽高组成的矩形区域。
textBoundingRect = CGRectUnion(textBoundingRect, rect);
}
这里引入一个概念,文本的实际行数。
在CoreText
库中,提供了计算当前给定size
的文本行数的方法。
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);
// 实际行数
int rowcount = CFArrayGetCount(rows);
在回到YYTextLayout中
判断条件如下
if (maximumNumberOfRows > 0) {
// 如果实际行数大于给定行数
if (rowCount > maximumNumberOfRows) {
needTruncation = YES;// 显示省略号
rowCount = maximumNumberOfRows;// 更新行数为指定的最大行数
do {
// 从需要被渲染的行数组中移除显示不下的内容
YYTextLine *line = lines.lastObject;
if (!line) break;
if (line.row < rowCount) break;
[lines removeLastObject];
} while (1);
}
}
至此,YYLabel
的行数numberOfLines
实现的方式就说清楚了。
可以简单概括为:
在自己实现一个继承自UIView
的Label控件时,要实现numberOfLines
属性,有如下步骤
①根据给定区域计算文本的实际行数
②当实际行数大于给定行数时从总的lines数组中移除不需要被渲染的line,并显示截断样式。