在Xcode 6中使用AutoLayout约束来模拟方面拟合行为

我想使用AutoLayout以一种让人联想到UIImageView的方面适合内容模式的方式来大小和布局视图。

我在接口生成器的容器视图中有一个子视图。子视图有一些固有的纵横比,我希望尊重它。容器视图的大小在运行时之前是未知的。

如果容器视图的纵横比比子视图宽,那么我希望子视图的高度等于父视图的高度。

如果容器视图的纵横比大于子视图,那么我希望子视图的宽度等于父视图的宽度。

在这两种情况下,我希望子视图在容器视图中水平和垂直居中。

在Xcode 6或之前的版本中,是否有一种方法可以使用AutoLayout约束来实现这一点?理想情况下使用Interface Builder,但如果不是,也许可以通过编程方式定义这样的约束。

95512 次浏览

你不是在描述比例匹配;您正在描述方面拟合。(我已经编辑了你的问题。)子视图变得尽可能大,同时保持它的纵横比并完全适合它的父视图。

不管怎样,你可以用自动布局来做这个。你完全可以在Xcode 5.1的IB中完成。让我们从一些视图开始:

some views

浅绿色视图的长宽比为4:1。深绿色视图的长宽比为1:4。我将设置约束,以便蓝色视图填充屏幕的上半部分,粉红色视图填充屏幕的下半部分,每个绿色视图尽可能地扩展,同时保持其纵横比并适合其容器。

首先,我将在蓝色视图的所有四个边创建约束。我将它固定到每条边上最近的邻居,距离为0。我确保关闭了页边距:

蓝色约束

注意,我更新了帧。我发现在设置约束时在视图之间留出空间更容易,只需手动将常量设置为0(或任何值)。

接下来,我将粉红色视图的左、下、右边缘固定到最近的邻居。我不需要设置上边约束因为它的上边已经被约束到蓝色视图的下边。

粉色约束

我还需要在粉色和蓝色视图之间设置等高约束。这将使它们各自占据屏幕的一半:

enter image description here

如果我告诉Xcode现在更新所有的帧,我得到这个:

容器布局

所以到目前为止我建立的约束条件是正确的。我撤销它并开始处理浅绿色视图。

对浅绿色视图进行方面拟合需要五个约束:

  • 浅绿色视图上的所需优先级纵横比约束。你可以在Xcode 5.1或更高版本的xib或storyboard中创建这个约束。
  • 必需优先级约束,限制浅绿色视图的宽度小于或等于其容器的宽度。
  • 高优先级约束,将浅绿色视图的宽度设置为与其容器的宽度相等。
  • 一个必需的优先级约束,限制浅绿色视图的高度小于或等于其容器的高度。
  • 高优先级约束,将浅绿色视图的高度设置为与其容器的高度相等。

让我们考虑两个宽度约束。小于或等于约束本身不足以确定浅绿色视图的宽度;许多宽度将符合限制。由于存在歧义,自动布局将尝试选择一个解决方案,使其他约束(高优先级但不是必需的)中的错误最小化。最小化误差意味着使宽度尽可能接近容器的宽度,同时不违反所需的小于或等于约束。

同样的事情也发生在高度限制上。由于还需要宽高比约束,它只能沿一个轴最大化子视图的大小(除非容器恰好与子视图具有相同的宽高比)。

首先我创建纵横比约束:

top aspect

然后我用容器创建等宽和等高的约束:

top equal size

我需要将这些约束编辑为小于或等于约束:

top小于或等于size

接下来,我需要用容器创建另一组等宽和高的约束:

top equal size again

我需要让这些新的约束的优先级低于要求的优先级:

top equal not required

最后,你要求子视图在它的容器中居中,所以我将设置这些约束:

top居中

现在,为了测试,我将选择视图控制器并让Xcode更新所有的帧。这是我得到的结果:

错误的顶部布局

哦!子视图已经扩展到完全填充它的容器。如果我选择它,我可以看到它实际上保持了它的纵横比,但它在做一个方面-填满而不是一个方面-适合

问题是,在一个小于或等于的约束上,哪个视图在约束的两端很重要,而Xcode设置的约束与我的期望相反。我可以选择两个约束中的每一个,并反转其第一项和第二项。相反,我将只选择子视图并将约束更改为大于或等于:

fix top constraints

