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


336

我想使用AutoLayout以一种类似于UIImageView的方面适合内容模式的方式来调整视图的大小和布局。

我在Interface Builder的容器视图内有一个子视图。子视图具有一些我要尊重的固有长宽比。直到运行时,容器视图的大小都是未知的。

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

如果容器视图的长宽比高于子视图,则我希望子视图的宽度等于父视图的宽度。

无论哪种情况,我都希望子视图在容器视图内水平和垂直居中。

有没有一种方法可以在Xcode 6或以前的版本中使用AutoLayout约束来实现此目的?理想情况下使用Interface Builder,但如果不是这样,则可以通过程序定义此类约束。

Answers:


1046

您并不是在描述适合的比例;您正在描述方面适合。(在这方面,我已经编辑了您的问题。)子视图在保持其宽高比并完全适合其父级的同时变得尽可能大。

无论如何,您可以使用自动布局来做到这一点。从Xcode 5.1开始,您可以完全在IB中完成此操作。让我们从一些观点开始:

一些意见

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

首先,我将在蓝色视图的所有四个侧面上创建约束。我将其固定到每个边缘上最近的邻居(距离为0)。我确保关闭边距:

蓝色约束

请注意,我还没有更新框架。我发现在设置约束时在视图之间留出空间比较容易,只需手动将常量设置为0(或其他任何值)即可。

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

粉色约束

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

在此处输入图片说明

如果我告诉Xcode现在更新所有框架,则会得到以下信息:

放置的容器

因此,到目前为止,我设置的约束是正确的。我撤消该操作,然后开始在浅绿色视图上工作。

符合外观的浅绿色视图需要五个约束:

  • 浅绿色视图上的要求优先宽高比约束。您可以使用Xcode 5.1或更高版本在xib或情节提要中创建此约束。
  • 将浅绿色视图的宽度限制为小于或等于其容器的宽度的必需优先级约束。
  • 高优先级约束将浅绿色视图的宽度设置为其容器的宽度。
  • 将浅绿色视图的高度限制为小于或等于其容器的高度的必需优先级约束。
  • 高优先级约束将浅绿色视图的高度设置为其容器的高度。

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

高度约束也会发生相同的情况。并且由于还需要长宽比约束,因此它只能使子视图沿一个轴的大小最大化(除非容器碰巧具有与子视图相同的长宽比)。

所以首先我创建宽高比约束:

顶面

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

大小相等

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

顶部小于或等于大小

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

顶部再相等

而且我需要使这些新约束小于所需的优先级:

顶等式不是必需的

最后,您要求子视图以其容器为中心,因此我将设置这些约束:

居中

现在,要进行测试,我将选择视图控制器,并要求Xcode更新所有框架。这是我得到的:

顶部布局不正确

糟糕!子视图已扩展为完全填充其容器。如果选择它,我可以看到它实际上保持了宽高比,但是它正在执行宽高比填充而不是宽高比拟合

问题在于,在小于或等于约束条件下,约束的每个端点上的哪个视图都很重要,并且Xcode设置的约束与我的预期相反。我可以选择两个约束中的每个约束并颠倒其第一和第二项。相反,我将选择子视图并将约束更改为大于或等于:

修复主要约束

Xcode更新布局:

正确的顶部布局

现在,我对底部的深绿色视图执行相同的操作。我需要确保其长宽比为1:4(Xcode以一种奇怪的方式调整了它的大小,因为它没有约束)。因为它们是相同的,所以我不再显示步骤。结果如下:

正确的顶部和底部布局

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

iPhone 4S测试

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

iPhone 6测试

为了方便起见,我已将我的最终情节提要上传到该要点


27
我用了LICEcap。它是免费(GPL)并直接记录到GIF。只要GIF动画的大小不超过2 MB,您就可以像其他任何图像一样将其发布到此站点。
rob mayoff 2014年

1
@LucaTorella仅具有高优先级约束,所以布局不明确。 仅仅因为它今天获得了您想要的结果,并不意味着它将在下一版iOS中获得。 假设子视图的纵横比为1:1,超级视图的尺寸为99×100,并且您只有所需的纵横比和高优先级相等约束。然后,自动布局可以将子视图的大小设置为99×99(总错误1)或100×100(总错误1)。一种尺寸是适合外观的尺寸;另一个是方面填充。添加所需的约束将产生唯一的最小误差解决方案。
rob mayoff

1
@LucaTorella感谢您对纵横比约束的可用性进行的更正。
rob mayoff 2014年

11
只需发布一则帖子,即可获得10,000多个更好的应用程序……令人难以置信。
DonVaughn 2014年

2
哇..在这里堆栈溢出我见过的最好的答案。谢谢你为这个先生..
0yeoj

24

罗布,你的回答真棒!我也知道这个问题是专门关于通过使用自动布局来实现的。但是,作为参考,我想展示如何在代码中完成此操作。就像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并设置其childView为s viewDidLoad

- (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


1
@DanielGalasko谢谢,您是正确的。我在回答中提到了这一点。我只是认为比较两个解决方案涉及的步骤将是一个有用的参考。
约翰内斯·法伦克鲁格

1
也非常有编程参考。
ctpenrose 2014年

@JohannesFahrenkrug如果您有辅导项目,可以分享吗?谢谢您:)
Erhan Demirci 2015年

1
@ErhanDemirci当然,您可以去:github.com/jfahrenkrug/SPWKAspectFitView
Johannes Fahrenkrug

8

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

#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);
}];

5

这是针对macOS的。

我在使用Rob的方法来实现OS X应用程序的方面适合时遇到问题。但是我用另一种方式做到了-我没有使用宽度和高度,而是使用了前导,尾随,顶部和底部空间。

基本上,添加两个前导空格,其中一个>> 0 @ 1000要求的优先级,另一个为= 0 @ 250低优先级。对尾部,顶部和底部空间进行相同的设置。

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

然后工作完成了!

在此处输入图片说明 在此处输入图片说明


您将如何以编程方式进行此操作?
FateNuller

@FateNuller只是按照步骤进行?
brianLikeApple

4

我发现自己想要宽高比填充行为,以便UIImageView可以保持自己的宽高比并完全填充容器视图。令人困惑的是,我的UIImageView打破了高优先级的等宽和等高约束(如Rob的回答所述),并以全分辨率渲染。

解决方案只是将UIImageView的“内容压缩阻力优先级”设置为低于等宽和等高约束的优先级:

耐压缩性


2

这是@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 ();
    }
}

1

也许这是砌筑的最短答案它也支持长宽比填充和拉伸。

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);
        }
    }
}];
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.