【问题标题】:How does UILabel vertically center its text?UILabel 如何垂直居中其文本?
【发布时间】:2015-08-25 21:32:38
【问题描述】:

对于我实际想要实现的目标,存在很多困惑。所以请让我改写我的问题。很简单:

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


以下是关于我为什么要寻找 Apple 的文本居中方法的一些背景信息:

  • 我不满意他们如何在此方法中居中文本
  • 我有自己的算法,可以按照我想要的方式完成工作

但是,有些地方我无法将这个算法注入其中,即使是方法调配也不行。了解他们使用的确切算法(或等效算法)将解决我的个人问题。


这是您可以尝试的demo project。如果您对我对 Apple 实现的问题感兴趣,请查看左侧(基本 UILabel):当您使用滑块增加字体大小时,文本会上下跳跃,而不是逐渐向下移动。

同样,我所寻找的只是-drawTextInRect: 的实现,它与基本UILabel 完全相同——但不调用super

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

    [self.attributedText drawInRect:centeredRect];
}

【问题讨论】:

  • 对不起,我没抓住重点!刚刚删除了我的帖子
  • 您的意思是要更改文本边界吗?或者你真的想实现-drawTextInRect?似乎是 2 个不同的问题。
  • 你能举一个在 UILabel 中没有垂直居中的文本的例子吗?我假设您正在考虑后代?
  • @ChristianSchnorr 这有点重,但您可以调整UILabels 默认drawTextInRect: 并为所有UILabels 添加一个自己的属性,该属性定义是使用您自己的实现还是默认实现.这样您就可以在任何UILabel 实例中使用自己的绘图,例如通过使用label.useCustomDrawing = YES;。 -- 你也可以创建一个UILabel 的简单子类(它不能有任何存储属性),然后在运行时替换你想要修改的标签的类(并且不能自己实例化):object_setClass(evilLabel, MyCoolLabel.class);跨度>
  • @ChristianSchnorr 如果您认为这个问题已经得到了充分的解释,那么让它运行并享受您得到的答案。现在,除其他外,我正在查看一个问题:“很长一段时间以来,我一直对各种 Apple 视图中的文本没有正确垂直居中这一事实感到恼火。”还有一条评论说“你对我的帖子所做的编辑只会让人们更加困惑,因为这意味着我关心图像的居中,这又是我不关心。”我建议你真的没有你想象的那么清楚。

标签: ios objective-c iphone uilabel typography


【解决方案1】:

我没有答案,但我做了一些研究并认为我会分享它。我使用了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 模拟器运行它。)

我们可以看到,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 是什么,是我们想要弄清楚的。让我们试着像科学家一样研究这个。 Here 是我将幻灯片拖到末尾然后拖到开头时的 Xtrace 输出。我还在滑块的每次更改处添加了一个 NSLog,这有助于分离条目。我写了a Python script 来根据标签高度绘制错误。

有趣。我们看到错误有一些线性关系,但如果你明白我的意思,它们会在某些点奇怪地在线条之间跳跃。这里发生了什么?在解决这个问题之前,我会失眠。 :)

【讨论】:

  • 嗯 - 你没有真的回答我的问题。但是你 a) 教会了我一些新的东西 b) 给了我一种方法,至少可以确定 Apple 使用的是哪个 y 值。 -- 我会尝试找出一个合适的公式来计算这个值。赏金是你的,但我不会接受你的回答,因为它没有回答我的问题。如果我找到匹配的公式,我会及时通知您。
  • 哇,谢谢! :) 感谢您发布一个有趣的问题,非常神秘。我对没有破解它很不满意,并且很想更新,实际上我自己可能还有另一个尝试。正在考虑我可以更彻底地分析已知变量之间的关系,做一些绘图。
【解决方案2】:

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

至于为什么UILabel 的行为方式是这样的:看起来Apple 试图将ascender and descender 之间的任何内容居中。但是,实际的文本绘制引擎总是将基线捕捉到像素边界。如果在计算绘制文本的矩形时不注意这一点,基线可能会看似随意地上下跳跃。


主要问题是找到 Apple 放置基线的位置。 AutoLayout 提供了 firstBaselineAnchor,但遗憾的是无法从代码中读取它的值。只要标签的高度四舍五入到像素,以下 sn-p 似乎可以正确预测所有屏幕 scales 和 contentScaleFactors 的基线。

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 屏幕上出现错误。

例如,在 121px 容器 (40.333pt) 中将 47px (15.667pt) 内容居中会给我们一个 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 了解有效的实施。

【讨论】:

  • @TomvanZummeren 是的,我没有玩过这些。我相信同样的逻辑也适用:您只需要调整当前的 self.font.lineHeight 即可考虑绘制多条线。
【解决方案3】:

我在自己制作的贴纸应用中处理了很多。

我认为 UILabel 的问题可能不在于字体,包括升序和降序。我的假设是 UILabel 在定位对明显垂直位置有影响的文本正文时会考虑这些值。很多字体的问题是这些值在 TTF / OTF 文件中完全搞砸了,所以你得到例如使用 .sizeToFit()、重叠线、不稳定的垂直位置等对下降器进行剪裁。

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

1) 使用 NSAttributedString 和更多带有 NSBaselineOffsetAttributeName 的基线。 TTTAttributedLabel 是一个很好的捷径。

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

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

【讨论】:

  • 你误解了我的问题。 Apple 是否能很好地使文本居中是无关紧要的。我只想了解他们的算法。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-10-15
  • 1970-01-01
  • 2011-07-23
  • 1970-01-01
  • 2015-09-16
相关资源
最近更新 更多