Xcode更新布局:

正确的顶部布局

现在我对底部的深绿色视图做同样的事情。我需要确保它的纵横比是1:4 (Xcode以一种奇怪的方式调整了它,因为它没有约束)。我不会再展示步骤了,因为它们是一样的。结果如下:

正确的上下布局

现在我可以在iPhone 4S模拟器中运行它,它的屏幕大小与IB使用的屏幕大小不同,并测试旋转:

iphone 4s test

我可以在iPhone 6模拟器中进行测试:

iphone 6 test

为了方便大家,我把最后的故事板上传到这个要点

罗伯,你的答案太棒了! 我也知道这个问题是专门关于实现这一点使用自动布局。但是,作为参考,我想展示如何在代码中实现这一点。像Rob演示的那样,设置顶部和底部视图(蓝色和粉红色)。然后创建一个自定义AspectFitView:

AspectFitView.h:

#import <UIKit/UIKit.h>


@interface AspectFitView : UIView


@property (nonatomic, strong) UIView *childView;


@end

AspectFitView.m:

#import "AspectFitView.h"


@implementation AspectFitView


- (void)setChildView:(UIView *)childView
{
if (_childView) {
[_childView removeFromSuperview];
}


_childView = childView;


[self addSubview:childView];
[self setNeedsLayout];
}


- (void)layoutSubviews
{
[super layoutSubviews];


if (_childView) {
CGSize childSize = _childView.frame.size;
CGSize parentSize = self.frame.size;
CGFloat aspectRatioForHeight = childSize.width / childSize.height;
CGFloat aspectRatioForWidth = childSize.height / childSize.width;


if ((parentSize.height * aspectRatioForHeight) > parentSize.height) {
// whole height, adjust width
CGFloat width = parentSize.width * aspectRatioForWidth;
_childView.frame = CGRectMake((parentSize.width - width) / 2.0, 0, width, parentSize.height);
} else {
// whole width, adjust height
CGFloat height = parentSize.height * aspectRatioForHeight;
_childView.frame = CGRectMake(0, (parentSize.height - height) / 2.0, parentSize.width, height);
}
}
}


@end

接下来,将故事板中蓝色和粉色视图的类更改为AspectFitViews。最后,你设置两个出口到你的视图控制器topAspectFitViewbottomAspectFitView,并在viewDidLoad中设置它们的__abc3:

- (void)viewDidLoad {
[super viewDidLoad];


UIView *top = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 500, 100)];
top.backgroundColor = [UIColor lightGrayColor];


UIView *bottom = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 500)];
bottom.backgroundColor = [UIColor greenColor];


_topAspectFitView.childView = top;
_bottomAspectFitView.childView = bottom;
}

所以在代码中做到这一点并不难,它仍然具有很强的适应性,适用于可变大小的视图和不同的纵横比。

2015年7月更新:在这里找到一个演示应用程序:https://github.com/jfahrenkrug/SPWKAspectFitView

我需要一个解决方案从接受的答案,但从代码执行。我发现的最优雅的方法是使用砌体框架

#import "Masonry.h"


...


[view mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(view.mas_height).multipliedBy(aspectRatio);
make.size.lessThanOrEqualTo(superview);
make.size.equalTo(superview).with.priorityHigh();
make.center.equalTo(superview);
}];

我发现自己想要aspect-填满行为,以便UIImageView能够保持自己的纵横比并完全填充容器视图。令人困惑的是,我的UIImageView打破了高优先级等宽和等高的限制(在Rob的回答中描述了),并以全分辨率渲染。

解决方案是简单地设置UIImageView的内容压缩阻力优先级低于等宽等高约束的优先级:

Content Compression Resistance

这是@rob_mayoff对以代码为中心的方法的出色回答的一个端口,使用NSLayoutAnchor对象并移植到Xamarin。对我来说,NSLayoutAnchor和相关类使AutoLayout更容易编程:

public class ContentView : UIView
{
public ContentView (UIColor fillColor)
{
BackgroundColor = fillColor;
}
}


