【问题标题】:Auto layout and centering a view in between two other views自动布局和在其他两个视图之间居中视图
【发布时间】:2013-06-24 09:58:09
【问题描述】:

所以我有两个矩形视图和一个将它们连接在一起的视图。见图http://cl.ly/image/340Q1l381b3L

是否可以限制红色视图使其保持在绿色视图垂直范围内?我想让它保持在它们重叠区域的中心。

虽然这不一定保持居中,但我认为我可以将以下两个约束应用于绿色视图和红色视图,以至少将红色视图绑定到绿色视图。

[NSLayoutConstraint
 constraintWithItem:redView
 attribute:NSLayoutAttributeCenterY
 relatedBy:NSLayoutRelationGreaterThanOrEqual
 toItem:greenView
 attribute:NSLayoutAttributeBottom
 multiplier:1.0f
 constant:0.0f];

[NSLayoutConstraint
 constraintWithItem:redView
 attribute:NSLayoutAttributeCenterY
 relatedBy:NSLayoutRelationLessThanOrEqual
 toItem:greenView
 attribute:NSLayoutAttributeTop
 multiplier:1.0f
 constant:0.0f];

但这会导致

2013-06-26 22:13:27.493 MiniMeasure[25896:303] 无法同时满足约束: ( "NSLayoutConstraint:0x1002cebf0 RedView:0x1002cdf90.centerY >= GreenView:0x1018a34c0.bottom", "NSLayoutConstraint:0x1002cf720 RedView:0x1002cdf90.centerY

很明显,它不喜欢其中一个绿色视图有高度限制并试图打破它。但我也需要绿色视图来保持它们的大小。两个绿色视图都有宽度和高度限制。

有什么想法/建议吗?

谢谢!

【问题讨论】:

  • 你能在你期望的地方买到绿色的吗?他们会动吗?
  • 是的。绿色的可以仅使用约束来移动和调整大小。这是我无法约束的红色视图。
  • 我的意思是他们不总是在相同的相对位置上吃饭?如果是这样,它可能会很复杂,但我可以想到一些解决方案。
  • 哦,对了。正确的。绿色视图旨在以任何方式重新定位。只有当两个绿色视图有重叠区域时,红色视图才可见。但是当它可见时,我需要对其进行约束,使其位于重叠区域的中间(如果有意义的话)。 :) 这些显示了绿色视图被移动后的样子。 cl.ly/image/2G2s040O0N0Qcl.ly/image/1K2T1Y0g0226

标签: cocoa nsview autolayout


【解决方案1】:

基于 cmets,一种解决方案是不对红色矩形使用自动布局,而是在超级视图或与超级视图具有相同帧矩形的子视图中简单地使用 drawRect。

首先使用 KVO 来通知我们边界的绿色矩形框的更改(根据您的实现而定)

改变了?

如果您在两个绿色矩形上使用 NSUnionRect,则一些 if 语句可以告诉您是否显示红色矩形,并提供绘制或放置矩形的逻辑。

如果联合矩形在一个方向上大于绿色矩形的总和而在另一个方向上更小,则绘制一个红色矩形。

可以通过比较它们的边缘来绘制哪里。

伪代码 如果 unionRect.size.x > (greenRect1.size.x + greenRect2.size.x) AND unionRect.size.y

您可以使用相同的方法找出红色矩形相对于绿色矩形的约束条件,但这实际上会增加复杂性。

由于工作量和复杂性以及潜在的含义混淆,我不相信所有内容都属于自动布局。

【讨论】:

  • 所以我目前确实有一个可行的解决方案,只需将框架设置在红色矩形上,但我的整个应用程序都是自动布局的,而且感觉很脏。我只是想知道我是否遗漏了什么,但我认为你是对的......在这种情况下使用自动布局只会增加复杂性。感谢您的反馈!
  • 但是请看一下 Rob 的帖子。很有启发性。
  • 但我认为诀窍在于实用主义。 Apple 正在推动自动布局并忽略这并不总是最好的解决方案。我也在学习。
【解决方案2】:

您可以通过自动布局获得所需的内容。示范:

但是,如果绿色视图没有任何垂直重叠,则结果可能不是您想要的:

那么,我是怎么做到的呢?

