Emulowanie zachowania dopasowania aspektu przy użyciu ograniczeń AutoLayout w Xcode 6

Chcę użyć AutoLayout do rozmiaru i układu widoku w sposób przypominający tryb zawartości interfejsu UIImageView.

Mam subview wewnątrz widoku kontenera w Interface Builder. Subview ma pewne nieodłączne proporcje, które chciałbym szanować. Rozmiar widoku kontenera jest Nieznany do czasu uruchomienia.

Jeśli proporcje widoku kontenera są szersze niż widok podrzędny, to chcę, aby wysokość widoku podrzędnego była równa wysokości widoku nadrzędnego.

Jeśli pojemnik proporcje widoku są wyższe niż podview, a następnie chcę, aby szerokość podview była równa szerokości widoku nadrzędnego.

W obu przypadkach chciałbym, aby subview był wyśrodkowany poziomo i pionowo w widoku kontenera.

Czy istnieje sposób, aby to osiągnąć przy użyciu ograniczeń AutoLayout w Xcode 6 lub w poprzedniej wersji? Najlepiej używać kreatora interfejsów, ale jeśli nie, możliwe jest programowe definiowanie takich ograniczeń.

Author: rob mayoff, 2014-09-10

7 answers

Nie opisujesz skali do dopasowania; opisujesz aspekt dopasowania. (Zredagowałem twoje pytanie w tym zakresie.) Subview staje się tak duży, jak to możliwe, zachowując swój współczynnik proporcji i dopasowując się całkowicie wewnątrz jego rodzica.

W każdym razie, można to zrobić z auto layout. Możesz to zrobić całkowicie w IB od Xcode 5.1. Zacznijmy od kilku widoków:

niektóre widoki

Jasnozielony Widok ma proporcje 4: 1. Ciemnozielony Widok ma proporcje 1: 4. I ' m Ustaw ograniczenia tak, aby Niebieski widok wypełniał górną połowę ekranu, różowy widok wypełniał dolną połowę ekranu, a każdy zielony widok rozszerzał się w miarę możliwości, zachowując proporcje i dopasowując się do kontenera.

Najpierw stworzę ograniczenia na wszystkich czterech stronach niebieskiego widoku. Przypiszę go do najbliższego sąsiada na każdej krawędzi, z odległości 0. Upewniam się, że wyłączam marginesy:

niebieskie ograniczenia

Zauważ, że ja nie aktualizuję rama jeszcze. Łatwiej jest mi zostawić miejsce między widokami podczas ustawiania ograniczeń i po prostu ustawić stałe na 0 (lub cokolwiek) ręcznie.

Następnie przypinam lewą, dolną i prawą krawędź różowego widoku do najbliższego sąsiada. Nie muszę ustawiać ograniczenia górnej krawędzi, ponieważ jego górna krawędź jest już ograniczona do dolnej krawędzi niebieskiego widoku.

pink

Potrzebuję też ograniczenia wysokości między różowym i niebieskim widokiem. To ich uczyni każde wypełnienie połowy ekranu:

Tutaj wpisz opis obrazka

Jeśli powiem Xcode, aby teraz zaktualizował wszystkie ramki, dostaję to:

kontenery rozkładane

Więc ograniczenia, które do tej pory ustawiłem są poprawne. Cofam to i zaczynam pracę nad jasnozielonym widokiem.

Dopasowanie widoku jasnozielonego wymaga pięciu ograniczeń:

  • ograniczenie proporcji o priorytecie wymaganym w widoku jasnozielonym. Możesz utworzyć to ograniczenie w xib lub storyboard z Xcode 5.1 lub później.
  • ograniczenie o wymaganym priorytecie ograniczające szerokość jasnozielonego widoku, aby była mniejsza lub równa szerokości kontenera.
  • ograniczenie o wysokim priorytecie ustawiające szerokość jasnozielonego widoku równą szerokości kontenera.
  • ograniczenie o wymaganym priorytecie ograniczające wysokość jasnozielonego widoku, aby była mniejsza lub równa wysokości jego kontenera.
  • ograniczenie o wysokim priorytecie ustawiające wysokość jasnozielonego widoku jest równa wysokości jego pojemnika.

Rozważmy dwa ograniczenia szerokości. Ograniczenie mniejsze lub równe samo w sobie nie jest wystarczające do określenia szerokości jasnozielonego widoku; wiele szerokości będzie pasować do ograniczenia. Ponieważ istnieje niejednoznaczność, autolayout spróbuje wybrać rozwiązanie, które minimalizuje błąd w drugim ograniczeniu (o wysokim priorytecie, ale nie jest wymagane). Minimalizacja Błędu oznacza, że szerokość jest jak najbardziej zbliżona do szerokości kontenera, nie naruszając jednocześnie wymaganego ograniczenia.

To samo dzieje się z ograniczeniem wysokości. A ponieważ wymagane jest również ograniczenie proporcji, może ono tylko zmaksymalizować rozmiar subview wzdłuż jednej osi (chyba że kontener ma taki sam współczynnik proporcji jak subview).

Więc najpierw tworzę ograniczenie proporcji:

top aspect

Następnie tworzę równe ograniczenia szerokości i wysokości z "kontener": {]}

górny równy rozmiar

Muszę edytować te ograniczenia, aby były mniej niż równe:

top mniejszy lub równy rozmiar

Następnie muszę utworzyć kolejny zestaw o jednakowej szerokości i wysokości z kontenerem:

top equal size again