public class MyController : UIViewController
{
public override void ViewDidLoad ()
{
base.ViewDidLoad ();


//Starting point:
var view = new ContentView (UIColor.White);


blueView = new ContentView (UIColor.FromRGB (166, 200, 255));
view.AddSubview (blueView);


lightGreenView = new ContentView (UIColor.FromRGB (200, 255, 220));
lightGreenView.Frame = new CGRect (20, 40, 200, 60);


view.AddSubview (lightGreenView);


pinkView = new ContentView (UIColor.FromRGB (255, 204, 240));
view.AddSubview (pinkView);


greenView = new ContentView (UIColor.Green);
greenView.Frame = new CGRect (80, 20, 40, 200);
pinkView.AddSubview (greenView);


//Now start doing in code the things that @rob_mayoff did in IB


//Make the blue view size up to its parent, but half the height
blueView.TranslatesAutoresizingMaskIntoConstraints = false;
var blueConstraints = new []
{
blueView.LeadingAnchor.ConstraintEqualTo(view.LayoutMarginsGuide.LeadingAnchor),
blueView.TrailingAnchor.ConstraintEqualTo(view.LayoutMarginsGuide.TrailingAnchor),
blueView.TopAnchor.ConstraintEqualTo(view.LayoutMarginsGuide.TopAnchor),
blueView.HeightAnchor.ConstraintEqualTo(view.LayoutMarginsGuide.HeightAnchor, (nfloat) 0.5)
};
NSLayoutConstraint.ActivateConstraints (blueConstraints);


//Make the pink view same size as blue view, and linked to bottom of blue view
pinkView.TranslatesAutoresizingMaskIntoConstraints = false;
var pinkConstraints = new []
{
pinkView.LeadingAnchor.ConstraintEqualTo(blueView.LeadingAnchor),
pinkView.TrailingAnchor.ConstraintEqualTo(blueView.TrailingAnchor),
pinkView.HeightAnchor.ConstraintEqualTo(blueView.HeightAnchor),
pinkView.TopAnchor.ConstraintEqualTo(blueView.BottomAnchor)
};
NSLayoutConstraint.ActivateConstraints (pinkConstraints);




//From here, address the aspect-fitting challenge:


lightGreenView.TranslatesAutoresizingMaskIntoConstraints = false;
//These are the must-fulfill constraints:
var lightGreenConstraints = new []
{
//Aspect ratio of 1 : 5
NSLayoutConstraint.Create(lightGreenView, NSLayoutAttribute.Height, NSLayoutRelation.Equal, lightGreenView, NSLayoutAttribute.Width, (nfloat) 0.20, 0),
//Cannot be larger than parent's width or height
lightGreenView.WidthAnchor.ConstraintLessThanOrEqualTo(blueView.WidthAnchor),
lightGreenView.HeightAnchor.ConstraintLessThanOrEqualTo(blueView.HeightAnchor),
//Center in parent
lightGreenView.CenterYAnchor.ConstraintEqualTo(blueView.CenterYAnchor),
lightGreenView.CenterXAnchor.ConstraintEqualTo(blueView.CenterXAnchor)
};
//Must-fulfill
foreach (var c in lightGreenConstraints)
{
c.Priority = 1000;
}
NSLayoutConstraint.ActivateConstraints (lightGreenConstraints);


//Low priority constraint to attempt to fill parent as much as possible (but lower priority than previous)
var lightGreenLowPriorityConstraints = new []
{
lightGreenView.WidthAnchor.ConstraintEqualTo(blueView.WidthAnchor),
lightGreenView.HeightAnchor.ConstraintEqualTo(blueView.HeightAnchor)
};
//Lower priority
foreach (var c in lightGreenLowPriorityConstraints)
{
c.Priority = 750;
}


NSLayoutConstraint.ActivateConstraints (lightGreenLowPriorityConstraints);


//Aspect-fit on the green view now
greenView.TranslatesAutoresizingMaskIntoConstraints = false;
var greenConstraints = new []
{
//Aspect ratio of 5:1
NSLayoutConstraint.Create(greenView, NSLayoutAttribute.Height, NSLayoutRelation.Equal, greenView, NSLayoutAttribute.Width, (nfloat) 5.0, 0),
//Cannot be larger than parent's width or height
greenView.WidthAnchor.ConstraintLessThanOrEqualTo(pinkView.WidthAnchor),
greenView.HeightAnchor.ConstraintLessThanOrEqualTo(pinkView.HeightAnchor),
//Center in parent
greenView.CenterXAnchor.ConstraintEqualTo(pinkView.CenterXAnchor),
greenView.CenterYAnchor.ConstraintEqualTo(pinkView.CenterYAnchor)
};
//Must fulfill
foreach (var c in greenConstraints)
{
c.Priority = 1000;
}
NSLayoutConstraint.ActivateConstraints (greenConstraints);


//Low priority constraint to attempt to fill parent as much as possible (but lower priority than previous)
var greenLowPriorityConstraints = new []
{
greenView.WidthAnchor.ConstraintEqualTo(pinkView.WidthAnchor),
greenView.HeightAnchor.ConstraintEqualTo(pinkView.HeightAnchor)
};
//Lower-priority than above
foreach (var c in greenLowPriorityConstraints)
{
c.Priority = 750;
}


NSLayoutConstraint.ActivateConstraints (greenLowPriorityConstraints);


this.View = view;


view.LayoutIfNeeded ();
}
}