首先,有五个视图。有窗口的内容视图、两个可拖动的绿色视图、文本字段(“中心”)和棕褐色间隔视图。可拖动视图、文本字段和分隔符都是窗口内容视图的直接子视图。特别是,文本字段不是间隔视图的子视图。

其次,我需要设置一些约束的优先级,所以我定义了一个辅助函数:

static NSLayoutConstraint *constraintWithPriority(NSLayoutConstraint *constraint,
    NSLayoutPriority priority)
{
    constraint.priority = priority;
    return constraint;
}

接下来我创建左侧可拖动视图。我为其 X 位置和宽度、高度和 Y 位置设置了约束。

- (void)createLeftView {
    leftView = [[DraggableView alloc] init];
    leftView.translatesAutoresizingMaskIntoConstraints = NO;
    [rootView addSubview:leftView];

    NSDictionary *views = NSDictionaryOfVariableBindings(leftView);
    [rootView addConstraints:[NSLayoutConstraint
        constraintsWithVisualFormat:@"H:|-(20)-[leftView(80)]"
        options:0 metrics:nil views:views]];
    [leftView addConstraints:[NSLayoutConstraint
        constraintsWithVisualFormat:@"V:[leftView(120)]"
        options:0 metrics:nil views:views]];
    leftView.yConstraint = [NSLayoutConstraint
        constraintWithItem:leftView attribute:NSLayoutAttributeTop
        relatedBy:NSLayoutRelationEqual
        toItem:rootView attribute:NSLayoutAttributeTop
        multiplier:1 constant:20];
    [rootView addConstraint:leftView.yConstraint];
}

请注意,我的DraggableView 类通过调整其yConstraint 的常量来处理鼠标拖动。由于我需要访问 Top 约束,因此我直接设置了该约束,而不是使用可视格式。

我非常相似地创建了正确的可拖动视图,除了它被锚定到根视图的后缘。

- (void)createRightView {
    rightView = [[DraggableView alloc] init];
    rightView.translatesAutoresizingMaskIntoConstraints = NO;
    [rootView addSubview:rightView];

    NSDictionary *views = NSDictionaryOfVariableBindings(rightView);
    [rootView addConstraints:[NSLayoutConstraint
        constraintsWithVisualFormat:@"H:[rightView(80)]-(20)-|"
        options:0 metrics:nil views:views]];
    [rightView addConstraints:[NSLayoutConstraint
        constraintsWithVisualFormat:@"V:[rightView(120)]"
        options:0 metrics:nil views:views]];
    rightView.yConstraint = [NSLayoutConstraint
        constraintWithItem:rightView attribute:NSLayoutAttributeTop
        relatedBy:NSLayoutRelationEqual
        toItem:rootView attribute:NSLayoutAttributeTop
        multiplier:1 constant:20];
    [rootView addConstraint:rightView.yConstraint];
}

现在是棘手的部分。我创建了一个额外的视图,我称之为垫片。我已经让视图可见,以便更容易理解这个演示是如何工作的。通常你会把垫片隐藏起来;隐藏视图仍然参与布局。

