Emulowanie zachowania dopasowania do kształtu przy użyciu ograniczeń AutoLayout w Xcode 6


336

Chcę użyć AutoLayout do zmiany rozmiaru i układu widoku w sposób, który przypomina tryb zawartości dopasowany do formatu UIImageView.

Mam widok podrzędny w widoku kontenera w Konstruktorze interfejsów. Podview ma pewne nieodłączne proporcje, które chciałbym uszanować. Rozmiar widoku kontenera jest nieznany do czasu uruchomienia.

Jeśli współczynnik proporcji widoku kontenera jest szerszy niż widok podrzędny, chcę, aby wysokość widoku podrzędnego była równa wysokości widoku nadrzędnego.

Jeśli współczynnik kształtu widoku kontenera jest wyższy niż widok podrzędny, chcę, aby szerokość widoku podrzędnego była równa szerokości widoku nadrzędnego.

W obu przypadkach chcę, aby widok podrzędny był wyśrodkowany poziomo i pionowo w widoku kontenera.

Czy istnieje sposób na osiągnięcie tego za pomocą ograniczeń AutoLayout w Xcode 6 lub w poprzedniej wersji? Idealnie przy użyciu Konstruktora interfejsów, ale jeśli nie, możliwe jest programowe zdefiniowanie takich ograniczeń.

Odpowiedzi:


1046

Nie opisujesz dopasowania do skali; opisujesz dopasowanie aspektu. (Zredagowałem twoje pytanie w tym względzie.) Widok podrzędny staje się tak duży, jak to możliwe, przy jednoczesnym zachowaniu współczynnika kształtu i dopasowaniu całkowicie do jego rodzica.

W każdym razie możesz to zrobić za pomocą automatycznego układu. Możesz to zrobić całkowicie w IB od Xcode 5.1. Zacznijmy od kilku widoków:

niektóre poglądy

Jasnozielony widok ma współczynnik kształtu 4: 1. Ciemnozielony widok ma współczynnik kształtu 1: 4. Zamierzam skonfigurować 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 rozwijał się tak bardzo, jak to możliwe, przy jednoczesnym zachowaniu jego proporcji i dopasowania pojemnik.

Najpierw utworzę ograniczenia ze wszystkich czterech stron niebieskiego widoku. Przypinam go do najbliższego sąsiada na każdej krawędzi, w odległości 0. Upewnij się, że marginesy są wyłączone:

niebieskie ograniczenia

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

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.

różowe ograniczenia

Potrzebuję również ograniczenia równości wysokości między widokiem różowym i niebieskim. Spowoduje to, że każde z nich wypełni połowę ekranu:

wprowadź opis zdjęcia tutaj

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

rozłożone pojemniki

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

Dopasowanie aspektu do jasnozielonego widoku wymaga pięciu ograniczeń:

  • Ograniczenie proporcji wymaganego priorytetu do jasnozielonego widoku. Możesz utworzyć to ograniczenie w Xib lub Storyboard w Xcode 5.1 lub nowszym.
  • Ograniczenie o wymaganym priorytecie ograniczające szerokość jasnozielonego widoku do mniejszej lub równej szerokości jego kontenera.
  • Ograniczenie o wysokim priorytecie ustalające szerokość jasnozielonego widoku na równą szerokości jego 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 ustalające wysokość jasnozielonego widoku na równą wysokości jego kontenera.

Rozważmy dwa ograniczenia szerokości. Samo ograniczenie mniejsze lub równe nie jest wystarczające do określenia szerokości jasnozielonego widoku; wiele szerokości będzie pasować do ograniczenia. Ponieważ występuje niejednoznaczność, autoukład będzie próbował wybrać rozwiązanie, które minimalizuje błąd w innym ograniczeniu (o wysokim priorytecie, ale niewymaganym). Minimalizacja błędu oznacza, że ​​szerokość powinna być jak najbliższa szerokości kontenera, nie naruszając wymaganego ograniczenia mniejszego lub równego.

To samo dzieje się z ograniczeniem wysokości. A ponieważ wymagane jest również ograniczenie współczynnika proporcji, może ono jedynie zmaksymalizować rozmiar widoku podrzędnego wzdłuż jednej osi (chyba że kontener ma taki sam współczynnik kształtu jak widok podrzędny).

Najpierw tworzę ograniczenie proporcji:

najwyższy aspekt

Następnie tworzę równe ograniczenia szerokości i wysokości z kontenerem:

górny równy rozmiar

Muszę edytować te ograniczenia, aby były mniejsze lub równe:

górny mniejszy lub równy rozmiar

Następnie muszę utworzyć inny zestaw równych ograniczeń szerokości i wysokości z kontenerem:

znowu najlepszy równy rozmiar

I muszę sprawić, by te nowe ograniczenia były mniejsze niż wymagany priorytet:

równa góra nie jest wymagana

Na koniec poprosiłeś o wyśrodkowanie widoku podrzędnego w jego kontenerze, więc ustawię te ograniczenia:

najlepiej wyśrodkowany

Teraz, aby przetestować, wybiorę kontroler widoku i poproszę Xcode o aktualizację wszystkich ramek. Oto co otrzymuję:

niepoprawny górny układ

Ups! Widok podrzędny został rozszerzony, aby całkowicie wypełnić jego pojemnik. Jeśli go wybiorę, widzę, że faktycznie zachował swój współczynnik kształtu, ale robi wypełnienie aspektu zamiast dopasowania kształtu .

Problem polega na tym, że w przypadku ograniczenia mniejszego lub równego ważne jest, który widok jest na każdym końcu ograniczenia, a Xcode ustawił ograniczenie przeciwnie do moich oczekiwań. Mogłem wybrać każde z dwóch ograniczeń i odwrócić jego pierwszy i drugi element. Zamiast tego po prostu wybiorę widok podrzędny i zmienię ograniczenia, aby były większe niż lub równe:

