Эмулирующее поведение с точки зрения соответствия с использованием ограничений AutoLayout в Xcode 6
Я хочу использовать AutoLayout для размера и компоновки представления таким образом, который напоминает режим содержимого, совместимый с форматом UIImageView.
У меня есть subview внутри представления контейнера в Interface Builder. Подвижность имеет некоторое неотъемлемое соотношение сторон, которое я хочу уважать. Размер контейнера неизвестен до времени выполнения.
Если соотношение сторон контейнера шире, чем subview, тогда я хочу, чтобы высота подзадача была равна высоте родительского представления.
Если соотношение сторон контейнера выше, чем subview, тогда я хочу, чтобы ширина подвидности была равна ширине представления родителя.
В любом случае я хочу, чтобы subview располагалось в горизонтальном и вертикальном направлениях в представлении контейнера.
Есть ли способ достичь этого, используя ограничения AutoLayout в Xcode 6 или в предыдущей версии? В идеале с помощью Interface Builder, но если не возможно, то можно программно определить такие ограничения.
Ответы
Ответ 1
Вы не описываете масштабирование по размеру; вы описываете аспект. (Я отредактировал ваш вопрос в этом отношении.) Подвью становится настолько большим, насколько возможно, сохраняя его соотношение сторон и полностью устанавливая его внутри родителя.
В любом случае, вы можете сделать это с автоматической компоновкой. Вы можете сделать это полностью в IB с Xcode 5.1. Начнем с некоторых представлений:
![some views]()
Светло-зеленый вид имеет соотношение сторон 4: 1. Темно-зеленый вид имеет соотношение сторон 1: 4. Я собираюсь установить ограничения, чтобы синий вид заполнял верхнюю половину экрана, розовый вид заполняет нижнюю половину экрана, и каждый зеленый вид расширяется как можно больше, сохраняя его соотношение сторон и подгонку в его контейнер.
Во-первых, я создам ограничения на всех четырех сторонах синего представления. Я привяжу его к ближайшему соседу на каждом краю с расстоянием 0. Я обязательно отключу поля:
![blue constraints]()
Обратите внимание, что я не обновляет фрейм. Мне легче оставить пространство между представлениями при настройке ограничений и просто установить константы в 0 (или что-то другое) вручную.
Затем я привожу левый, нижний и правый края розового вида к ближайшему соседу. Мне не нужно устанавливать ограничение верхнего края, потому что его верхний край уже привязан к нижнему краю синего представления.
![pink constraints]()
Мне также нужно ограничение равных высот между розовым и синим. Это заставит их заполнить половину экрана:
![enter image description here]()
Если я скажу Xcode обновлять все фреймы сейчас, я получаю следующее:
![containers laid out]()
Таким образом, ограничения, которые я установил до сих пор, верны. Я отменяю это и начинаю работу на светло-зеленом фоне.
Аспектный подход к светло-зеленому виду требует пяти ограничений:
- Ограничение соотношения приоритетов с приоритетом приоритета на светло-зеленом экране. Вы можете создать это ограничение в xib или раскадровке с Xcode 5.1 или новее.
- Ограничение требуемого приоритета, ограничивающее ширину светло-зеленого вида, меньше или равно ширине его контейнера.
- Ограничение с высоким приоритетом, задающее ширину светло-зеленого вида, равную ширине его контейнера.
- Ограничение требуемого приоритета, ограничивающее высоту светло-зеленого вида, меньше или равно высоте его контейнера.
- Ограничение с высоким приоритетом, определяющее высоту светло-зеленого изображения, равное высоте его контейнера.
Рассмотрим два ограничения ширины. Ограничение меньше или равно, само по себе, недостаточно для определения ширины светло-зеленого вида; многие ширины будут соответствовать ограничению. Поскольку существует двусмысленность, автоопределение будет пытаться выбрать решение, которое минимизирует ошибку в другом (высокоприоритетное, но не обязательное) ограничение. Минимизация ошибки означает, что ширина как можно ближе к ширине контейнера, не нарушая требуемое ограничение меньше или равно.
То же самое происходит с ограничением высоты. А так как ограничение соотношения сторон также требуется, оно может максимизировать размер подсмотра вдоль одной оси (если только контейнер не имеет того же соотношения сторон, что и подвью).
Итак, сначала создаю ограничение соотношения сторон:
![top aspect]()
Затем я создаю ограничения ширины и высоты с контейнером:
![top equal size]()
Мне нужно отредактировать эти ограничения, чтобы быть ограничениями меньше или равными:
![top less than or equal size]()
Далее мне нужно создать еще один набор ограничений ширины и высоты с контейнером:
![top equal size again]()
И мне нужно сделать эти новые ограничения менее приоритетными:
![top equal not required]()
Наконец, вы попросили центрировать его в контейнере, поэтому я установлю эти ограничения:
![top centered]()
Теперь, чтобы проверить, я выберу контроллер представления и попрошу Xcode обновить все фреймы. Это то, что я получаю:
<Т411 >
Oops! Subview расширился, чтобы полностью заполнить его контейнер. Если я его выберу, я вижу, что на самом деле он сохранил соотношение сторон, но он делает аспект заполнить вместо аспект .
Проблема заключается в том, что при ограничении, отличном от или равном, важно, какое представление находится на каждом конце ограничения, а Xcode настроил ограничение, противоположное моему ожиданию. Я мог бы выбрать каждое из двух ограничений и отменить его первый и второй элементы. Вместо этого я просто выберем subview и изменим ограничения на большее или равное:
![fix top constraints]()
Xcode обновляет макет:
![correct top layout]()
Теперь я делаю все то же самое для темно-зеленого представления внизу. Мне нужно убедиться, что его соотношение сторон составляет 1: 4 (Xcode изменил его по-своему, поскольку он не имел ограничений). Я не буду показывать шаги снова, так как они одинаковы. Здесь результат:
![correct top and bottom layout]()
Теперь я могу запустить его в симуляторе iPhone 4S, который имеет другой размер экрана, чем используемый IB, и проверить вращение:
![iphone 4s test]()
И я могу протестировать в симуляторе iPhone 6:
![iphone 6 test]()
Я загрузил свой последний раскадровки в этот смысл для вашего удобства.
Ответ 2
Роб, ваш ответ потрясающий!
Я также знаю, что этот вопрос конкретно связан с достижением этого, используя автоматическую компоновку. Однако, как ссылка, я хотел бы показать, как это можно сделать в коде. Вы создали верхний и нижний вид (синий и розовый), как показал Роб. Затем вы создаете пользовательский 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
s. Наконец, вы устанавливаете два выхода на свой монитор управления topAspectFitView
и bottomAspectFitView
и устанавливаете их childView
в 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. Найти демо-приложение здесь: https://github.com/jfahrenkrug/SPWKAspectFitView
Ответ 3
Мне понадобилось решение из принятого ответа, но было выполнено из кода. Самый элегантный способ, который я нашел, - использовать Масонную структуру.
#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);
}];
Ответ 4
Это для macOS.
У меня есть проблема, чтобы использовать способ Rob для достижения соответствия аспектам приложения OS X. Но я сделал это другим способом. Вместо использования ширины и высоты я использовал начальное, конечное, верхнее и нижнее пространство.
В принципе, добавьте два ведущих пробела, где один → = 0 @1000, требуется приоритет, а другой - = 0 @250 с низким приоритетом. Выполняйте те же настройки в трейлинг, верхнем и нижнем пространстве.
Конечно, вам нужно установить соотношение сторон и центр X и центр Y.
И тогда работа выполнена!
![введите описание изображения здесь]()
Ответ 5
Мне показалось, что я хочу, чтобы стиль заполнял, чтобы UIImageView сохранял собственное соотношение сторон и полностью заполнял вид контейнера. Смутно, мой UIImageView нарушал BOTH высокоприоритетные ограничения равной ширины и равной высоты (описанные в ответ Rob) и рендеринг с полным разрешением.
Решением было просто установить приоритет сопротивления сжатию содержимого UIImageView ниже приоритета ограничений равной ширины и равной высоты:
![Content Compression Resistance]()
Ответ 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 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 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_scaleAspectFit,
ContentMode_scaleAspectFill,
ContentMode_scaleToFill // stretch
}
// ....
[containerView addSubview:subview];
[subview mas_makeConstraints:^(MASConstraintMaker *make) {
if (contentMode == ContentMode_scaleToFill) {
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_scaleAspectFit) {
make.width.height.lessThanOrEqualTo(containerView);
}
else { // contentMode == ContentMode_scaleAspectFill
make.width.height.greaterThanOrEqualTo(containerView);
}
}
}];