A cada ano aumenta a variedade de dispositivos iOS com tamanhos de telas diferentes e assim o Auto Layout se torna cada vez mais importante no desenvolvimento. Já usei muito autoresizingMask e calculei muito frame na mão, mas tenho apreciado cada vez mais o Auto Layout e tenho feito dele minha principal ferramenta de layout. Entretanto, essa semana me deparei com um problema imune ao Auto Layout – talvez devido minha falta de habilidade.
A ideia é ter uma View2 que pertence a um view controller 2 dentro de uma View1, pertencente a um view controller 1. A View2 pode ter um tamanho arbitrário definido pelo view controller 1. A View2 irá conter uma imagem que deve ser centralizada. O tamanho dessa imagem é arbitrário e deve ser redimensionado de maneira que a distância do lado maior mais próximo da borda seja de x
pontos.
Até aí nada de complicado, mas também é necessário que seja possível fazer zoom e scroll da imagem e a margem de x
pontos seja mantida independente da ampliação.
Bom, nossa boa e velha amiga UIScrollView parece ser uma ótima candidata para salvar o dia, mas para isso, precisamos entender melhor com ela funciona. Uma ótima referência é um artigo da edição sobre views do objc.io, Understanding Scroll Views. Vou resumir alguns conceitos básicos e colocar um pouco da minha visão, mas o recomendo a leitura, assim como dos outros artigos dessa edição sobre views. Para entender como uma scroll view funciona, precisamos entender o que significam 3 propriedades: contentOffset, contentSize e contentInset.
contentOffset
O contentOffset define a posição do scroll, isto é, o deslocamento das sub views da scroll view. Na prática, ela é a origin do bounds da scroll view, mas alguém poderia perguntar: a origem não é sempre {0,0}? Nem sempre. Um ponto qualquer ({xS,yS}) de uma subView é convertido para o sistema de coordenadas ({x,y}) de sua super view (view) da seguinte forma:
x = xS + subView.frame.origin.x + view.bounds.origin.x y = yS + subView.frame.origin.y + view.bounds.origin.y
Como normalmente view.bounds.origin = {0,0} para calcular a posição de um ponto qualquer da subView na view é só somar a origem do frame da subView. Isso significa que quando mudamos a origin do bounds de uma view, todas as suas sub views vão ser deslocadas pela mesma quantidade. Truque maroto!
Se consideramos que a UIImageView tem origin = {0,0}, o contentOffset é a distância entre o canto superior esquerdo da image view e o da scroll view, como ilustrado na figura acima.
contentSize
É o tamanho do conteúdo apresentado. No caso da figura, o contentSize é igual o frame.size da image view. Num caso geral, ele só depende do tamanho e disposição das sub views; nunca da scroll view.
contentInset
Usado para definir uma margem para apresentação do conteúdo, por padrão, seu valor é {0,0,0,0}, e portanto o tamanho da área que pode apresentar conteúdo é igual à scrollView.frame.
Quando o contentSize for menor que o tamanho da scroll view, isso significa que as sub views irão ficar no canto superior da esquerda, fixas. Uma maneira de centralizar o conteúdo é colocar um inset como metade da diferença de tamanho entre a scroll view e o contentSize:
GFloat xInset = (CGRectGetWidth(scrollView.frame) - scrollView.contentSize.width)/2.; CGFloat yInset = (CGRectGetHeight(scrollView.frame) - scrollView.contentSize.height)/2.; scrollView.contentInset = UIEdgeInsetsMake(yInset, xInset, yInset, xInset);
Quando o contentSize for maior que que a scroll view, o contentInset define os limites máximos de scroll. Por exemplo no caso da UIImageView que tem a frame.origin = {0,0}, os limites da UIImageView não podem “entrar” na área definida pelo frame da scrool view descontado o contentInset. A figura abaixo deve deixar isso mais claro:
Ok, agora fica fácil escrever o código que adiciona uma imagem à uma scroll view e define essas propriedades corretamente:
- (void)updateWithImage:(UIImage *)image { UIScrollView *scrollView = self.scrollView; UIImageView *imageView = self.imageView; imageView.image = image; CGSize imageSize = image.size; CGRect imageFrame = CGRectMake(0, 0, imageSize.width, imageSize.height); imageView.frame = imageFrame; scrollView.contentSize = imageSize; CGRect scrollViewFrame = scrollView.frame; // Set Inset UIEdgeInsets insets = self.scrollViewDefaultInset; insets = [self insetsForContentFrame:imageFrame insideScrollViewWithFrame:scrollViewFrame withDefaultInsets:insets]; scrollView.contentInset = insets; // Set Zoom CGSize scrollViewSize = CGSizeMake(CGRectGetWidth(scrollViewFrame) - insets.left - insets.right, CGRectGetHeight(scrollViewFrame) - insets.top - insets.bottom); CGFloat xMinZoomScale = scrollViewSize.width/(imageSize.width + 2. * kMargin); CGFloat yMinZoomScale = scrollViewSize.height/(imageSize.height + 2. * kMargin); CGFloat minimumZoomScale = MIN(xMinZoomScale, yMinZoomScale); scrollView.minimumZoomScale = minimumZoomScale; scrollView.maximumZoomScale = minimumZoomScale * kMaxZoomFactor; // Fit on screen scrollView.zoomScale = minimumZoomScale; }
Sendo que função que calcula os insets é:
- (UIEdgeInsets)insetsForContentFrame:(CGRect)contentFrame insideScrollViewWithFrame:(CGRect)scrollViewFrame withDefaultInsets:(UIEdgeInsets)insets { CGSize contentSize = contentFrame.size; CGSize scrollViewSize = CGSizeMake(CGRectGetWidth(scrollViewFrame) - insets.left - insets.right, CGRectGetHeight(scrollViewFrame) - insets.top - insets.bottom); CGFloat margin = kMargin; CGFloat xInset = MAX((scrollViewSize.width - contentSize.width)/2., margin); CGFloat yInset = MAX((scrollViewSize.height - contentSize.height)/2., margin); insets.left += xInset; insets.right += xInset; insets.top += yInset; insets.bottom += yInset; return insets; }
Para habilitar o zoom só falta implementar um método do UIScrollViewDelegate:
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView { return self.imageView; }
Esse método apenas retorna qual a view que será aplicado o zoom. Da primeira vez que vi achei muito estranho, mas entender como a UIScrollView faz o zoom tudo ficou muito mais claro.
Bônus: zoomScale
A UIScrollView faz zoom aplicando uma transformação na view retornada pelo método do delagate descrito acima. Isto é, aplica uma CGAffineTransform do tipo CGAffineTransformMakeScale(zoomScale, zoomScale) na subview. Isso faz com que o frame da subView seja alterado! E portanto, o contentSize da scrollView; por isso sempre que ocontentSize e, consequentemente, o zoomScale forem alterados, o contentInset deve ser recalculado. Isso pode ser feito facilmente implementando mais um método do delegate:
- (void)scrollViewDidZoom:(UIScrollView *)scrollView { UIEdgeInsets insets = self.scrollViewDefaultInset; insets = [self insetsForContentFrame:self.imageView.frame insideScrollViewWithFrame:scrollView.frame withDefaultInsets:insets]; scrollView.contentInset = insets; }
A UIScrollView é uma classe muito importante no UIKit. Seu funcionamento é muito simples, mas entender como ela funciona exatamente pode não ser uma tarefa muito simples.
Um exemplo dessa solução funcionando pode ser encontrada no repositório UIScrollView-Center. Qualquer dúvida, críticas e comentários são bem vindos, a maneira mais fácil de me encontrar é no Twitter.