UIScrollView ao resgate, não tão rápido...
20 Sep 2015Bem vindo ao Invariante, este é o nosso primeiro post. A idéia do blog é postar no mínimo dois artigos por mês, se quiser saber mais sobre a gente vá em Sobre.
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 idéia é 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é ai 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 do artigo, assim como os 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:
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 a 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 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
:
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:
Sendo que função que calcula os insets é:
Para habilitar o zoom só falta implementar um método do UIScrollViewDelegate
:
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 o contentSize
e, consequentemente, o zoomScale
forem alterados o contentInset
deve ser recalculado. Isso pode ser feito facilmente implementando mais um método do delegate:
A UIScrollView
é uma classe muito importante no UIKit
, seu funcionamento é muito simples, mas entender como ela exatamente funciona 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.
Diogo Tridapalli
@diogot