【问题标题】:Emulating aspect-fit behaviour using AutoLayout constraints in Xcode 6在 Xcode 6 中使用 AutoLayout 约束模拟切面匹配行为
【发布时间】:2014-11-04 04:20:18
【问题描述】:

我想使用 AutoLayout 以类似于 UIImageView 的 aspect-fit 内容模式的方式调整视图的大小和布局。

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

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

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

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

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

【问题讨论】:

    标签: ios objective-c xcode autolayout


    【解决方案1】:

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

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

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

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

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

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

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

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

    所以到目前为止我设置的约束是正确的。我撤消该操作并开始处理浅绿色视图。

    Aspect-fiting 浅绿色视图需要五个约束:

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

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

    高度限制也会发生同样的事情。并且由于还需要aspect-ratio约束,所以只能最大化subview沿一个轴的大小(除非容器恰好和子view有相同的宽高比)。

    首先我创建纵横比约束:

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

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

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

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

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

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

    哎呀!子视图已扩展以完全填满其容器。如果我选择它,我可以看到它实际上保持了它的纵横比,但它正在做一个 aspect-fill 而不是 aspect-fit

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

    Xcode 更新布局:

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

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

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

    为了您的方便,我已将我的最终故事板上传到 this gist

    【讨论】:

    • 我使用了LICEcap。它是免费的 (GPL) 并直接记录到 GIF。只要动画 GIF 不超过 2 MB,您就可以像任何其他图片一样将其发布到本网站。
    • @LucaTorella 只有高优先级约束,布局是模棱两可的。 仅仅因为它今天得到了你想要的结果并不意味着它会在下一个版本的 iOS 中。 假设子视图想要纵横比 1:1,父视图大小为 99×100,而你只有所需的纵横比和高优先级等式约束。然后自动布局可以将子视图设置为尺寸 99×99(总误差 1)或尺寸 100×100(总误差 1)。一种尺寸是适合纵横比的;另一个是方面填充。添加所需的约束会产生唯一的最小错误解决方案。
    • @LucaTorella 感谢您对纵横比限制的可用性进行更正。
    • 10,000 个更好的应用,只需一篇文章...令人难以置信。
    • 哇..我见过的堆栈溢出中的最佳答案..谢谢你的这个先生..
    【解决方案2】:

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

    【讨论】:

    【解决方案3】:

    这是为 macOS 设计的。

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

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

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

    然后工作就完成了!

    【讨论】:

    • 您如何以编程方式执行此操作?
    • @FateNuller 只是按照步骤操作?
    • 为了简单明了,这实际上应该被接受。也适用于 iOS。
    • 这也适用于 iOS,这应该被接受的答案:)
    【解决方案4】:

    我需要接受答案的解决方案,但从代码中执行。我发现最优雅的方法是使用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);
    }];
    

    【讨论】:

    • thanx man,这太完美了,太简单了,代码也更少:)
    【解决方案5】:

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

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

    【讨论】:

      【解决方案6】:

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

      【讨论】:

        【解决方案7】:

        也许这是最短的答案,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);
                }
            }
        }];
        

        【讨论】:

          【解决方案8】:

          我找不到任何现成的完全编程的解决方案,所以这是我对 swift 5 对视图的 aspect fill 扩展的看法:

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

          希望对你有帮助~

          【讨论】:

            猜你喜欢
            • 2018-09-20
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2015-04-02
            相关资源
            最近更新 更多