字符串渲染

本文翻译自 objc #9,感谢  @方一雄 提供校对。

 

在这次issue中我们已经讨论了很多关于字符串不同的话题,从编码到本地化再到语法分析。但多数情况下,字符串最终还是需要被绘制到屏幕上供用户查看、交互。这篇文章涵盖了最基本、最好的练习,以及在用户界面上呈现字符串可能遇到的常见陷阱。

如何将字符串绘制到屏幕上

为了简单起见,我们先看看UIKit在字符串渲染方面为我们提供了哪些控件。之后我们将讨论一下对于字符串的渲染, iOS 和 OS X 系统中有哪些相似和不同。

UIKit 提供了很多可以在屏幕上显示和编辑文本的类。每一个类都是为特定使用情况准备的,所以为了避免不必要的问题,为你手上的任务挑选正确的工具是非常重要的。

UILabel

UILabel是将文本绘制到屏幕上最简单的方式。它是UIView的一个子类,用来显示少量的只读文本。文本可以被展示在一行或多行,如果文本不能适应指定的空间我们还可以使用不同的方式裁剪。尽管labels使用的方式很简单,但是这里有几个技巧还是值得我们提一提的。

labels默认只显示一行,但是你可以将numberOfLines属性设为其他值来改变这一行为。将它设置为一个大于1的值,文本的行数将会被限制为这个指定的值,如果设置为0则是告诉label不管文本占多少行都显示出来。

通过设置text属性,Labels可以显示简单的纯文本,而设置attributedText属性则可以让label显示富文本。当使用纯文本的时候,你可以使用label的font,textColor,

textAlignment,shadowColor和shadowOffset属性改变它的外观,如果你希望改变整个程序所有Label的风格,你也可以使用[UILabel appearance] 这个方法来进行全局的更改。

Attributed strings提供了更加灵活的风格可供选择,字符串的不同部分可以使用不同的风格。让我们看看常见布局部分,下面给出attributed strings一些示例。(下文“常见布局”那一节给出了具体的关于 Attributed String 的一些例子。)

除了通过上文提到的那些属性来调整UILabel 的显示风格外,你还可以通过设置UILabel的这3个BOOL值的属性adjustsFontSizeToWidth,minimumScaleFactor,adjustsLetterSpacingToFitWidth 来让 UILabel 根据所显示的文本的内容自动地进行调整。如果你非常在意用户界面的美观,那么你就不要开启这些属性,因为这会使文字的显示效果变得不那么美观,但是有的时候,比如在进行App的不同语言的本土化的时候,你会遇到一些很棘手的问题,除了使用这些选项外很难找到别的解决办法。不信的话,你可以打开 iPhone,在设置中把系统语言改为德语,然后你就会发现苹果官方出品的程序里到处都是被压扁变了形的丑陋不堪的文本。这种处理方法并不完美,但有时却很有用。

如果你使用这些选项让UIKit压缩你的文本以适配,如果压缩的时候想让文本保持在同一条基线上或需要对齐到左上角,那么你可以定义baselineAdjustment属性。然而,这个选项只对单行labels起作用。

当你使用上述的方法让文本自动缩放大小以适配你的 UILabel 时,你可以使用 baselineAdjustment 这个属性来调整缩放时文本的基准线,是保持统一基准线还是对齐到你的 Label 的左上角。注意,这个属性仅在单行的 Lable (即 numberOfLines 属性值为1时)中生效。

UITextField

像labels一样,text fields可以处理纯文本或带属性的文本。但labels只是能显示文本而已,text fields还可以处理用户输入。然而,text fields只限于单行文本。因此,UITextField是UIControl的一个子类,它会挂钩到(hook into)响应链,并且当用户开始或结束编辑时分发(deliver)这些行为消息。如果想要得到更多的控制权,你可以实现text field的代理。

Text fields有一系列控制文本输入行为的选项。UITextField 实现了UITextInputTraits协议,这个协议需要你指定键盘外观和操作的各种细节,比如,需要显示哪种键盘,返回按钮的响应事件是什么。

当没有文本输入的时候Text fields还可以显示一个占位符,在右手边显示一个标准的清除按钮,控制任意左右两个辅助视图。你还可以为其设置一个背景图片,这样我们就可以用一个可变大小的图片为text field自定义边框风格了。

但每当你需要输入多行文本的时候,你就需要使用到UITextField的大哥了……

UITextView

Text views是显示或编辑大量文本的理想选择。UITextView是UIScrollView的一个子类,所以它能允许用户前后滚动达到处理溢出文本的目的。和text fields一样,text views也能处理纯文本和带属性的文本。Text views还也实现了UITextInputTraits协议来控制键盘的行为和外观。

但除了text view处理多行文本的能力外,它最大的卖点就是你可以使用、定制整个Text Kit堆。你可以自定义行为或为layout manager、text container或text storage替换你自定义的子类。objc.io issue #5中有提到Text Kit方面的文章

不幸的是,UITextView在iOS7中还有些问题。目前还是1.0版本。它是基于OS X Text Kit从头开始重新实现的。iOS7之前,它是基于Webkit并且功能很少。

