[英]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 更新所有幀。 這就是我得到的:
哎呀! 子視圖已擴展以完全填充其容器。 如果我選擇它,我可以看到實際上它保持了它的縱橫比,但它正在執行縱橫比填充而不是縱橫比匹配。
問題是,在小於或等於約束上,約束的每一端哪個視圖很重要,而 Xcode 設置的約束與我的預期相反。 我可以選擇兩個約束中的每一個並反轉其第一項和第二項。 相反,我將只選擇子視圖並將約束更改為大於或等於:
Xcode 更新布局:
現在我對底部的深綠色視圖執行所有相同的操作。 我需要確保它的縱橫比是 1:4(Xcode 以一種奇怪的方式調整了它的大小,因為它沒有限制)。 我不會再顯示這些步驟,因為它們是相同的。 結果如下:
現在我可以在 iPhone 4S 模擬器中運行它,它的屏幕尺寸與使用的 IB 不同,並測試旋轉:
我可以在 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
。 最后,您將兩個出口設置到您的topAspectFitView
和bottomAspectFitView
並在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
我需要接受的答案中的解決方案,但從代碼中執行。 我發現的最優雅的方法是使用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.