对于我实际想要实现的目标存在很多困惑。所以请让我重新提出我的问题。它很简单:
Apple如何在
UILabel
的{{1}}内找到文字?
以下是我为什么要寻找Apple中心文字方法的背景信息:
然而,有些地方我不能注入这个算法,甚至没有方法调整。了解他们使用的确切算法(或相当于该算法)将解决我的个人问题。
这是您可以试验的demo project。如果您对Apple的实现问题感兴趣,请查看左侧(基本UILabel):文本在使用滑块增加字体大小时上下跳动,而不是逐渐向下移动。
同样,我正在寻找的是-drawTextInRect:
的实现与基础-drawTextInRect:
完全相同 - 但不调用UILabel
。
super
答案 0 :(得分:8)
我没有答案,但我做了一些研究,并认为我会分享它。我使用了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
是什么,是我们想要弄清楚的。让我们试着像科学家一样研究这个问题。当我将幻灯片拖到结尾然后到开头时,Here是Xtrace输出。我还在每次更改滑块时添加了一个NSLog,这有助于分隔条目。我写了a Python script来根据标签高度绘制错误。
有趣。我们看到错误有一些线性,但是如果你理解我的意思,它们会在某些点之间奇怪地跳线。这里发生了什么?在这个问题解决之前我会失眠。 :)
答案 1 :(得分:2)
在WWDC工作了两年零一周后,我终于弄明白到底是怎么回事。
至于UILabel
行为方式的原因:看起来Apple试图将ascender and descender之间的任何谎言置于中心位置。但是,实际的文本绘制引擎始终将基线捕捉到像素边界。如果在计算绘制文本的矩形时没有注意到这一点,那么基线可能会随意上下跳动。
主要问题是找到Apple放置基线的位置。 AutoLayout提供了firstBaselineAnchor,但遗憾的是,它的值不能从代码中读取。只要标签的高度四舍五入为像素,以下代码段就会在所有屏幕scale
和contentScaleFactor
处正确预测基线。
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了解有效的实施方案。
答案 2 :(得分:-2)
我在一个贴心的应用程序中处理了很多这个问题。
我认为UILabel的问题可能与包括上升器和下降器在内的字体不同。我的假设是UILabel在定位文本正文时会考虑这些值,这会对明显的垂直位置产生影响。大量字体的问题在于这些值完全搞砸了TTF / OTF文件,因此您可以获得使用.sizeToFit(),重叠线条,不稳定的垂直位置等剪切下降器。
有三种方法可以解决这个问题:
1)使用NSAttributedString和更多基线与NSBaselineOffsetAttributeName。 TTTAttributedLabel是一个很好的捷径。
2)使用CoreText和CoreGraphics在自定义UIView子类中的CGLayer中呈现自己的文本布局,并有效地创建自己的UILabel等效项。这很难!
3)修复字体文件,使基线,上升和下降都正确。