Emulando o comportamento de ajuste de aspecto usando restrições de AutoLayout no Xcode 6

336

Desejo usar o AutoLayout para dimensionar e distribuir uma exibição de uma maneira que seja semelhante ao modo de conteúdo de ajuste de aspecto do UIImageView.

Eu tenho uma subvisão dentro de uma exibição de contêiner no Interface Builder. A subvisualização possui algumas proporções inerentes que desejo respeitar. O tamanho da visualização do contêiner é desconhecido até o tempo de execução.

Se a proporção da visualização da contêiner for maior que a subvisão, quero que a altura da subvisão seja igual à altura da visualização pai.

Se a proporção da exibição da contêiner for mais alta que a subvisualização, desejo que a largura da subvisualização seja igual à largura da visualização pai.

Em ambos os casos, desejo que a subvisão seja centralizada horizontal e verticalmente na visualização do contêiner.

Existe uma maneira de conseguir isso usando restrições de AutoLayout no Xcode 6 ou na versão anterior? Idealmente, usando o Interface Builder, mas se não for possível, é possível definir tais restrições programaticamente.

chris838
fonte

Respostas:

1046

Você não está descrevendo a escala para ajustar; você está descrevendo o ajuste do aspecto. (Editei sua pergunta a esse respeito.) A subvisão se torna a maior possível, mantendo sua proporção e ajustando-se inteiramente ao pai.

De qualquer forma, você pode fazer isso com o layout automático. Você pode fazer isso totalmente no IB a partir do Xcode 5.1. Vamos começar com algumas visualizações:

algumas visualizações

A exibição em verde claro tem uma proporção de 4: 1. A vista verde escura tem uma proporção de 1: 4. Vou configurar restrições para que a visualização azul preencha a metade superior da tela, a visualização rosa preencha a metade inferior da tela, e cada visualização verde se expanda o máximo possível, mantendo a proporção e a adequação à sua recipiente.

Primeiro, criarei restrições nos quatro lados da vista azul. Vou fixá-lo ao seu vizinho mais próximo em cada borda, com uma distância de 0. Certifique-se de desativar as margens:

restrições azuis

Observe que eu não atualizo o quadro ainda. Acho mais fácil deixar espaço entre as visualizações ao configurar restrições e apenas definir as constantes como 0 (ou o que for) manualmente.

Em seguida, fixo as bordas esquerda, inferior e direita da visualização rosa no vizinho mais próximo. Não preciso configurar uma restrição de borda superior porque sua borda superior já está restrita à borda inferior da visualização azul.

restrições cor de rosa

Também preciso de uma restrição de alturas iguais entre as vistas rosa e azul. Isso fará com que cada um preencha metade da tela:

insira a descrição da imagem aqui

Se eu disser ao Xcode para atualizar todos os quadros agora, recebo o seguinte:

recipientes dispostos

Portanto, as restrições que estabeleci até agora estão corretas. Eu desfiz isso e começo a trabalhar na visualização verde clara.

O ajuste da aparência da visualização em verde claro requer cinco restrições:

  • Uma restrição de proporção de prioridade necessária na exibição verde clara. Você pode criar essa restrição em um xib ou storyboard com o Xcode 5.1 ou posterior.
  • Uma restrição de prioridade exigida que limita a largura da vista verde clara a ser menor ou igual à largura do seu contêiner.
  • Uma restrição de alta prioridade que define a largura da exibição em verde claro como igual à largura do seu contêiner.
  • Uma restrição de prioridade necessária que limita a altura da vista verde clara a ser menor ou igual à altura do seu contêiner.
  • Uma restrição de alta prioridade que define a altura da vista verde clara como igual à altura do contêiner.

Vamos considerar as duas restrições de largura. A restrição menor que ou igual, por si só, não é suficiente para determinar a largura da visualização em verde claro; muitas larguras caberão na restrição. Como existe ambiguidade, o autolayout tentará escolher uma solução que minimize o erro na outra restrição (de alta prioridade, mas não necessária). Minimizar o erro significa tornar a largura o mais próximo possível da largura do contêiner, sem violar a restrição menor ou igual à necessária.

O mesmo acontece com a restrição de altura. E como a restrição de proporção também é necessária, ela só pode maximizar o tamanho da subvisão ao longo de um eixo (a menos que o contêiner tenha a mesma proporção que a subvisão).

Então, primeiro eu crio a restrição de proporção:

aspecto superior

Em seguida, crio restrições iguais de largura e altura com o contêiner:

tamanho igual superior

Preciso editar essas restrições para serem menores ou iguais:

top tamanho menor ou igual

Em seguida, preciso criar outro conjunto de restrições iguais de largura e altura com o contêiner:

top tamanho igual novamente

E preciso tornar essas novas restrições abaixo da prioridade necessária:

superior igual não obrigatório

Por fim, você solicitou que a subvisão seja centralizada em seu contêiner, então eu configurarei essas restrições:

topo centrado

Agora, para testar, selecionarei o controlador de exibição e pedirei ao Xcode para atualizar todos os quadros. Isto é o que eu recebo:

layout superior incorreto

Opa! A subvisão foi expandida para preencher completamente seu contêiner. Se eu selecioná-lo, posso ver que, de fato, ele mantém sua proporção, mas está preenchendo um aspecto em vez de um ajuste de aspecto .

O problema é que, com uma restrição menor ou igual, importa qual é a visão em cada extremidade da restrição, e o Xcode configurou a restrição oposta à minha expectativa. Eu poderia selecionar cada uma das duas restrições e reverter seus primeiro e segundo itens. Em vez disso, vou apenas selecionar a subvisão e alterar as restrições para serem maiores que ou iguais:

corrigir as principais restrições

O Xcode atualiza o layout:

layout superior correto

Agora, faço as mesmas coisas na visualização verde escura na parte inferior. Eu preciso ter certeza de que a proporção é de 1: 4 (o Xcode o redimensionou de uma maneira estranha, pois não tinha restrições). Não mostrarei as etapas novamente, pois são iguais. Aqui está o resultado:

layout superior e inferior correto

Agora posso executá-lo no simulador do iPhone 4S, que possui um tamanho de tela diferente do utilizado pelo IB, e testar a rotação:

teste do iphone 4s

E posso testar no simulador do iPhone 6:

teste do iphone 6

Enviei meu storyboard final para esta essência para sua conveniência.

Rob Mayoff
fonte
27
Eu usei o LICEcap . É gratuito (GPL) e grava diretamente no GIF. Contanto que o GIF animado não tenha mais de 2 MB, você poderá publicá-lo neste site como qualquer outra imagem.
Rob mayoff
11
@LucaTorella Com apenas as restrições de alta prioridade, o layout é ambíguo. Só porque obtém o resultado desejado hoje, não significa que será na próxima versão do iOS. Suponha que a subvisão deseje a proporção 1: 1, a super visão tenha tamanho 99 × 100 e você tenha apenas as relações de aspecto e restrições de igualdade de alta prioridade necessárias. O layout automático pode definir a subvisão para o tamanho 99 × 99 (com erro total 1) ou tamanho 100 × 100 (com erro total 1). Um tamanho é adequado ao aspecto; o outro é preenchimento de aspecto. Adicionar as restrições necessárias produz uma solução exclusiva de menor erro.
Rob mayoff
11
@LucaTorella Obrigado pela correção referente à disponibilidade de restrições de proporção.
Rob mayoff
11
10.000 aplicativos melhores com apenas um post ... incrível.
precisa saber é o seguinte
2
Uau .. A melhor resposta aqui no estouro de pilha que eu já vi .. Obrigado por este senhor .. #
0yeoj
24

Rob, sua resposta é incrível! Eu também sei que esta pergunta é especificamente sobre como fazer isso usando o layout automático. No entanto, apenas como referência, gostaria de mostrar como isso pode ser feito no código. Você configura as vistas superior e inferior (azul e rosa), como Rob mostrou. Então você cria um costume 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

Em seguida, você altera a classe das visualizações azul e rosa no storyboard para AspectFitViews. Finalmente você definir duas saídas para o seu viewcontroller topAspectFitViewe bottomAspectFitViewe definir suas childViews em 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;
}

Portanto, não é difícil fazer isso no código e ainda é muito adaptável e funciona com visualizações de tamanho variável e proporções diferentes.

Atualização em julho de 2015 : encontre um aplicativo de demonstração aqui: https://github.com/jfahrenkrug/SPWKAspectFitView

Johannes Fahrenkrug
fonte
11
@DanielGalasko Obrigado, você está correto. Menciono isso na minha resposta. Eu apenas pensei que seria uma referência útil para comparar quais etapas estão envolvidas em ambas as soluções.
Johannes Fahrenkrug 21/10
11
É muito útil ter também a referência programática.
Ctpenrose
@JohannesFahrenkrug se você tiver um projeto tutorial, pode compartilhar? obrigado :)
Erhan Demirci
11
@ErhanDemirci Claro, aqui vai: github.com/jfahrenkrug/SPWKAspectFitView
Johannes Fahrenkrug
8

Eu precisava de uma solução da resposta aceita, mas executada a partir do código. A maneira mais elegante que encontrei é usar a estrutura de alvenaria .

#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);
}];
Tomasz Bąk
fonte
5

Isso é para o macOS.

Estou com problemas para usar a maneira de Rob de obter um ajuste de aspecto no aplicativo OS X. Mas fiz isso de outra maneira - em vez de usar largura e altura, usei espaço inicial, final, superior e inferior.

Basicamente, adicione dois espaços à esquerda onde um é> = 0 @ 1000 de prioridade necessária e outro é = 0 @ 250 de baixa prioridade. Faça as mesmas configurações para o espaço final, superior e inferior.

Obviamente, você precisa definir a proporção e o centro X e o centro Y.

E então o trabalho está feito!

insira a descrição da imagem aqui insira a descrição da imagem aqui

brianLikeApple
fonte
Como você pode fazer isso programaticamente?
FateNuller
@FateNuller Basta seguir os passos?
BrianLikeApple
4

Eu me vi querendo um comportamento de preenchimento de aspecto para que um UIImageView mantivesse sua própria proporção e preenchesse completamente a exibição do contêiner. De maneira confusa, meu UIImageView estava quebrando AMBAS as restrições de alta prioridade e largura e altura iguais (descritas na resposta de Rob) e renderizando em resolução total.

A solução foi simplesmente definir a prioridade de resistência à compactação de conteúdo do UIImageView menor que a prioridade das restrições de largura e altura iguais:

Resistência à compressão de conteúdo

Gregor
fonte
2

Esta é uma porta da excelente resposta do @ rob_mayoff para uma abordagem centrada em código, usando NSLayoutAnchorobjetos e portados para o Xamarin. Para mim, NSLayoutAnchore as classes relacionadas tornaram o AutoLayout muito mais fácil de programar:

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 ();
    }
}
Larry OBrien
fonte
1

Talvez essa seja a resposta mais curta, com a Maçonaria , que também suporta o preenchimento e alongamento de aspectos.

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);
        }
    }
}];
Mr. Ming
fonte