I muszę uczynić te nowe ograniczenia mniej niż wymagany Priorytet:

top equal nie jest wymagany

Na koniec poprosiłeś o wyśrodkowanie subview w kontenerze, więc skonfiguruję te ograniczenia:

top wyśrodkowany

Teraz, aby test, wybieram kontroler widoku i poprosić Xcode, aby zaktualizować wszystkie ramki. Oto co dostaję:

nieprawidłowy układ góry

Oops! Subview rozszerzył się, aby całkowicie wypełnić swój kontener. Jeśli go zaznaczę, widzę, że w rzeczywistości zachowuje swój współczynnik proporcji, ale robi aspect - fill zamiast aspect- fit .

Problem polega na tym, że na mniejszym lub równym ograniczeniu ważne jest, który Widok znajduje się na każdym końcu ograniczenia, a Xcode ustawił ograniczenie odwrotne od moich oczekiwań. Mogłem wybrać każdy z dwóch ograniczeń i odwrócić jego pierwszą i drugą pozycję. Zamiast tego po prostu zaznaczę subview i zmienię ograniczenia na większe lub równe:

fix top constraints

Xcode aktualizuje układ:

poprawny układ góry

Teraz robię to samo z ciemnozielonym widokiem na dole. Muszę się upewnić, że jego współczynnik proporcji jest 1: 4 (Xcode zmienił rozmiar w dziwny sposób, ponieważ nie ma ograniczeń). I won ' t show znowu kroki, bo są takie same. Oto wynik:

poprawny układ góry i dołu

Teraz mogę go uruchomić w symulatorze iPhone 4S, który ma inny rozmiar ekranu niż używany IB, i test rotacji:

test iphone 4s

A ja mogę przetestować w symulatorze iPhone 6:

iphone 6 test

Załadowałem mój ostatni storyboard do ten gist dla Twojej wygody.

 1024
Author: rob mayoff,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-09-26 18:39:01

Rob, Twoja odpowiedź jest niesamowita! Wiem też, że to pytanie dotyczy w szczególności osiągnięcia tego za pomocą auto-layout. Jednak jako odniesienie chciałbym pokazać, jak można to zrobić w kodzie. Ustawiasz górne i dolne widoki (niebieskie i różowe), tak jak pokazywał Rob. Następnie tworzysz własny 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

Następnie zmieniasz klasę niebieskich i różowych widoków w storyboardzie na AspectFitViews. na koniec ustawiasz dwa wyjścia do swojego viewcontrollera topAspectFitView i bottomAspectFitView i ustawiasz ich childView s W 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;
}

Więc nie jest trudno to zrobić w kodzie i nadal jest bardzo elastyczny i działa z różnymi widokami i różnymi proporcjami.

Aktualizacja lipiec 2015 : Znajdź aplikację demo tutaj: https://github.com/jfahrenkrug/SPWKAspectFitView

 24
Author: Johannes Fahrenkrug,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-07-17 13:06:04

Potrzebowałem rozwiązania z zaakceptowanej odpowiedzi, ale wykonanego z kodu. Najbardziej eleganckim sposobem, jaki znalazłem, jest użycie ramy Murowanej .

#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);
}];
 6
Author: Tomasz Bąk,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-01-14 13:05:39

To jest dla macOS.

Mam problem z wykorzystaniem sposobu Roba, aby uzyskać dopasowanie aspect fit w aplikacji OS X. Ale zrobiłem to w inny sposób-zamiast używać szerokości i wysokości, użyłem wiodącej, ciągłej, górnej i dolnej przestrzeni.

Zasadniczo, dodaj dwie wiodące spacje, gdzie jedna to > = 0 @ 1000 wymagany priorytet, a druga to = 0 @250 niski priorytet. Wykonaj te same ustawienia dla spacji końcowej, górnej i dolnej.

Oczywiście trzeba ustawić proporcje i środek X i środek Y.

A potem robota skończona!

Tutaj wpisz opis obrazka Tutaj wpisz opis obrazka

 4
Author: brianLikeApple,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-01-17 09:51:31

Uznałem, że chcę zachowania aspect - fill , aby interfejs UIImageView zachowywał swój własny współczynnik proporcji i całkowicie wypełniał widok kontenera. Co ciekawe, mój interfejs użytkownika łamał zarówno ograniczenia o wysokim priorytecie o równej szerokości, jak i równej wysokości (opisane w odpowiedzi Roba) oraz renderowanie w pełnej rozdzielczości.

Rozwiązaniem było po prostu ustawienie priorytetu odporności na kompresję zawartości UIImageView poniżej priorytetu równej szerokości i równej wysokości ograniczenia:

Odporność Na Ściskanie Zawartości

 3
Author: Gregor,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-05-31 03:37:23

Jest to port doskonałej odpowiedzi @rob_mayoff na podejście zorientowane na kod, wykorzystujące obiekty NSLayoutAnchor i przeniesione do Xamarin. Dla mnie, NSLayoutAnchor i pokrewne klasy znacznie ułatwiły 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
Author: Larry OBrien,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-02-20 00:00:36

Być może jest to najkrótsza odpowiedź, z murem .

[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);
        if (contentMode == ContentMode_scaleAspectFit) {
            make.width.height.lessThanOrEqualTo(containerView);
        }
        else { // contentMode == ContentMode_scaleAspectFill
            make.width.height.greaterThanOrEqualTo(containerView);
        }
    }
}];
 0
Author: iwill,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-06-12 12:33:17