为啥 iOS 自动布局会导致预 Retina 显示器出现明显的舍入错误(包括单元测试)

Posted

技术标签:

【中文标题】为啥 iOS 自动布局会导致预 Retina 显示器出现明显的舍入错误(包括单元测试)【英文标题】:Why does iOS auto layout lead to apparent rounding errors on pre-Retina displays (unit test included)为什么 iOS 自动布局会导致预 Retina 显示器出现明显的舍入错误(包括单元测试) 【发布时间】:2013-10-13 19:03:34 【问题描述】:

我目前很难理解为什么以下单元测试在 iPad 2 上失败。相对于所需的精确居中,自动布局似乎稍微(0.5 分)在 superview 内错误定位 superview受两个布局约束。似乎特别奇怪的是,关键测试(但最后一个断言)在 iPhone 5 上通过,因此明显的舍入误差只影响一个(ios 6)平台。这是怎么回事?

更新 1 我已更改代码以确保即使 translatesAutoresizingMaskIntoConstraintsNO,两个框架在宽度和高度方面都受到充分限制,建议作为可能的相关补救措施here。但是,这显然不会改变这种情况。

#import "BugTests.h"

@implementation BugTests

- (void)testCenteredLayout 
    UIView *superview = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 768, 88)];
    superview.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin;
    superview.translatesAutoresizingMaskIntoConstraints = YES;

    UILabel *view = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 0, 0)];
    view.text = @"Single Round against iPad.";
    view.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin;
    view.translatesAutoresizingMaskIntoConstraints = NO;
    [view addConstraint:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeWidth  relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:0.0 constant:206.0]];
    [view addConstraint:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:0.0 constant: 21.0]];

    [superview addSubview:view];

    [superview addConstraint:[NSLayoutConstraint constraintWithItem:superview attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0.0]];
    [superview addConstraint:[NSLayoutConstraint constraintWithItem:superview attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:view attribute:NSLayoutAttributeCenterY multiplier:1.0 constant:0.0]];

    STAssertEquals(superview.center, CGPointMake(384, 44), nil); // succeeds
    STAssertEquals(view.center,      CGPointMake(  0,  0), nil); // succeeds

    [superview setNeedsLayout];
    [superview layoutIfNeeded];

    STAssertTrue(!superview.hasAmbiguousLayout, nil);

    STAssertEquals(superview.frame.size, CGSizeMake(768, 88), nil); // succeeds
    STAssertEquals(view.frame.size,      CGSizeMake(206, 21), nil); // succeeds

    STAssertEquals(superview.center, CGPointMake(384, 44), nil); // succeeds

    STAssertEquals(superview.center, view.center,            nil); // fails: why?
    STAssertEquals(view.center,      CGPointMake(384, 44.5), nil); // succeeds: why?


@end

更新 2 我在第二个单元测试中隔离了另一个(显然)相同问题的实例。这一次它涉及一个顶部(不是中心)约束,而这一次小数点坐标似乎是触发器。 (测试在 Retina 之前的设备上也成功,例如使用 y = 951,即奇点坐标。)我检查了各种模拟器配置(在我的物理 iPad 2 和 iPhone 5 旁边)发生确实似乎与没有拉蒂娜显示器。 (再次感谢@ArkadiuszHolko 的领导。)

我目前从这些测试中的感觉是,如果需要在前 Retina 显示器上进行精确点自动布局,则必须避免奇数高度和分数 y 坐标。但为什么呢?

- (void)testNonRetinaAutoLayoutProblem2 
    UIView *superview = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 768, 1004)];
    superview.autoresizingMask = UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin;
    superview.translatesAutoresizingMaskIntoConstraints = YES;

    CGFloat y = 950.5; // see e.g. pageControlTopConstraint

    UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 0)];
    view.translatesAutoresizingMaskIntoConstraints = NO;
    [superview addConstraint:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeLeading  relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeLeading        multiplier:1.0 constant:0.0]];
    [superview addConstraint:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeTrailing       multiplier:1.0 constant:0.0]];
    [superview addConstraint:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeTop      relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeTop            multiplier:1.0 constant:y]];
    [superview addConstraint:[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeHeight   relatedBy:NSLayoutRelationEqual toItem:nil       attribute:NSLayoutAttributeNotAnAttribute multiplier:0.0 constant:8]];

    [superview addSubview:view];

    [superview setNeedsLayout];
    [superview layoutIfNeeded];

    STAssertTrue(!superview.hasAmbiguousLayout, nil);
    STAssertTrue(!view.hasAmbiguousLayout,      nil);

    STAssertEquals(superview.frame, CGRectMake(0, 0,       768, 1004), nil); // succeeds
    STAssertEquals(view.frame,      CGRectMake(0, y,       768,    8), nil); // fails: why?
    STAssertEquals(view.frame,      CGRectMake(0, y + 0.5, 768,    8), nil); // succeeds: why?

【问题讨论】:

它是否只发生在 iOS 6 上的 iPad 2 上? @ArkadiuszHolko 该问题出现在运行 iOS 6.1.3 的 iPad 2 上。它不会出现在运行 iOS 6.1.4 的 iPhone 5 上。这些是最新的 iOS 6 版本。我目前没有办法在 iOS 7 上进行测试。我还能尝试什么? 您能看到将view 的高度从21 更改为22 时会发生什么吗? iPhone 和 iPad 现在的结果是否一致? iPad 没有视网膜显示屏,这可能是造成差异的原因。 如果我将 view 的高度从 21 更改为 22,则问题不再出现。 (感谢您的建议!)问题在于,与我隔离问题的简单单元测试不同,在实际应用程序中,其中一些坐标不是简单设置的。在我更好地理解根本原因的性质和有效性之前,我赶紧修改所有这些计算。正如您的评论所暗示的那样,也许这与奇点坐标、自动布局和预视网膜 iPad 显示器的组合有关。 【参考方案1】:

您所展示的是自动布局讨厌未对齐的视图。在非视网膜设备上,最近的 pixel 是最近的 point,因此它会四舍五入为整数。在视网膜屏幕上,最近的 像素 是最近的 半点,因此它四舍五入到最接近的 0.5。您可以通过将第二个测试中的 y 更改为 950.25 并注意 view.frame 保持 0, 950.5, 768, 8 来证明这一点(而不是更改为 0, 950.25, 768, 8 )。

(只是为了证明它是四舍五入而不是ceiling,如果将 y 更改为 950.2 view.frame 将变为 0, 950, 768, 8。)

【讨论】:

因此,我认为视图必须与整个像素坐标完全对齐的隐式约束比所有显式约束具有更高的优先级...

以上是关于为啥 iOS 自动布局会导致预 Retina 显示器出现明显的舍入错误(包括单元测试)的主要内容,如果未能解决你的问题,请参考以下文章

为啥关闭 translatesAutoResizingMasks 会导致自动布局崩溃,抱怨我需要调用 [super layoutSubviews]?

iOS:多行 uilabel 仅显示带有自动布局的一行

自动布局Autoresizing与Autolayout

为啥我的 superview-with-subview 在模态显示(使用自动布局)时会缩小?

预填充Google地图自动填充功能(iOS / Swift)

为啥 Ios 会忽略它下面的顶部栏和布局项?