簡體   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