繁体   English   中英

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

[英]Emulating aspect-fit behaviour using AutoLayout constraints in Xcode 6

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

我在 Interface Builder 的容器视图中有一个子视图。 子视图有一些我希望尊重的固有纵横比。 容器视图的大小在运行之前是未知的。

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

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

在任何一种情况下,我都希望子视图在容器视图中水平和垂直居中。

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

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

无论如何,您可以使用自动布局来做到这一点。 从 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 测试

为方便起见,我已将我的最终故事板上传到此要点

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

接下来,您将故事板中蓝色和粉红色视图的类更改为AspectFitView 最后,您将两个出口设置到您的topAspectFitViewbottomAspectFitView并在viewDidLoad设置它们的childView

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

这适用于 macOS。

我在使用 Rob 的方式在 OS X 应用程序上实现方面拟合时遇到问题。 但我用另一种方式制作它——我没有使用宽度和高度,而是使用前导、尾随、顶部和底部空间。

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

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

然后工作就完成了!

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

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

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

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

解决方法很简单,就是将 UIImageView 的 Content Compression Resistance Priority 设置为低于等宽等高约束的优先级:

内容抗压性

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

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

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

我找不到任何现成的完全编程的解决方案,所以这是我在swift 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)),
            ])
        }
    }
}

这是优先级扩展(来自另一个线程,但我用 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
    }
}

希望有帮助~

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM