Эмулирующее поведение с точки зрения соответствия с использованием ограничений 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);
        }
    }
}];