Эмуляция поведения подгонки аспекта с использованием ограничений AutoLayout в Xcode 6

336

Я хочу использовать AutoLayout для определения размера и разметки представления способом, напоминающим режим контента в формате UIImageView.

У меня есть подпредставление внутри представления контейнера в Интерфейсном Разработчике. Подвид имеет некоторое соотношение сторон, которое я хочу уважать. Размер контейнера неизвестен до времени выполнения.

Если форматное соотношение контейнерного представления шире, чем у подпредставления, тогда я хочу, чтобы высота подпредставления равнялась высоте родительского представления.

Если соотношение сторон представления контейнера выше, чем у подпредставления, тогда я хочу, чтобы ширина подпредставления была равна ширине родительского представления.

В любом случае я хочу, чтобы подпредставление было центрировано горизонтально и вертикально в пределах контейнера.

Есть ли способ достичь этого, используя ограничения AutoLayout в Xcode 6 или в предыдущей версии? В идеале использовать Interface Builder, но если нет, то возможно определить такие ограничения программно.

chris838
источник

Ответы:

1046

Вы не описываете масштабирование, чтобы соответствовать; ты описываешь аспектное соответствие. (Я отредактировал ваш вопрос в этом отношении.) Подвид становится максимально большим, сохраняя соотношение сторон и полностью вписываясь в родительский элемент.

В любом случае, вы можете сделать это с помощью автоматического макета. Вы можете сделать это полностью в IB начиная с Xcode 5.1. Давайте начнем с некоторых взглядов:

некоторые взгляды

Светло-зеленый вид имеет соотношение сторон 4: 1. Темно-зеленый вид имеет соотношение сторон 1: 4. Я собираюсь установить ограничения таким образом, чтобы синий вид заполнял верхнюю половину экрана, розовый вид заполнял нижнюю половину экрана, и каждый зеленый вид максимально расширялся, сохраняя при этом свое соотношение сторон и вписываясь в него. контейнер.

Сначала я создам ограничения для всех четырех сторон синего вида. Я прикреплю его к ближайшему соседу на каждом ребре с расстоянием 0. Я обязательно отключу поля:

синие ограничения

Обратите внимание, что я пока не обновляю фрейм. Мне проще оставить пространство между представлениями при установке ограничений, и просто установить константы в 0 (или что-то еще) вручную.

Затем я прикрепляю левый, нижний и правый края розового изображения к ближайшему соседу. Мне не нужно устанавливать ограничение верхнего края, потому что его верхний край уже ограничен нижним краем синего вида.

розовые ограничения

Мне также нужно ограничение равных высот между розовым и синим видами. Это заставит их каждый заполнить половину экрана:

введите описание изображения здесь

Если я скажу Xcode обновить все кадры сейчас, я получу это:

контейнеры разложены

Так что ограничения, которые я установил до сих пор, верны. Я отменяю это и начинаю работу над светло-зеленым видом.

Аспектная подгонка светло-зеленого вида требует пяти ограничений:

  • Требуемое приоритетное соотношение сторон на светло-зеленом виде. Вы можете создать это ограничение в XIB или раскадровке с помощью Xcode 5.1 или более поздней версии.
  • Ограничение обязательного приоритета, ограничивающее ширину светло-зеленого вида меньше или равной ширине его контейнера.
  • Ограничение с высоким приоритетом, устанавливающее ширину светло-зеленого вида равным ширине его контейнера.
  • Ограничение обязательного приоритета, ограничивающее высоту светло-зеленого вида меньше или равной высоте его контейнера.
  • Ограничение с высоким приоритетом, устанавливающее высоту светло-зеленого вида равным высоте его контейнера.

Давайте рассмотрим два ограничения ширины. Ограничение «меньше или равно» само по себе недостаточно для определения ширины светло-зеленого изображения; много ширины будут соответствовать ограничению. Поскольку существует неоднозначность, autolayout попытается выбрать решение, которое минимизирует ошибку в другом (высокоприоритетном, но не обязательном) ограничении. Минимизация ошибки означает, что ширина должна быть как можно ближе к ширине контейнера, при этом не нарушая обязательное ограничение меньше или равное.

То же самое происходит с ограничением по высоте. И поскольку также требуется ограничение соотношения сторон, оно может максимизировать размер подпредставления только по одной оси (если только у контейнера не будет такое же соотношение сторон, как у подпредставления).

Итак, сначала я создаю ограничение соотношения сторон:

главный аспект

Затем я создаю равные ограничения ширины и высоты с контейнером:

верхний равный размер

Мне нужно отредактировать эти ограничения, чтобы они были меньше или равны:

верх меньше или равен размеру

Далее мне нужно создать еще один набор равных ограничений ширины и высоты с контейнером:

снова равный размер

И мне нужно сделать эти новые ограничения меньше требуемого приоритета:

верхний равный не требуется

Наконец, вы попросили центрировать подпредставление в его контейнере, поэтому я установлю эти ограничения:

по центру

Теперь, чтобы протестировать, я выберу контроллер представления и попрошу Xcode обновить все кадры. Вот что я получаю:

неверный макет топа

