前因
是咁的。
最近重构项目代码,看到某个页面的页面元素不多,UI前人实现起来却用了一大pie代码(主要方法是sizeWithFont)。
经分析,这pie代码是用来求UILabel最后一个character的frame
的,得到frame然后判断是否有足够宽度放置诸如HOT、顶等图片,是则放置,否则换行放置,并在点击时候做相应的动画。
乍一看,心生疑惑。UILabel应该有提供相关方法吧。各种头文件里转了一圈,结果。当然没有。
(我的天呐+捂嘴状
又转了一圈
Demo
后果
...
倒是在NSLayoutManager.h里找到一个近似的。
- (CGRect)boundingRectForGlyphRange:(NSRange)glyphRange inTextContainer:(NSTextContainer *)container
用法也很简单,提供一个glyphRange以及对应的textContainer,便返回该字形在textContainer里的frame。
我抓了抓头,这是要用到TextKit。TextKit虽然是iOS7就有的产物,但其三件套NSTextStorage、NSLayoutManager、NSTextContainer有些同学(就是我)平时用得不是很多,这UILabel又没有自带三件套,需要自己实现。(似乎UITextView有?
好吧,顺便复习下加深下理解。
Textkit的三件法宝:。objc有篇译文提到可以把文本系统看做一个MVC架构,即NSTextStorage -> Model,NSLayoutManager -> Controller, NSTextContainer -> View,我们可以这样理解,就不容易弄混。
就我们的需求来讲,配置好三件套的互相依赖,给NSTextStorage赋值,设置NSTextContainer的属性,NSLayoutManager就会帮我们布局。布局完成后,我们就可以向NSLayoutManager询问布局的情况,也可以通过一些代理方法改变其布局。
没有3了。
好的。代码时间。
- (void)setupBasic
{
self.textStorage = [NSTextStorage new];
self.layoutManager = [NSLayoutManager new];
self.textContainer = [NSTextContainer new];
[self.textStorage addLayoutManager:self.layoutManager];
[self.layoutManager addTextContainer:self.textContainer];
}
上面这段代码是初始化三件套。
- (void)configWithLabel:(UILabel *)label
{
self.textContainer.size = label.bounds.size;
self.textContainer.lineFragmentPadding = 0;
self.textContainer.maximumNumberOfLines = label.numberOfLines;
self.textContainer.lineBreakMode = label.lineBreakMode;
NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:label.text];
NSRange textRange = NSMakeRange(0, attributedText.length);
[attributedText addAttribute:NSFontAttributeName value:label.font range:textRange];
NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new];
paragraphStyle.alignment = label.textAlignment;
[attributedText addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:textRange];
[self.textStorage setAttributedString:attributedText];
}
根据指定的label配置我们的'View'(NSTextContainer)与'Model'(NSTextStorage)。这里要保证label是有size的,如果用了Autolayout,则要在如viewDidLayoutSubviews
等layout已经完成的地方configLabel,否则textContainer接收到的size是错误的。
- (CGRect)characterRectAtIndex:(NSUInteger)charIndex
{
if (charIndex >= self.textStorage.length) {
NSLog(@"Plz enter a correct number");
return CGRectZero;
}
NSRange characterRange = NSMakeRange(charIndex, 1);
NSRange glyphRange = [self.layoutManager glyphRangeForCharacterRange:characterRange actualCharacterRange:nil];
return [self.layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:self.textContainer];
}
给textStorage赋值完毕后,我们就可以询问我们的'Controller',获取相应的frame。
另外提一下,给textStorage赋值完毕并布局完毕后,layoutManager便会调用以下代理方法。
- (void)layoutManager:(NSLayoutManager *)layoutManager didCompleteLayoutForTextContainer:(nullable NSTextContainer *)textContainer atEnd:(BOOL)layoutFinishedFlag
补充
需要注意的是,刚开始我获取到的frame的x值与实际的x值总有一些误差,便猜想是我们创建出来的NSTextContainer与UILabel的布局有差异所致,所幸NSTextContainer的头文件较短,经测试,将lineFragmentPadding设为0
即可。
我这里直接把label本身作为参数传了过来,其实只需要把NSTextStorage等所需的属性都赋值正确就可以正确布局。这里用到label的属性有:
bounds.size
numberOfLines
lineBreakMode
font
textAlignment
懊恼
懊恼的是,以上写法也并未达到我的期望,要是有一个自带三件套的UILabel就好了。
用TextView代替Label
UIKit还真给了一个自带三件套的'UILabel',不过它叫UITextView。
google下本文的需求,stackoverflow上就有一个答案建议使用UITextView实现。
UILabel doesn't have any methods for doing this. You can do it with UITextView, because it implements the UITextInput protocol. You will want to set the text view's editable property to NO.
核心代码为简单易懂。答主特别提到要把editable设置为NO。
- (CGRect)rectInTextView:(UITextView *)textView stringRange:(NSRange)stringRange
{
UITextPosition *begin = [textView positionFromPosition:textView.beginningOfDocument offset:stringRange.location];
UITextPosition *end = [textView positionFromPosition:begin offset:stringRange.length];
UITextRange *textRange = [textView textRangeFromPosition:begin toPosition:end];
return [textView firstRectForRange:textRange];
}
不过实际上设置同样的text,UITextView与UILabel呈现出来的样子是不一致的。万恶的罪魁是TextContainer(藏镜人不服。
textView.scrollEnabled = NO;
textView.scrollsToTop = NO;
textView.editable = NO;
textView.textContainerInset = UIEdgeInsetsZero;
textView.textContainer.lineFragmentPadding = 0;
如上设置后,UITexView简直就是另一个UILabel了。
不过如果真的用TextView去代替Label似乎有点不太舒服,所以我Demo里的另一种做法是依然用Label做展示,seeker里用TextView做计算。
总结
平时处理文本遇到困难的话,我们可能第一时间会想到CoreText,其实来iOS7开始就有的TextKit已经提供了很多便利的方法,我们平时写UI时也该多想想是否可以用较新的API去实现,视野才能更开阔。