Mac中又是什么情况呢?

现在我们的讨论已经覆盖了UIKit中基本的text类,我们继续解释一下这些类在AppKit中结构的不同之处。

首先,AppKit中并没有类似UILabel的控件。而显示文本最基本的类是NSTextField。我们将text field设为不可编辑、不可选择,这样便等同于iOS中的UILabel了。虽然NSTextField听起来类似于UITextField,但NSTextField并不限制于单行文本。

NSTextView,换句话说,就是等同于UITextView,它也为我们揭露了整个Cocoa Text System。但它还囊括了很多额外的功能。很大的原因是因为Mac是一个具有指针设备(鼠标)的电脑。最值得注意的就是包含了设置、编辑制表符的标尺。

Core Text

上面我们讨论的所有类最终都使用Core Text布局、绘制真实的符号。Core Text是一个非常强大的framework,它已经超出我们这篇文章讨论的范围。但是如果你曾经需要通过完全自定义的方式绘制文本(e.g.贝塞尔曲线),那你需要详细的了解一下。

Core Text在任何绘图方面为你提供了充分的灵活性。然而,Core Text非常难于操作。它是一个复杂的Core Foundation / C API。Core Text 在排版方面给了你充分的使用权。

在Table View中显示动态Text

可能和所有人都打过交道的字符串绘制方法就是最常见的可变高度的table view cells。你能在社交媒体应用中见到这种。table view的delegate有一个方法。tableView:heightForRowAtIndexPath:,这便是用来计算高度的。iOS7之前,很难通过一种可靠的方式使用它。

在我们的示例中,我们将会在table view中显示一列语录:

uitableview-finished

首先,为了实现完全的自定义,我们创建一个UITableViewCell的子类。在这个子类中,我们需要亲自为我们的label布局:

- (void)layoutSubviews
{
    [super layoutSubviews];
    self.textLabel.frame = CGRectInset(self.bounds, MyTableViewCellInset,MyTableViewCellInset);
}

MyTableViewCellInset被定义为一个常量,所以我们可以将它用在table view的delegate的高度计算中。最简单、准确计算高度的方法是将字符串转换成带属性的字符串,然后计算出带属性字符串的高度。我们使用table view的宽度减去两倍的MyTableViewCellInset常量(前面和后面的空间)。为了计算真实的高度,我们使用boundingRectWithSize:options:context:.

第一个参数是限制text大小的。我们只需要关心宽度的限制,因此我们为高度传一个最大值常量 CGFLOAT_MAX.第二个参数是非常重要的:如果你传一个其他值,bounding rect无疑会出错。如果你想要调整字体缩放and/or追踪,你可以使用第三个参数。最终,一旦我们得到boundingRect,我们需要再次加上inset:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    CGFloat labelWidth = self.tableView.bounds.size.width - MyTableViewCellInset*2;
    NSAttributedString *text = [self attributedBodyTextAtIndexPath:indexPath];
    NSStringDrawingOptions options = NSStringDrawingUsesLineFragmentOrigin |NSStringDrawingUsesFontLeading;
    CGRect boundingRect = 1;
    return (CGFloat) (ceil(boundingRect.size.height) + MyTableViewCellInset*2);
}

对于bounding rect的结果还有两件敏感的事情,除非你读了文档,不然这两件事你不一定会知道:返回的size返回一个小数,文档中让我们使用ceil将结果四舍五入。最终,结果可能是会比实际的大一点。

请注意,因为我们的text是纯文本时,我们创建的attributedBodyTextAtIndexPath:方法也会在tableView:cellForRowAtIndexPath:中用到。这样,我们需要确保他们保持同步。

还有,看看文档(如下截图),我们可以看到iOS7发布后,很多方法都被弃用了。如果你通过查找网页或StackOverflow,你会发现很多答案、以及测量字符大小的变通方法。因为text system受到了重大检修(在内部实现中,所有的东西都使用TextKit进行绘制了,而不是WebKit),所以请使用新方法。

deprecated-methods

另一个动态调整table view cell大小的选择就是使用Auto Layout,你可以在这篇博文中找到更详细的说明。然后你可以利用contained lables的intrinsicContentSize。然而,现在自动布局比手动计算要慢很多。可是对于原型开发,这很完美:它允许你快速调整constraints并且移动事物(特别当你cell中不止一个element时这显得特别重要)。一旦你完成产品的设计迭代,然后你就可以用手动布局的方式重新编写代码。

使用Text Kit和NSAttributedString布局

使用Text Kit,你将会拥有令人惊讶的灵活性来创建专业级别的文本布局。随着这些灵活性带来的是如何组合为数众多的选项来完成复杂的布局。

我们准备给出几个示例并强调一些常见的布局问题,同时给出解决方案。

经典的文本

首先,让我们看一些经典的文本。我们将会使用Jacomy-Régnier的Histoire des nombres et de la numération mécanique,并设为Bodoni字体。最终截屏效果如下所示:

Layout-Example-1

这些都是由Text Kit完成的。两段文字之间的装饰也是text,使用的是Bodoni Ornaments字体。

我们为文体风格使用调整好的text。第一段从最左边开始,接下来的段落都会插入空格.

这有三种不同的风格:文体风格,首行缩进的变化风格,装饰物风格。

让我们先设置body1stAttributes:


CGFloat const fontSize = 15;

NSMutableDictionary *body1stAttributes = [NSMutableDictionary dictionary];

body1stAttributes[NSFontAttributeName] = [UIFont fontWithName:@"BodoniSvtyTwoITCTT-Book"  size:fontSize];

NSMutableParagraphStyle *body1stParagraph = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];

body1stParagraph.alignment = NSTextAlignmentJustified;

body1stParagraph.minimumLineHeight = fontSize + 3;

body1stParagraph.maximumLineHeight = body1stParagraph.minimumLineHeight;

body1stParagraph.hyphenationFactor = 0.97;

body1stAttributes[NSParagraphStyleAttributeName] = body1stParagraph;

我们将字体设置为BodoniSvtyTwoITCTT。这是字体的PostScript名。如果想寻找字体名,我们可以使用+[UIFont familyNames]首先得到可用的字体系列集合。一个字体系列就是我们所熟知的字型。每个字型或字体系列有一个或多个字体。为了得到这些字体的名字,我们可以使用+[UIFont fontNamesForFamilyName:]。注意一下,当你处理多样字体时,UIFontDescriptor类非常有用,e.g.当你想要知道一个给定的字体是什么版本的斜体。

许多设置位于NSParagraphStyle。我们创建一个默认风格的可变拷贝并做些调整。在我们的例子中,我们将会为字体大小加上3pt。

接着,我们会为这些段落的属性创建一个拷贝并修改他们来创建boddyAttributes,(注意,这是我们段落的属性,跟上文的body1stParagraph已经不是同一个了)


NSMutableDictionary *bodyAttributes = [body1stAttributes mutableCopy];

NSMutableParagraphStyle *bodyParagraph =

[bodyAttributes[NSParagraphStyleAttributeName] mutableCopy];

bodyParagraph.firstLineHeadIndent = fontSize;

bodyAttributes[NSParagraphStyleAttributeName] = bodyParagraph;

我们简单的创建了一个属性字典的可变拷贝,同时为了改变段落风格我们也需要创建一个可变拷贝。将firstLineHeadIndent设为和字体大小一样,我们便会得到想要的空格缩进。

接着,装饰段落风格:


NSMutableDictionary *ornamentAttributes = [NSMutableDictionary dictionary];

ornamentAttributes[NSFontAttributeName] = [UIFont fontWithName:@"BodoniOrnamentsITCTT" size:36];

NSMutableParagraphStyle *ornamentParagraph = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];

ornamentParagraph.alignment = NSTextAlignmentCenter;

ornamentParagraph.paragraphSpacingBefore = fontSize;

ornamentParagraph.paragraphSpacing = fontSize;

ornamentAttributes[NSParagraphStyleAttributeName] = ornamentParagraph;

这个很容易理解。我们使用装饰字体并将文本居中对齐。此外,在装饰字符的前后我们都要加空白段落。

数据表格

接下来是显示数字的table。我们想要将分数的小数点对齐显示,i.e.英语中的”.”:

Layout-Example-2

为了达到这个目的,我们需要指定table将中心停在分隔符上。

对于上面这个示例,我们简单的做一下:


NSCharacterSet *decimalTerminator = [NSCharacterSet

characterSetWithCharactersInString:decimalFormatter.decimalSeparator];

NSTextTab *decimalTab = [[NSTextTab alloc]

initWithTextAlignment:NSTextAlignmentCenter  location:100 options:@{NSTabColumnTerminatorsAttributeName:decimalTerminator}];

NSTextTab *percentTab = [[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentRight location:200 options:nil];

NSMutableParagraphStyle *tableParagraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];

tableParagraphStyle.tabStops = @[decimalTab, percentTab];

列表

另一个常见的使用情况就像list这样:

Layout-Example-3

缩进相对容易设置。我们需要确保序列号(1)和text或者着重号和text之间有一个制表符。然后我们像这样调整段落的风格:


NSMutableDictionary *listAttributes = [bodyAttributes mutableCopy];

NSMutableParagraphStyle *listParagraph =

[listAttributes[NSParagraphStyleAttributeName] mutableCopy];

listParagraph.headIndent = fontSize * 3;

listParagraph.firstLineHeadIndent = fontSize;

NSTextTab *listTab = [[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentNatural location:fontSize * 3  options:nil];

listParagraph.tabStops = @[listTab];

listAttributes[NSParagraphStyleAttributeName] = listParagraph;

我们将headIndent设置为真实文本的缩进,将firstLineHeadIndent设置为我们希望着重号具有的缩进。最终,和headIndent一样,我们需要在相同的位置增加一个制表符。着重号后的制表符会确保这行文本从正确的位置开始绘制。