这是针对macOS的。

我有问题使用罗伯的方式来实现方面适合OS X应用程序。但我用了另一种方法——我没有使用宽度和高度,而是使用了前导、后导、顶部和底部空间。

基本上,添加两个前导空格,其中一个是>= 0 @1000所需优先级,另一个是= 0 @250低优先级。对尾距、顶部和底部空间做相同的设置。

当然,你需要设置纵横比和中心X和中心Y。

然后工作就完成了!

enter image description here enter image description here < / p >

也许这是最简短的答案,使用砌筑,它也支持方面填充和拉伸。

typedef NS_ENUM(NSInteger, ContentMode) {
ContentMode_aspectFit,
ContentMode_aspectFill,
ContentMode_stretch
}


// ....


[containerView addSubview:subview];


[subview mas_makeConstraints:^(MASConstraintMaker *make) {
if (contentMode == ContentMode_stretch) {
make.edges.equalTo(containerView);
}
else {
make.center.equalTo(containerView);
make.edges.equalTo(containerView).priorityHigh();
make.width.equalTo(content.mas_height).multipliedBy(4.0 / 3); // the aspect ratio
if (contentMode == ContentMode_aspectFit) {
make.width.height.lessThanOrEqualTo(containerView);
}
else { // contentMode == ContentMode_aspectFill
make.width.height.greaterThanOrEqualTo(containerView);
}
}
}];

我找不到任何现成的完全程序化的解决方案,所以这里是我在斯威夫特5中对视图的方面填补扩展的看法:

extension UIView {


public enum FillingMode {
case full(padding:Int = 0)
case aspectFit(ratio:CGFloat)
// case aspectFill ...
}


public func addSubview(_ newView:UIView, withFillingMode fillingMode:FillingMode) {
newView.translatesAutoresizingMaskIntoConstraints = false
addSubview(newView)


switch fillingMode {
case let .full(padding):
let cgPadding = CGFloat(padding)


NSLayoutConstraint.activate([
newView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: cgPadding),
newView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -cgPadding),
newView.topAnchor.constraint(equalTo: topAnchor, constant: cgPadding),
newView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -cgPadding)
])
case let .aspectFit(ratio):
guard ratio != 0 else { return }


NSLayoutConstraint.activate([
newView.centerXAnchor.constraint(equalTo: centerXAnchor),
newView.centerYAnchor.constraint(equalTo: centerYAnchor),


newView.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor),
newView.leadingAnchor.constraint(equalTo: leadingAnchor).usingPriority(900),


newView.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor),
newView.trailingAnchor.constraint(equalTo: trailingAnchor).usingPriority(900),


newView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor),
newView.topAnchor.constraint(equalTo: topAnchor).usingPriority(900),


newView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor),
newView.bottomAnchor.constraint(equalTo: bottomAnchor).usingPriority(900),


newView.heightAnchor.constraint(equalTo: newView.widthAnchor, multiplier: CGFloat(ratio)),
])
}
}
}

这里是优先级扩展(来自另一个线程,但我"安全quot;在1…1000之间进行一些模式匹配:

extension NSLayoutConstraint {
/// Returns the constraint sender with the passed priority.
///
/// - Parameter priority: The priority to be set.
/// - Returns: The sended constraint adjusted with the new priority.
func usingPriority(_ priority: Int) -> NSLayoutConstraint {
self.priority = UILayoutPriority( (1...1000 ~= priority) ? Float(priority) : 1000 )
return self
}
}

希望能有所帮助~