- (void)createSpacerView {
    spacerView = [[SpacerView alloc] init];
    spacerView.translatesAutoresizingMaskIntoConstraints = NO;
    [rootView addSubview:spacerView];

垫片的水平约束很简单。分隔符的前缘被固定到左侧可拖动视图的后缘,而分隔符的后缘被固定到右侧可拖动视图的前缘。因此,spacer 总是准确地跨越可拖动(绿色)视图之间的水平间隙。

    NSDictionary *views = NSDictionaryOfVariableBindings(leftView, rightView, spacerView);
    [rootView addConstraints:[NSLayoutConstraint
        constraintsWithVisualFormat:@"H:[leftView][spacerView][rightView]"
        options:0 metrics:nil views:views]];

接下来,我们将 spacer 的顶部和底部边缘约束为等于根视图的顶部和底部边缘,但优先级为 1(优先级极低):

    [rootView addConstraints:[NSLayoutConstraint
        constraintsWithVisualFormat:@"V:|-(0@1)-[spacerView]-(0@1)-|"
        options:0 metrics:nil views:views]];

然后我们进一步限制 spacer 的上边缘大于或等于两个可拖动视图的上边缘的上边缘,优先级为 2(几乎为极低):

    [rootView addConstraint:constraintWithPriority([NSLayoutConstraint
        constraintWithItem:spacerView attribute:NSLayoutAttributeTop
        relatedBy:NSLayoutRelationGreaterThanOrEqual
        toItem:leftView attribute:NSLayoutAttributeTop
        multiplier:1 constant:0], 2)];
    [rootView addConstraint:constraintWithPriority([NSLayoutConstraint
        constraintWithItem:spacerView attribute:NSLayoutAttributeTop
        relatedBy:NSLayoutRelationGreaterThanOrEqual
        toItem:rightView attribute:NSLayoutAttributeTop multiplier:1 constant:0], 2)];

所以垫片的上边缘有三个约束:

  • spacerView.top == rootView.top 优先级为 1
  • spacerView.top >= leftView.top,优先级为 2
  • spacerView.top >= rightView.top 优先级为 2

这些约束结合起来,使垫片的顶部边缘尽可能高,同时不高于两个可拖动视图的顶部边缘。因此,间隔顶部边缘将等于可拖动视图的顶部边缘的下部。

我们需要在这里使用低优先级,因为自动布局将拒绝使间隔的高度为负。如果我们使用优先级 1000(默认),自动布局将开始调整可拖动视图的大小以强制它们始终具有一些垂直重叠。

我们在垫片的底部边缘设置了类似的约束:

    [rootView addConstraint:constraintWithPriority([NSLayoutConstraint
        constraintWithItem:spacerView attribute:NSLayoutAttributeBottom
        relatedBy:NSLayoutRelationLessThanOrEqual
        toItem:leftView attribute:NSLayoutAttributeBottom
        multiplier:1 constant:0], 2)];
    [rootView addConstraint:constraintWithPriority([NSLayoutConstraint
        constraintWithItem:spacerView attribute:NSLayoutAttributeBottom
        relatedBy:NSLayoutRelationLessThanOrEqual
        toItem:rightView attribute:NSLayoutAttributeBottom
        multiplier:1 constant:0], 2)];
}

因此,如果存在重叠,则间隔将始终跨越可拖动视图的垂直重叠。如果没有重叠,自动布局将打破一些约束,以避免给定间隔一个负高度。分隔符的高度为 0,固定到可拖动视图的近边缘之一。

最后,我将文本字段设置为在分隔符上水平和垂直居中:

- (void)createMiddleView {
    middleView = [[NSTextField alloc] init];
    middleView.translatesAutoresizingMaskIntoConstraints = NO;
    middleView.stringValue = @"Center";
    [middleView setEditable:NO];
    [middleView setSelectable:NO];
    [rootView addSubview:middleView];

    [rootView addConstraint:[NSLayoutConstraint
        constraintWithItem:middleView attribute:NSLayoutAttributeCenterX
        relatedBy:NSLayoutRelationEqual
        toItem:spacerView attribute:NSLayoutAttributeCenterX
        multiplier:1 constant:0]];
    [rootView addConstraint:[NSLayoutConstraint
        constraintWithItem:middleView attribute:NSLayoutAttributeCenterY
        relatedBy:NSLayoutRelationEqual
        toItem:spacerView attribute:NSLayoutAttributeCenterY
        multiplier:1 constant:0]];
}

由于五个视图(包括根视图)之间的所有这些约束,自动布局使中间视图以您请求的方式居中。

我不确定从 Xcode 4.6.3 开始是否可以在 nib 中设置这样的约束。如果可能的话,我敢肯定这很痛苦。

您可以找到我的完整演示项目源代码in this github repository。我在 Xcode 5-DP2 中创建了项目。

【讨论】:

  • 很棒的教程。使用 Xcode 4 版本会更棒...... ;) 可悲的是,它强调了 Auto Layout 的真正难度,这在很大程度上要归功于它在 Cocoa 中还很年轻。它确实让 CoreData 看起来很简单。
  • 如果您将源文件复制到在 Xcode 4 中创建的新项目中,它们应该可以工作。
  • 确实如此。是笔尖被吓坏了。 BTW +100 的 gif :D
  • 天哪。我什至没有看到这个答案.. 嗯。好吧,我将尝试一下,但是我没有告诉您的是,在任何绿色视图组合之间有许多绿色视图和潜在的红色视图..红色视图也可以垂直..因此复杂性。我现在只是想保持简单。非常好的教程,顺便说一句