napraw górne ograniczenia

Xcode aktualizuje układ:

prawidłowy górny układ

Teraz robię te same rzeczy w ciemnozielonym widoku na dole. Muszę się upewnić, że jego proporcje wynoszą 1: 4 (Xcode zmienił rozmiar w dziwny sposób, ponieważ nie miał ograniczeń). Nie będę więcej pokazywał kroków, ponieważ są one takie same. Oto wynik:

prawidłowy układ górny i dolny

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

test iPhone 4s

I mogę przetestować w symulatorze iPhone'a 6:

test iPhone'a 6

I przesłaniu moje ostateczne storyboard do tego GIST dla Twojej wygody.


27
Użyłem LICEcap . Jest bezpłatny (GPL) i nagrywa bezpośrednio do formatu GIF. Tak długo, jak animowany plik GIF ma nie więcej niż 2 MB, możesz opublikować go na tej stronie, jak każdy inny obraz.
rob mayoff

1
@LucaTorella Z ograniczeniami o wysokim priorytecie układ jest niejednoznaczny. To, że dzisiaj uzyska pożądany wynik, nie oznacza, że ​​będzie w następnej wersji iOS. Załóżmy, że widok podrzędny chce proporcji 1: 1, rozmiar podglądu ma rozmiar 99 × 100, a masz tylko wymagane proporcje i ograniczenia równości o wysokim priorytecie. Następnie układ automatyczny może ustawić widok podrzędny na rozmiar 99 × 99 (z całkowitym błędem 1) lub rozmiar 100 × 100 (z całkowitym błędem 1). Jeden rozmiar jest dopasowany do kształtu; drugi to aspekt-wypełnienie. Dodanie wymaganych ograniczeń tworzy unikalne rozwiązanie najmniej błędów.
rob mayoff

1
@LucaTorella Dziękujemy za korektę dotyczącą dostępności ograniczeń współczynnika kształtu.
rob mayoff

11
10 000 lepszych aplikacji z jednym postem ... niesamowite.
DonVaughn

2
Wow .. Najlepsza odpowiedź tutaj w przepełnieniu stosu, jaką kiedykolwiek widziałem .. Dziękuję za tego pana ...
0yeoj

24

Rob, twoja odpowiedź jest niesamowita! Wiem również, że to pytanie dotyczy konkretnie osiągnięcia tego celu za pomocą automatycznego układu. Jednak jako odniesienie chciałbym pokazać, jak można to zrobić w kodzie. Ustawiasz widoki górny i dolny (niebieski i różowy), tak jak pokazał Rob. Następnie tworzysz niestandardowy 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 zmień klasę niebieskich i różowych widoków w serii ujęć na AspectFitViews. Wreszcie można ustawić dwa wyloty do swojej viewcontroller topAspectFitViewa bottomAspectFitViewi ustawić swoje childViewsw 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;
}

Nie jest to trudne w kodzie, a mimo to jest bardzo elastyczny i działa z widokami o różnych rozmiarach i różnych proporcjach.

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


1
@DanielGalasko Dziękuję, masz rację. Wspominam o tym w mojej odpowiedzi. Pomyślałem, że użyteczne byłoby odniesienie do porównania, jakie kroki dotyczą obu rozwiązań.
Johannes Fahrenkrug

1
Bardzo pomocne jest również posiadanie referencji programowych.
ctpenrose,

@JohannesFahrenkrug, jeśli masz projekt samouczka, czy możesz go udostępnić? dziękuję :)
Erhan Demirci

1
@ErhanDemirci Pewnie, proszę bardzo: github.com/jfahrenkrug/SPWKAspectFitView
Johannes Fahrenkrug

8

Potrzebowałem rozwiązania z zaakceptowanej odpowiedzi, ale wykonałem z kodu. Najbardziej eleganckim sposobem, jaki znalazłem, jest użycie frameworka 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

To jest dla macOS.

Mam problem ze sposobem Roba, aby osiągnąć dopasowanie kształtu do aplikacji OS X. Ale zrobiłem to w inny sposób - zamiast szerokości i wysokości użyłem spacji wiodących, spływowych, górnej i dolnej.

Zasadniczo dodaj dwie spacje wiodące, gdzie jedna ma> = 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 musisz ustawić proporcje i środek X i środek Y.

A potem praca skończona!

wprowadź opis zdjęcia tutaj wprowadź opis zdjęcia tutaj


Jak możesz zrobić to programowo?
FateNuller

@FateNuller Po prostu postępujesz zgodnie z instrukcjami?
brianLikeApple

4

Stwierdziłem, że chcę zachowywać się przy wypełnianiu aspektu, aby UIImageView zachował swój własny współczynnik kształtu i całkowicie wypełnił widok kontenera. Mylące było to, że mój UIImageView łamał OBA wysokie ograniczenia o równej szerokości i równej wysokości (opisane w odpowiedzi Roba) i renderował w pełnej rozdzielczości.

Rozwiązaniem było po prostu ustawienie Priorytetu odporności na kompresję zawartości UIImageView niższego niż priorytet ograniczeń o jednakowej szerokości i równej wysokości:

Odporność na kompresję treści


2

Jest to znakomita odpowiedź @ rob_mayoff na podejście zorientowane na kod, wykorzystujące NSLayoutAnchorobiekty i przeniesione do Xamarina. Dla mnie NSLayoutAnchori powiązane klasy znacznie ułatwiły programowanie 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

Być może jest to najkrótsza odpowiedź, w przypadku kamieniarstwa , która obsługuje również wypełnianie aspektów i rozciąganie.

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);
        }
    }
}];
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.