UILabel 如何垂直居中其文本?
How does UILabel vertically center its text?
对于我实际想要实现的目标存在很多困惑。所以请让我重述我的问题。很简单:
How does Apple center text inside UILabel
's -drawTextInRect:
?
以下是我寻找 Apple 居中文本方法的一些背景信息:
- 我不满意他们在此方法中将文本居中的方式
- 我有自己的算法,可以按照我想要的方式完成工作
但是,有些地方我无法将此算法注入到其中,即使使用方法调配也是如此。了解他们使用的确切算法(或等效算法)将解决我个人的问题。
这里有一个 demo project 您可以试验一下。如果您对我的 Apple 实现问题感兴趣,请查看左侧(基本 UILabel):当您使用滑块增加字体大小时,文本会上下跳动,而不是逐渐向下移动。
同样,我要寻找的是 -drawTextInRect:
的实现,它与基 UILabel
完全相同——但不调用 super
.
- (void)drawTextInRect:(CGRect)rect
{
CGRect const centeredRect = ...;
[self.attributedText drawInRect:centeredRect];
}
我没有答案,但我做了一些研究并认为我会分享它。我使用 Xtrace,它可以让您跟踪 Objective-C 方法调用,以尝试找出 UILabel 在做什么。我将 Xtrace git 存储库中的 Xtrace.h
和 Xtrace.mm
添加到 Christian 的 UILabel 演示项目中。在 RootViewController.m
中,我添加了 #import "Xtrace.h"
,然后尝试了各种跟踪过滤器。将以下内容添加到 RootViewController 的 loadView
:
[Xtrace includeMethods:@"drawWithRect|drawInRect|drawTextInRect"];
[Xtrace traceClassPattern:@"String|UILabel|AppleLabel" excluding:nil];
给我这个输出:
| [<AppleLabel 0x7fedf141b520> drawTextInRect:{{0, 0}, {187.5, 333.5}}]
| [<NSConcreteMutableAttributedString 0x7fedf160ec30>/NSAttributedString drawInRect:{{0, 156.5}, {187.5, 333.5}}]
| [<UILabel 0x7fedf1418dc0> drawTextInRect:{{0, 0}, {187.5, 333.5}}]
| [<UILabel 0x7fedf1418dc0> _drawTextInRect:{{0, 0}, {187.5, 333.5}} baselineCalculationOnly:0]
| [<__NSCFConstantString 0x1051d1db0>/NSString drawWithRect:{{0, 156.3134765625}, {187.5, 20.287109375}} options:1L attributes:<__NSDictionaryM 0x7fedf141ae00> context:<NSStringDrawingContext 0x7fedf15503c0>]
(我 运行 使用 Xcode 7.0 beta,iOS 9.0 模拟器。)
我们可以看到,Apple 的实现不会将 drawInRect:
发送到 NSAttributedString,而是将 drawWithRect:options:attributes:
发送到 NSString。我猜这些最终会做同样的事情。
我们还可以准确地看到计算出的 Y 偏移量是多少。在滑块的这个初始位置,它是 156.3134765625。有趣的是,它不是落在某个四舍五入的整数值或 0.25 的倍数或类似值上的东西,否则这将是对跳跃的解释。
我们还看到 UILabel 发送 20.287109375 作为高度,这与 self.attributedText.size.height
计算的值相同。所以它似乎正在使用它,尽管我无法追踪那个调用(也许它是直接使用 Core Text?)。
这就是我被困的地方。一直在尝试查看 UILabel 计算的内容与明显的公式 "labelYOffset = (boundsHeight - labelHeight) / 2.0" 之间的区别,但我发现没有一致的模式。
更新 1.
所以我们可以将这个难以捉摸的 Y 偏移建模为
labelYOffset = (boundsHeight - labelHeight) / 2.0 + error
error
是什么,正是我们想要弄清楚的。让我们试着像科学家一样研究这个。 Here is Xtrace output when I've dragged the slide to the end and then to the beginning. I also added an NSLog at each change of the slider which helps with separating the entries. I wrote a Python script 根据标签高度绘制误差。
有意思。我们看到误差有一些线性,但如果您理解我的意思,它们会在某些点奇怪地跳到线之间。这里发生了什么?在解决这个问题之前,我会失眠。 :)
我在自己制作的贴图应用中经常处理这个问题。
我认为问题可能不是 UILabel 的问题,而是包含上升和下降的字体。我的假设是 UILabel 在定位文本正文时会考虑这些值,这对明显的垂直位置有影响。很多字体的问题是这些值在 TTF / OTF 文件中完全搞砸了,所以你得到例如使用 .sizeToFit()、重叠线、不稳定的垂直位置等剪裁下降部
有以下三种方法可以解决这个问题:
1) 使用 NSAttributedString 和更多带有 NSBaselineOffsetAttributeName 的基线。 TTTAttributedLabel 是一个很好的快捷方式。
2) 使用 CoreText 和 CoreGraphics 在自定义 UIView 子类中的 CGLayer 中呈现您自己的文本布局,并有效地创建您自己的 UILabel 等效项。这很难!
3) 修复字体文件,使基线、上升和下降都正确。
在 WWDC 两年零一周后,我终于弄明白到底是怎么回事了。
至于为什么 UILabel
会这样:看起来 Apple 试图将 ascender and descender 之间的任何内容居中。然而,实际的文本绘制引擎总是将基线对齐到像素边界。如果在计算绘制文本的矩形时不注意这一点,基线可能会看似随意地上下跳跃。
主要问题是找到 Apple 放置基线的位置。 AutoLayout 提供了一个 firstBaselineAnchor,但遗憾的是无法从代码中读取它的值。只要标签的高度四舍五入为像素,以下代码片段似乎可以正确预测所有屏幕 scale
s 和 contentScaleFactor
s 的基线。
extension UILabel {
public var predictedBaselineOffset: CGFloat {
let scale = self.window?.screen.scale ?? UIScreen.main.scale
let availablePixels = round(self.bounds.height * scale)
let preferredPixels = ceil(self.font.lineHeight * scale)
let ascenderPixels = round(self.font.ascender * scale)
let offsetPixels = ceil((availablePixels - preferredPixels) / 2)
return (ascenderPixels + offsetPixels) / scale
}
}
当标签的高度没有四舍五入到像素时,预测的基线往往会偏离一个像素,例如对于 2x 设备上 40.1pt 容器中的 13.0pt 字体或 3x 设备上 40.1pt 容器中的 16.0pt 字体。
请注意,我们必须在此处以像素级别工作。使用点(即立即除以屏幕比例而不是在最后除以)会导致@3x 屏幕上由于浮点不准确而出现差一错误。
例如,将 47px (15.667pt) 的内容置于 121px 容器 (40.333pt) 的中心,我们得到 12.333pt 的偏移量,由于浮点错误,该偏移量上限为 12.667pt。
为了回答我最初提出的问题,我们可以使用(希望是正确的)预测基线实现 -drawTextInRect:
,以产生与 UILabel
相同的结果。
public class AppleImitatingLabel: UILabel {
public override func drawText(in rect: CGRect) {
let offset = self.predictedBaselineOffset
let frame = rect.offsetBy(dx: 0, dy: offset)
self.attributedText!.draw(with: frame, options: [], context: nil)
}
}
请参阅 project on GitHub 了解工作实施。
对于我实际想要实现的目标存在很多困惑。所以请让我重述我的问题。很简单:
How does Apple center text inside
UILabel
's-drawTextInRect:
?
以下是我寻找 Apple 居中文本方法的一些背景信息:
- 我不满意他们在此方法中将文本居中的方式
- 我有自己的算法,可以按照我想要的方式完成工作
但是,有些地方我无法将此算法注入到其中,即使使用方法调配也是如此。了解他们使用的确切算法(或等效算法)将解决我个人的问题。
这里有一个 demo project 您可以试验一下。如果您对我的 Apple 实现问题感兴趣,请查看左侧(基本 UILabel):当您使用滑块增加字体大小时,文本会上下跳动,而不是逐渐向下移动。
同样,我要寻找的是 -drawTextInRect:
的实现,它与基 UILabel
完全相同——但不调用 super
.
- (void)drawTextInRect:(CGRect)rect
{
CGRect const centeredRect = ...;
[self.attributedText drawInRect:centeredRect];
}
我没有答案,但我做了一些研究并认为我会分享它。我使用 Xtrace,它可以让您跟踪 Objective-C 方法调用,以尝试找出 UILabel 在做什么。我将 Xtrace git 存储库中的 Xtrace.h
和 Xtrace.mm
添加到 Christian 的 UILabel 演示项目中。在 RootViewController.m
中,我添加了 #import "Xtrace.h"
,然后尝试了各种跟踪过滤器。将以下内容添加到 RootViewController 的 loadView
:
[Xtrace includeMethods:@"drawWithRect|drawInRect|drawTextInRect"];
[Xtrace traceClassPattern:@"String|UILabel|AppleLabel" excluding:nil];
给我这个输出:
| [<AppleLabel 0x7fedf141b520> drawTextInRect:{{0, 0}, {187.5, 333.5}}]
| [<NSConcreteMutableAttributedString 0x7fedf160ec30>/NSAttributedString drawInRect:{{0, 156.5}, {187.5, 333.5}}]
| [<UILabel 0x7fedf1418dc0> drawTextInRect:{{0, 0}, {187.5, 333.5}}]
| [<UILabel 0x7fedf1418dc0> _drawTextInRect:{{0, 0}, {187.5, 333.5}} baselineCalculationOnly:0]
| [<__NSCFConstantString 0x1051d1db0>/NSString drawWithRect:{{0, 156.3134765625}, {187.5, 20.287109375}} options:1L attributes:<__NSDictionaryM 0x7fedf141ae00> context:<NSStringDrawingContext 0x7fedf15503c0>]
(我 运行 使用 Xcode 7.0 beta,iOS 9.0 模拟器。)
我们可以看到,Apple 的实现不会将 drawInRect:
发送到 NSAttributedString,而是将 drawWithRect:options:attributes:
发送到 NSString。我猜这些最终会做同样的事情。
我们还可以准确地看到计算出的 Y 偏移量是多少。在滑块的这个初始位置,它是 156.3134765625。有趣的是,它不是落在某个四舍五入的整数值或 0.25 的倍数或类似值上的东西,否则这将是对跳跃的解释。
我们还看到 UILabel 发送 20.287109375 作为高度,这与 self.attributedText.size.height
计算的值相同。所以它似乎正在使用它,尽管我无法追踪那个调用(也许它是直接使用 Core Text?)。
这就是我被困的地方。一直在尝试查看 UILabel 计算的内容与明显的公式 "labelYOffset = (boundsHeight - labelHeight) / 2.0" 之间的区别,但我发现没有一致的模式。
更新 1. 所以我们可以将这个难以捉摸的 Y 偏移建模为
labelYOffset = (boundsHeight - labelHeight) / 2.0 + error
error
是什么,正是我们想要弄清楚的。让我们试着像科学家一样研究这个。 Here is Xtrace output when I've dragged the slide to the end and then to the beginning. I also added an NSLog at each change of the slider which helps with separating the entries. I wrote a Python script 根据标签高度绘制误差。
有意思。我们看到误差有一些线性,但如果您理解我的意思,它们会在某些点奇怪地跳到线之间。这里发生了什么?在解决这个问题之前,我会失眠。 :)
我在自己制作的贴图应用中经常处理这个问题。
我认为问题可能不是 UILabel 的问题,而是包含上升和下降的字体。我的假设是 UILabel 在定位文本正文时会考虑这些值,这对明显的垂直位置有影响。很多字体的问题是这些值在 TTF / OTF 文件中完全搞砸了,所以你得到例如使用 .sizeToFit()、重叠线、不稳定的垂直位置等剪裁下降部
有以下三种方法可以解决这个问题:
1) 使用 NSAttributedString 和更多带有 NSBaselineOffsetAttributeName 的基线。 TTTAttributedLabel 是一个很好的快捷方式。
2) 使用 CoreText 和 CoreGraphics 在自定义 UIView 子类中的 CGLayer 中呈现您自己的文本布局,并有效地创建您自己的 UILabel 等效项。这很难!
3) 修复字体文件,使基线、上升和下降都正确。
在 WWDC 两年零一周后,我终于弄明白到底是怎么回事了。
至于为什么 UILabel
会这样:看起来 Apple 试图将 ascender and descender 之间的任何内容居中。然而,实际的文本绘制引擎总是将基线对齐到像素边界。如果在计算绘制文本的矩形时不注意这一点,基线可能会看似随意地上下跳跃。
主要问题是找到 Apple 放置基线的位置。 AutoLayout 提供了一个 firstBaselineAnchor,但遗憾的是无法从代码中读取它的值。只要标签的高度四舍五入为像素,以下代码片段似乎可以正确预测所有屏幕 scale
s 和 contentScaleFactor
s 的基线。
extension UILabel {
public var predictedBaselineOffset: CGFloat {
let scale = self.window?.screen.scale ?? UIScreen.main.scale
let availablePixels = round(self.bounds.height * scale)
let preferredPixels = ceil(self.font.lineHeight * scale)
let ascenderPixels = round(self.font.ascender * scale)
let offsetPixels = ceil((availablePixels - preferredPixels) / 2)
return (ascenderPixels + offsetPixels) / scale
}
}
当标签的高度没有四舍五入到像素时,预测的基线往往会偏离一个像素,例如对于 2x 设备上 40.1pt 容器中的 13.0pt 字体或 3x 设备上 40.1pt 容器中的 16.0pt 字体。
请注意,我们必须在此处以像素级别工作。使用点(即立即除以屏幕比例而不是在最后除以)会导致@3x 屏幕上由于浮点不准确而出现差一错误。
例如,将 47px (15.667pt) 的内容置于 121px 容器 (40.333pt) 的中心,我们得到 12.333pt 的偏移量,由于浮点错误,该偏移量上限为 12.667pt。
为了回答我最初提出的问题,我们可以使用(希望是正确的)预测基线实现 -drawTextInRect:
,以产生与 UILabel
相同的结果。
public class AppleImitatingLabel: UILabel {
public override func drawText(in rect: CGRect) {
let offset = self.predictedBaselineOffset
let frame = rect.offsetBy(dx: 0, dy: offset)
self.attributedText!.draw(with: frame, options: [], context: nil)
}
}
请参阅 project on GitHub 了解工作实施。