UILabel如何垂直居中文本?

21

关于我的实际目的,有很多人感到困惑。请让我重新表述我的问题,它很简单:

  

如何在UILabel-drawTextInRect:中居中文本?


以下是一些关于为什么我正在寻找苹果公司居中文本方法的背景:

  • 我不满意他们在这个方法中居中文本的方式。
  • 我有自己的算法可以解决这个问题。

但是,在某些地方,我无法插入此算法,即使使用方法交换也不行。了解他们使用的确切算法(或等价算法)将解决我的个人问题。


这里是一个演示项目,您可以进行实验。如果您想知道我对苹果公司实现的问题,请看左侧(基本UILabel):随着您使用滑块增加字体大小,文本会上下跳动,而不是逐渐向下移动。

再次说明,我要找的只是一个实现-drawTextInRect:与基本UILabel完全相同的方法,但不调用super

- (void)drawTextInRect:(CGRect)rect
{
    CGRect const centeredRect = ...;

    [self.attributedText drawInRect:centeredRect];
}

1
抱歉,我误解了!我刚刚删除了我的帖子。 - Alex Cio
因为我正在改变渲染方式,所以我必须撤销之前苹果所做的操作,因此我正试图理解他们在 UILabel 中犯了什么错误。 - Christian Schnorr
1
你能举个UILabel中文本垂直不居中的例子吗?我假设你考虑了下行。 - user3821934
1
@ChristianSchnorr 这可能有点繁琐,但你可以调用 UILabel 的默认 drawTextInRect: 并添加一个自定义属性到所有的 UILabel 中,以便定义是否使用你自己的实现还是默认实现。这样你就可以在任何 UILabel 实例中使用自己的绘制方式,例如:通过使用 label.useCustomDrawing = YES;。-- 你也可以创建一个简单的 UILabel 子类(不需要存储属性),然后在运行时替换要修改的标签的类(无法自己实例化):object_setClass(evilLabel, MyCoolLabel.class); - fluidsonic
3
如果你认为问题已经足够清晰地解释了,那就让它继续运行并享受你得到的答案。目前,我正在看一些问题,其中有一个说“相当长的一段时间以来,我一直对各种苹果视图中的文本没有垂直居中感到恼火”,还有一个评论说:“另外,你编辑我的帖子只会让人更加困惑,因为它暗示我关心图像的中心对齐,而实际上,我并不关心。”我建议你并不像你自认为的那样清楚明确。 - Tommy
显示剩余35条评论
3个回答

8

我没有答案,但是我做了一些研究并想分享一下。我使用Xtrace,它可以让您跟踪Objective-C方法调用,尝试弄清楚UILabel正在做什么。我从Xtrace git存储库中添加了Xtrace.hXtrace.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模拟器运行此程序。

我们可以看到,苹果的实现并没有将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是什么。让我们像科学家一样研究它。这里是我将滑块拖到最后,然后拖回开头时的Xtrace输出。我还在每次滑块变化时添加了一个NSLog,这有助于分离条目。我编写了一个Python脚本来绘制标签高度与误差之间的关系。

Plot or UILabel error in vertical centering against label height

有趣。我们发现错误存在一定的线性关系,但在某些点上它们会奇怪地跳跃到其他行,如果你明白我的意思的话。这是怎么回事?我会失眠直到解决这个问题。 :)


好吧 - 你并没有真正回答我的问题。但是,a)你教会了我一些新东西,b)你给了我一个至少可以确定苹果公司使用哪个y值的方法。--我将尝试找出一个适当的公式来计算这个值。赏金归你所有,但我不会接受你的答案,因为它没有回答我的问题。如果我找到了匹配的公式,我会让你知道更新情况。 - Christian Schnorr
哇,谢谢! :) 谢谢您发布一个有趣的问题,真是个谜。我对自己没有破解它感到不满,并且很想得到更新,我可能会再试一次。我在考虑可以更彻底地分析已知变量之间的关系,做一些绘图。 - skagedal

2

在 WWDC 两年一周之后,我终于弄清楚到底发生了什么。

至于为什么 UILabel 的行为是这样的:看起来 Apple 尝试让位于 上升和下降 之间的内容居中。然而,实际的文本绘制引擎总是将基线捕捉到像素边界。如果在计算要绘制文本的矩形时不注意这一点,基线可能会看似任意地上下跳动。


主要问题在于找到 Apple 放置基线的位置。AutoLayout 提供了 firstBaselineAnchor,但是它的值不幸地无法从代码中读取。以下代码段似乎能够正确预测所有屏幕 scalecontentScaleFactor 下标签的基线,只要标签的高度被舍入到像素上。

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
    }
}

当标签的高度没有被舍入到像素时,预测的基线往往会偏离一个像素,例如,在2倍设备上的40.1pt容器中使用13.0pt字体或在3倍设备上的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)
    }
}

请查看GitHub上的项目以获取可用的实现。


@TomvanZummeren 这是真的,我还没有尝试过那些。但我相信相同的逻辑也适用:您只需要调整当前的 self.font.lineHeight 来考虑绘制多行即可。 - Christian Schnorr

-2

我在一个贴纸应用程序中经常处理这个问题。

我认为问题可能不是UILabel本身,而是字体包括上升和下降的高度。我的假设是,当UILabel定位文本主体时,它会考虑这些值,这对外观垂直位置有影响。许多字体的问题在于,这些值在TTF / OTF文件中完全混乱,因此您会遇到例如使用.sizeToFit()时下降线被剪切、重叠行、垂直位置不正确等问题。

有三种方法可以解决这个问题:

1)使用NSAttributedString并使用NSBaselineOffsetAttributeName更改基线。TTTAttributedLabel是一个好的快捷方式。

2)使用CoreText和CoreGraphics在自定义UIView子类中的CGLayer中呈现自己的文本布局,并有效地创建自己的UILabel等效项。这很难!

3)修复字体文件,使基线、上升和下降线都正确。


你误解了我的问题。苹果公司是否能够很好地居中文本并不重要,我只想了解他们的算法。 - Christian Schnorr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接