К сожалению! Подвид расширился, чтобы полностью заполнить свой контейнер. Если я выберу его, то смогу увидеть, что на самом деле он сохранил свое соотношение сторон, но вместо аспектного соответствия он выполняет аспектное заполнение .

Проблема в том, что при ограничении «меньше или равно» имеет значение, какое представление находится на каждом конце ограничения, и XCode установил ограничение, противоположное моему ожиданию. Я мог бы выбрать каждое из двух ограничений и поменять местами его первый и второй элементы. Вместо этого я просто выберу подпредставление и изменим ограничения, чтобы они были больше или равны:

исправить верхние ограничения

Xcode обновляет макет:

правильное расположение сверху

Теперь я делаю все то же самое с темно-зеленым видом снизу. Мне нужно убедиться, что его соотношение сторон составляет 1: 4 (Xcode странным образом изменил его размер, поскольку у него не было ограничений). Я не буду показывать шаги снова, так как они одинаковы. Вот результат:

правильное расположение сверху и снизу

Теперь я могу запустить его в симуляторе iPhone 4S, размер экрана которого отличается от используемого IB, и проверить вращение:

тест iphone 4s

И я могу проверить в симуляторе iPhone 6:

тест iphone 6

Я загрузил свою последнюю раскадровку в этот список для вашего удобства.

Роб Майофф
источник
27
Я использовал LICEcap . Это бесплатно (GPL) и записи непосредственно в GIF. Пока размер анимированного GIF не превышает 2 МБ, вы можете разместить его на этом сайте, как и любое другое изображение.
Роб Майофф
1
@LucaTorella При наличии только высокоприоритетных ограничений макет неоднозначен. То, что сегодня вы получите желаемый результат, не означает, что оно будет в следующей версии iOS. Предположим, что подпредставлению требуется соотношение сторон 1: 1, размер суперпредставления - размер 99 × 100, и у вас есть только требуемые ограничения по соотношению сторон и высокоприоритетного равенства. Затем автоматическое расположение может установить подпредставление размером 99 × 99 (с общей ошибкой 1) или размером 100 × 100 (с общей ошибкой 1). Один размер подгонки; другой аспект-заполнить. Добавление необходимых ограничений дает уникальное решение с наименьшей ошибкой.
Роб Майофф
1
@LucaTorella Спасибо за исправления, касающиеся доступности ограничений соотношения сторон.
Роб Майофф
11
10000 лучших приложений с одним постом ... невероятно.
DonVaughn
2
Ничего себе .. Лучший ответ здесь в переполнении стека, которое я когда-либо видел .. Спасибо за это, сэр ..
0yeoj
24

Роб, твой ответ потрясающий! Я также знаю, что этот вопрос конкретно о достижении этого с помощью автоматического макета. Однако, просто для справки, я хотел бы показать, как это можно сделать в коде. Вы настраиваете вид сверху и снизу (синий и розовый) так, как показал Роб. Затем вы создаете кастом 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. Наконец вы установили два выхода на ваш ViewController 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

Йоханнес Фаренкруг
источник
1
@DanielGalasko Спасибо, вы правы. Я упоминаю об этом в своем ответе. Я просто подумал, что это было бы полезным справочным материалом для сравнения этапов, используемых в обоих решениях.
Йоханнес Фаренкруг
1
Очень полезно иметь программную ссылку.
ctpenrose
@JohannesFahrenkrug если у вас есть учебный проект, вы можете поделиться? спасибо :)
Эрхан Демирчи
1
@ErhanDemirci Конечно, вы идете: github.com/jfahrenkrug/SPWKAspectFitView
Йоханнес Фаренкруг
8

Мне нужно решение из принятого ответа, но выполненное из кода. Самый элегантный способ, который я нашел, это использование фреймворка Masonry .

#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);
}];
Томаш Бон
источник
5

Это для macOS.

У меня есть проблема, чтобы использовать способ Роба для достижения аспектного соответствия в приложении OS X. Но я сделал это другим способом - вместо ширины и высоты я использовал ведущий, трейлинг, верх и низ.

В основном, добавьте два начальных пробела, где один>> 0 @ 1000 требует приоритет, а другой = 0 @ 250 с низким приоритетом. Сделайте те же настройки для трейлинга, верхнего и нижнего пространства.

Конечно, вам нужно установить соотношение сторон и центр X и центр Y.

И тогда работа сделана!

введите описание изображения здесь введите описание изображения здесь

brianLikeApple
источник
Как вы могли бы делать это программно?
FateNuller
@FateNuller Просто следуйте инструкциям?
brianLikeApple
4

Я обнаружил, что мне нужно поведение аспектной заливки, чтобы UIImageView поддерживал свое собственное соотношение сторон и полностью заполнял контейнерное представление. Смущает, что мой UIImageView нарушал ОБА высокоприоритетные ограничения равной ширины и равной высоты (описанные в ответе Роба) и рендеринг с полным разрешением.

Решением было просто установить приоритет сопротивления сжатия содержимого UIImageView ниже, чем приоритет ограничений равной ширины и равной высоты:

Сопротивление сжатию содержимого

Грегор
источник
2

Это отличный ответ @ 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 ();
    }
}
Ларри Обриен
источник
1

Возможно, это самый короткий ответ с 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);
        }
    }
}];
Мистер мин
источник