Centralize o conteúdo do UIScrollView quando menor

140

Eu tenho um UIImageViewinterior UIScrollViewque eu uso para aplicar zoom e rolagem. Se a imagem / o conteúdo da exibição de rolagem for maior que a exibição de rolagem, tudo funcionará bem. No entanto, quando a imagem se torna menor que a exibição de rolagem, ela fica no canto superior esquerdo da exibição de rolagem. Gostaria de mantê-lo centralizado, como o aplicativo Fotos.

Alguma idéia ou exemplo sobre como manter o conteúdo do UIScrollViewcentro quando menor?

Estou trabalhando com o iPhone 3.0.

O código a seguir quase funciona. A imagem retornará ao canto superior esquerdo se eu apertá-la depois de atingir o nível mínimo de zoom.

- (void)loadView {
    [super loadView];

    // set up main scroll view
    imageScrollView = [[UIScrollView alloc] initWithFrame:[[self view] bounds]];
    [imageScrollView setBackgroundColor:[UIColor blackColor]];
    [imageScrollView setDelegate:self];
    [imageScrollView setBouncesZoom:YES];
    [[self view] addSubview:imageScrollView];

    UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"WeCanDoIt.png"]];
    [imageView setTag:ZOOM_VIEW_TAG];
    [imageScrollView setContentSize:[imageView frame].size];
    [imageScrollView addSubview:imageView];

    CGSize imageSize = imageView.image.size;
    [imageView release];

    CGSize maxSize = imageScrollView.frame.size;
    CGFloat widthRatio = maxSize.width / imageSize.width;
    CGFloat heightRatio = maxSize.height / imageSize.height;
    CGFloat initialZoom = (widthRatio > heightRatio) ? heightRatio : widthRatio;

    [imageScrollView setMinimumZoomScale:initialZoom];
    [imageScrollView setZoomScale:1];

    float topInset = (maxSize.height - imageSize.height) / 2.0;
    float sideInset = (maxSize.width - imageSize.width) / 2.0;
    if (topInset < 0.0) topInset = 0.0;
    if (sideInset < 0.0) sideInset = 0.0;
    [imageScrollView setContentInset:UIEdgeInsetsMake(topInset, sideInset, -topInset, -sideInset)];
}

- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
    return [imageScrollView viewWithTag:ZOOM_VIEW_TAG];
}

/************************************** NOTE **************************************/
/* The following delegate method works around a known bug in zoomToRect:animated: */
/* In the next release after 3.0 this workaround will no longer be necessary      */
/**********************************************************************************/
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale {
    [scrollView setZoomScale:scale+0.01 animated:NO];
    [scrollView setZoomScale:scale animated:NO];
    // END Bug workaround

    CGSize maxSize = imageScrollView.frame.size;
    CGSize viewSize = view.frame.size;
    float topInset = (maxSize.height - viewSize.height) / 2.0;
    float sideInset = (maxSize.width - viewSize.width) / 2.0;
    if (topInset < 0.0) topInset = 0.0;
    if (sideInset < 0.0) sideInset = 0.0;
    [imageScrollView setContentInset:UIEdgeInsetsMake(topInset, sideInset, -topInset, -sideInset)];
}
hpique
fonte
Você já resolveu esse problema completamente? Estou lutando com o mesmo problema.
Jonas
1
Atenção: Use o valor inicialZoom para calcular a inserção, se NÃO for um (sem zoom). Por exemplo, use estas linhas: float topInset = (maxSize.height - imageSize.height * initialZoom) / 2.0; float sideInset = (maxSize.width - imageSize.width * initialZoom) / 2.0; e finalmente defina o valor inicial do zoom [imageScrollView setZoomScale: initialZoom];
Felix

Respostas:

228

Eu tenho uma solução muito simples! Tudo o que você precisa é atualizar o centro da sua subvisão (visualização de imagem) enquanto amplia o ScrollViewDelegate. Se a imagem ampliada for menor que a visualização de rolagem, ajuste o subview.center else center é (0,0).

- (void)scrollViewDidZoom:(UIScrollView *)scrollView 
{
    UIView *subView = [scrollView.subviews objectAtIndex:0];

    CGFloat offsetX = MAX((scrollView.bounds.size.width - scrollView.contentSize.width) * 0.5, 0.0);
    CGFloat offsetY = MAX((scrollView.bounds.size.height - scrollView.contentSize.height) * 0.5, 0.0);

    subView.center = CGPointMake(scrollView.contentSize.width * 0.5 + offsetX, 
                                 scrollView.contentSize.height * 0.5 + offsetY);
}
Erdemus
fonte
5
Para mim, esta solução é mais instável do que usar o NYOBetterZoom de Liam. Talvez dependa do tamanho da imagem, etc. A moral; usar a solução que melhor se adapte às suas necessidades
wuf810
2
Stackoverflow GOLD STAR para isso. Eu estava lutando com isso, mas a solução é muito simples.
n13 01/03
2
para simplificar um pouco:CGFloat offsetX = MAX((scrollView.bounds.size.width - scrollView.contentSize.width) * 0.5, 0.0);
Marek R
6
Esse ajuste me ajudou quando o scrollview tinha inserções: CGFloat offsetX = MAX((scrollView.bounds.size.width - scrollView.contentInset.left - scrollView.contentInset.right - scrollView.contentSize.width) * 0.5, 0.0); CGFloat offsetY = MAX((scrollView.bounds.size.height - scrollView.contentInset.top - scrollView.contentInset.bottom - scrollView.contentSize.height) * 0.5, 0.0);
Kieran Harper
3
Percebi que isso, como muitas outras técnicas de centralização, parece sofrer de problemas ao usar zoomToRect:. O uso da contentInsetabordagem funciona melhor se você precisar dessa funcionalidade. Consulte petersteinberger.com/blog/2013/how-to-center-uiscrollview para obter mais detalhes.
Matej Bukovinski 26/01
51

A resposta de @ EvelynCordner foi a que funcionou melhor no meu aplicativo. Muito menos código do que as outras opções também.

Aqui está a versão Swift, se alguém precisar:

func scrollViewDidZoom(_ scrollView: UIScrollView) {
    let offsetX = max((scrollView.bounds.width - scrollView.contentSize.width) * 0.5, 0)
    let offsetY = max((scrollView.bounds.height - scrollView.contentSize.height) * 0.5, 0)
    scrollView.contentInset = UIEdgeInsetsMake(offsetY, offsetX, 0, 0)
}
William T.
fonte
4
Este funciona muito bem! A vista foi animada corretamente depois de encolher.
Carter Medlin
4
Coloquei esse código em uma função e também o chamei em viewDidLayoutSubviews para garantir que ele foi configurado corretamente inicialmente.
Carter Medlin
Boa ligação @CarterMedlin me ajudou muito com minha carga inicial do Swift 3.
AJ Hernandez
Isso parece funcionar, mas não consigo entender o porquê, pois sempre parece calcular os mesmos detalhes: os limites da exibição de rolagem são fixos, assim como o tamanho do conteúdo.
Vaddadi Kartick
As alterações contentSize ao aplicar zoom no scrollView O tamanho é fixo apenas
Michał Ziobro
25

Ok, eu estive lutando contra isso nos últimos dois dias e, finalmente, tendo chegado a uma solução bastante confiável (até agora ...), achei que deveria compartilhá-lo e evitar outras dores. :) Se você encontrar algum problema com esta solução, grite!

Eu basicamente passei pelo que todo mundo tem: pesquisando no StackOverflow, os Fóruns de desenvolvedores da Apple, procurei o código por three20, ScrollingMadness, ScrollTestSuite, etc. Tentei ampliar o quadro UIImageView, jogando com o deslocamento e / ou inserções do UIScrollView do ViewController, etc., mas nada funcionou muito bem (como todo mundo descobriu também).

Depois de dormir, tentei alguns ângulos alternativos:

  1. Subclassificar o UIImageView para alterar seu próprio tamanho dinamicamente - isso não funcionou bem.
  2. Subclassificar o UIScrollView para que ele altere seu próprio contentOffset dinamicamente - esse é o que parece ser um vencedor para mim.

Com esse método UIScrollView de subclasse, estou substituindo o mutador contentOffset, para que ele não esteja configurando {0,0} quando a imagem é dimensionada menor que a viewport - em vez disso, está configurando o deslocamento para que a imagem seja mantida centralizada na viewport. Até agora, parece sempre funcionar. Verifiquei-o com imagens amplas, altas, minúsculas e grandes e não possui o problema "funciona, mas aperte com o zoom mínimo".

Fiz upload de um projeto de exemplo no github que usa essa solução, você pode encontrá-lo aqui: http://github.com/nyoron/NYOBetterZoom

Liam Jones
fonte
2
Para os interessados ​​- atualizei o projeto vinculado acima para que fique um pouco menos dependente do ViewController fazer a "coisa certa", o próprio UIScrollView personalizado cuida de mais detalhes.
Liam Jones
2
Liam, você é demais. Estou dizendo isso como um autor de ScrollingMadness. BTW no iPad na configuração 3.2+ contentInset em scrollViewDidZoom (um novo método de delegação 3.2+) Just Works.
Andrey Tarantsov
Código muito arrumado. Estou coçando a cabeça com isso há um tempo. Adicionado o projeto para handyiphonecode.com
samvermette
Isso é ótimo. Obrigado. Não foi compensando meu UIStatusBar sendo escondido embora, então eu mudei a linha anOffset.y = -(scrollViewSize.height - zoomViewSize.height) / 2.0paraanOffset.y = (-(scrollViewSize.height - zoomViewSize.height) / 2.0) + 10;
Gordon Fontenot
A solução dada bi Erdemus é muito mais simples na minha opinião.
gyozo kudor 02/09
23

Esse código deve funcionar na maioria das versões do iOS (e foi testado para funcionar na versão 3.1 acima).

É baseado no código WWDC da Apple para o fotocoller.

Adicione o seguinte à sua subclasse de UIScrollView e substitua tileContainerView pela exibição que contém sua imagem ou blocos:

- (void)layoutSubviews {
    [super layoutSubviews];

    // center the image as it becomes smaller than the size of the screen
    CGSize boundsSize = self.bounds.size;
    CGRect frameToCenter = tileContainerView.frame;

    // center horizontally
    if (frameToCenter.size.width < boundsSize.width)
        frameToCenter.origin.x = (boundsSize.width - frameToCenter.size.width) / 2;
    else
        frameToCenter.origin.x = 0;

    // center vertically
    if (frameToCenter.size.height < boundsSize.height)
        frameToCenter.origin.y = (boundsSize.height - frameToCenter.size.height) / 2;
    else
        frameToCenter.origin.y = 0;

    tileContainerView.frame = frameToCenter;
}
JosephH
fonte
3
essa deve ser a resposta aceita, porque é mais simples. Obrigado. Você salvou minha vida!!!!!! : D
Duck
Depois de remover todas as chamadas da API do iOS 3.2+, a lógica de centralização parece funcionar apenas em dispositivos com iOS 3.2+ e não 3.1.3 (irregular, trêmulo, obtém deslocamento aleatório). Comparei as saídas de tamanho e origem do quadro entre as versões 3.1.3 e 3.2+ e, embora correspondam, por algum motivo, a exibição filho ainda é posicionada incorretamente. Muito estranho. Apenas a resposta de Liam Jone funcionou para mim.
David H
1
As soluções @Erdemus e @JosephH funcionam, mas a UIScrollViewabordagem da subclasse parece preferível: é invocada quando a exibição é exibida pela primeira vez + continuamente enquanto o zoom está em andamento (enquanto scrollViewDidZoomé invocado apenas uma vez por rolagem e após o fato)
SwiftArchitect
22

Para uma solução mais adequada para exibições de rolagem que usam o layout automático, use inserções de conteúdo da exibição de rolagem em vez de atualizar os quadros das sub-visualizações da exibição de rolagem.

- (void)scrollViewDidZoom:(UIScrollView *)scrollView
{
    CGFloat offsetX = MAX((scrollView.bounds.size.width - scrollView.contentSize.width) * 0.5, 0.0);
    CGFloat offsetY = MAX((scrollView.bounds.size.height - scrollView.contentSize.height) * 0.5, 0.0);

    self.scrollView.contentInset = UIEdgeInsetsMake(offsetY, offsetX, 0.f, 0.f);
}
Evelyn Cordner
fonte
1
Isso funcionou para mim, no entanto, se a imagem for menor, de alguma forma, esse código (quando chamado diretamente) não atualizou a visualização de rolagem. O que eu precisava TODO foi pela primeira vez o UIViewque está dentro do UIScrollviewpara o mesmo centro ( view.center = scrollview.center;) e, em seguida, no scrollViewDidZoomconjunto do view.frame.origin xe ypara 0novamente.
precisa saber é o seguinte
Funciona bem para mim também, mas eu fiz dispatch_asynca fila principal na viewWillAppearqual chamo scrollViewDidZoom:na minha visualização principal de rolagem. Isso faz a exibição aparecer com imagens centralizadas.
Pascal
Faça uma chamada para esse código viewDidLayoutSubviewspara garantir que ele esteja configurado corretamente quando a exibição aparecer pela primeira vez.
Carter Medlin
21

Atualmente, estou subclassificando UIScrollViewe substituindo setContentOffset:para ajustar o deslocamento com base em contentSize. Funciona com pitada e zoom programático.

@implementation HPCenteringScrollView

- (void)setContentOffset:(CGPoint)contentOffset
{
    const CGSize contentSize = self.contentSize;
    const CGSize scrollViewSize = self.bounds.size;

    if (contentSize.width < scrollViewSize.width)
    {
        contentOffset.x = -(scrollViewSize.width - contentSize.width) / 2.0;
    }

    if (contentSize.height < scrollViewSize.height)
    {
        contentOffset.y = -(scrollViewSize.height - contentSize.height) / 2.0;
    }

    [super setContentOffset:contentOffset];
}

@end

Além de ser curto e agradável, esse código produz um zoom muito mais suave que a solução @Erdemus. Você pode vê-lo em ação na demonstração do RMGallery .

hpique
fonte
6
Você não precisa subclassificar UIScrollView para implementar esse método. A Apple permite que você veja eventos de rolagem e zoom nos métodos de delegação. Especificamente: - (vazio) scrollViewDidZoom: (UIScrollView *) scrollView;
Rog
1
Melhor solução que eu já vi (funcione mesmo ao usar restrições).
Frizlab
12

Passei um dia lutando com esse problema e acabei implementando o scrollViewDidEndZooming: withView: atScale: da seguinte maneira:

- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale {
    CGFloat screenWidth = [[UIScreen mainScreen] bounds].size.width;
    CGFloat screenHeight = [[UIScreen mainScreen] bounds].size.height;
    CGFloat viewWidth = view.frame.size.width;
    CGFloat viewHeight = view.frame.size.height;

    CGFloat x = 0;
    CGFloat y = 0;

    if(viewWidth < screenWidth) {
        x = screenWidth / 2;
    }
    if(viewHeight < screenHeight) {
        y = screenHeight / 2 ;
    }

    self.scrollView.contentInset = UIEdgeInsetsMake(y, x, y, x);
}

Isso garante que, quando a imagem for menor que a tela, ainda haja espaço suficiente ao redor para que você possa posicioná-la no local exato que deseja.

(supondo que seu UIScrollView contenha um UIImageView para manter a imagem)

Basicamente, o que isso faz é verificar se a largura / altura da visualização da imagem é menor que a largura / altura da tela e, em caso afirmativo, criar uma inserção de metade da largura / altura da tela (você provavelmente poderá aumentar isso se quiser que a imagem sair dos limites da tela).

Observe que, como esse é um método UIScrollViewDelegate , não se esqueça de adicioná-lo à declaração do seu controlador de exibição, para evitar problemas de compilação.

Vicarius
fonte
7

A Apple lançou os vídeos da sessão da WWDC de 2010 para todos os membros do programa de desenvolvedor do iphone. Um dos tópicos discutidos é como eles criaram o aplicativo de fotos !!! Eles criam um aplicativo muito semelhante passo a passo e disponibilizam todo o código gratuitamente.

Também não usa API privada. Não posso colocar nenhum código aqui por causa do contrato de não divulgação, mas aqui está um link para o download do código de amostra. Você provavelmente precisará fazer login para obter acesso.

http://connect.apple.com/cgi-bin/WebObjects/MemberSite.woa/wa/getSoftware?code=y&source=x&bundleID=20645

E, aqui está um link para a página WWDC do iTunes:

http://insideapple.apple.com/redir/cbx-cgi.do?v=2&la=en&lc=&a=kGSol9sgPHP%2BtlWtLp%2BEP%2FnxnZarjWJglPBZRHd3oDbACudP51JNGS8KlsFgxZto9X%2BTsnqSbeUSWX0doe%2Fzv%2FN5XV55%2FomsyfRgFBysOnIVggO%2Fn2p%2BiweDK%2F%2FmsIXj

Jonah
fonte
1
O exemplo em questão é MyImagePicker e, hilariamente, exibe esse mesmo problema.
Colin Barrett
1
Eu deveria ter sido mais claro. O exemplo em questão é realmente "PhotoScroller" e não "MyImagePicker". Você está certo de que "MyImagePicker" não funciona corretamente. Mas, "PhotoScroller" faz. Tente.
Jonah
Você se lembra talvez do título do vídeo da WWDC onde eles discutem o Photoscroller?
Grzegorz Adam Hankiewicz
1
Chama-se "Design de aplicativos com vistas de rolagem".
Jonah
2

Ok, esta solução está funcionando para mim. Eu tenho uma subclasse de UIScrollViewcom uma referência ao UIImageViewque está exibindo. Sempre que o UIScrollViewzoom é aplicado, a propriedade contentSize é ajustada. É no levantador que eu dimensiono UIImageViewadequadamente e também ajusto sua posição central.

-(void) setContentSize:(CGSize) size{
CGSize lSelfSize = self.frame.size;
CGPoint mid;
if(self.zoomScale >= self.minimumZoomScale){
    CGSize lImageSize = cachedImageView.initialSize;
    float newHeight = lImageSize.height * self.zoomScale;

    if (newHeight < lSelfSize.height ) {
        newHeight = lSelfSize.height;
    }
    size.height = newHeight;

    float newWidth = lImageSize.width * self.zoomScale;
    if (newWidth < lSelfSize.width ) {
        newWidth = lSelfSize.width;
    }
    size.width = newWidth;
    mid = CGPointMake(size.width/2, size.height/2);

}
else {
    mid = CGPointMake(lSelfSize.width/2, lSelfSize.height/2);
}

cachedImageView.center = mid;
[super  setContentSize:size];
[self printLocations];
NSLog(@"zoom %f setting size %f x %f",self.zoomScale,size.width,size.height);
}

Sempre eu coloco a imagem no UIScrollViewredimensiono. O UIScrollViewno scrollview também é uma classe personalizada que eu criei.

-(void) resetSize{
    if (!scrollView){//scroll view is view containing imageview
        return;
    }

    CGSize lSize = scrollView.frame.size;

    CGSize lSelfSize = self.image.size; 
    float lWidth = lSize.width/lSelfSize.width;
    float lHeight = lSize.height/lSelfSize.height;

    // choose minimum scale so image width fits screen
    float factor  = (lWidth<lHeight)?lWidth:lHeight;

    initialSize.height = lSelfSize.height  * factor;
    initialSize.width = lSelfSize.width  * factor;

    [scrollView setContentSize:lSize];
    [scrollView setContentOffset:CGPointZero];
    scrollView.userInteractionEnabled = YES;
}

Com esses dois métodos, sou capaz de ter uma visão que se comporta exatamente como o aplicativo de fotos.

AHA
fonte
Isso parece funcionar, e é uma solução bastante simples. Algumas transições de zoom não são perfeitas, mas acredito que podem ser corrigidas. Vou experimentar um pouco mais.
Hpique #
A solução estava clara antes da atualização. Agora é um pouco confuso. Você pode esclarecer?
Hpique
2

A maneira como fiz isso é adicionar uma visão extra à hierarquia:

UIScrollView -> UIView -> UIImageView

UIViewa mesma proporção que a sua UIScrollViewe centralize-a UIImageViewnisso.

hatfinch
fonte
Obrigado, hatfinch. Vai tentar isso também. Você pode postar um código de exemplo ou alterar meu código de exemplo para mostrar como você constrói a hierarquia de visualizações?
Hpique
Esse tipo de trabalho. Exceto pelo fato de que, como a UIView é do tamanho do UIScrollView, se a imagem é menor (por exemplo, paisagem em vez de retrato), você pode rolar parte da imagem para fora da tela. O aplicativo de fotos não permite isso e parece muito melhor.
217 Jonah
2

Sei que algumas das respostas acima estão corretas, mas só quero dar minha resposta com algumas explicações. Os comentários farão você entender por que fazemos isso.

Quando carrego o scrollView pela primeira vez, escrevo o código a seguir para torná-lo central, observe que definimos contentOffsetprimeiro e, em seguida,contentInset

    scrollView.maximumZoomScale = 8
    scrollView.minimumZoomScale = 1

    // set vContent frame
    vContent.frame = CGRect(x: 0,
                            y: 0  ,
                            width: vContentWidth,
                            height: vContentWidth)
    // set scrollView.contentSize
    scrollView.contentSize = vContent.frame.size

    //on the X direction, if contentSize.width > scrollView.bounds.with, move scrollView from 0 to offsetX to make it center(using `scrollView.contentOffset`)
    // if not, don't need to set offset, but we need to set contentInset to make it center.(using `scrollView.contentInset`)
    // so does the Y direction.
    let offsetX = max((scrollView.contentSize.width - scrollView.bounds.width) * 0.5, 0)
    let offsetY = max((scrollView.contentSize.height - scrollView.bounds.height) * 0.5, 0)
    scrollView.contentOffset = CGPoint(x: offsetX, y: offsetY)

    let topX = max((scrollView.bounds.width - scrollView.contentSize.width) * 0.5, 0)
    let topY = max((scrollView.bounds.height - scrollView.contentSize.height) * 0.5, 0)
    scrollView.contentInset = UIEdgeInsets(top: topY, left: topX, bottom: 0, right: 0)

Em seguida, quando aperto o vContent, escrevo o código a seguir para torná-lo central.

func scrollViewDidZoom(_ scrollView: UIScrollView) {
    //we just need to ensure that the content is in the center when the contentSize is less than scrollView.size.
    let topX = max((scrollView.bounds.width - scrollView.contentSize.width) * 0.5, 0)
    let topY = max((scrollView.bounds.height - scrollView.contentSize.height) * 0.5, 0)
    scrollView.contentInset = UIEdgeInsets(top: topY, left: topX, bottom: 0, right: 0)
}
Changwei
fonte
2

Se contentInsetnão for necessário para mais nada, ele pode ser usado para centralizar o conteúdo do scrollview.

class ContentCenteringScrollView: UIScrollView {

    override var bounds: CGRect {
        didSet { updateContentInset() }
    }

    override var contentSize: CGSize {
        didSet { updateContentInset() }
    }

    private func updateContentInset() {
        var top = CGFloat(0)
        var left = CGFloat(0)
        if contentSize.width < bounds.width {
            left = (bounds.width - contentSize.width) / 2
        }
        if contentSize.height < bounds.height {
            top = (bounds.height - contentSize.height) / 2
        }
        contentInset = UIEdgeInsets(top: top, left: left, bottom: top, right: left)
    }
}

Vantagem, se essa abordagem for a que você ainda pode usar contentLayoutGuidepara colocar o conteúdo dentro do scrollview

scrollView.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
    imageView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
    imageView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
    imageView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
    imageView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor)
])

ou simplesmente arraste e solte o conteúdo no Interface Builder do Xcode.

Wojciech Nagrodzki
fonte
1

Você pode observar a contentSizepropriedade do UIScrollView (usando observação de valor-chave ou similar) e ajustar automaticamente contentInsetsempre que as contentSizealterações forem menores que o tamanho da exibição de rolagem.

Tim
fonte
Vai tentar. Isso é possível com os métodos UIScrollViewDelegate, em vez de observar o contentSize?
Hpique
Provavelmente; você usaria - (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale(descrito em developer.apple.com/iphone/library/documentation/UIKit/… ), mas eu prefiro observar contentSizeporque, caso contrário, você acaba descartando a nova escala de zoom e localizando-a de qualquer maneira. (A menos que você pode retirar um pouco de matemática incrível com base nos tamanhos relativos dos seus pontos de vista.)
Tim
Obrigado, Tim. scrollViewDidEndZooming é o que estou usando. Veja o código em questão (eu o adicionei após sua primeira resposta). Isso quase funciona. O único problema que permanece se, se eu beliscar a imagem depois que o ZoomScale mínimo for atingido, ela retornará ao canto superior esquerdo.
Hpique
Você quer dizer que funciona (centraliza a imagem) para beliscar em qualquer escala que não seja a escala mínima de zoom e só quebra se você tentar beliscar novamente quando estiver na escala mínima? Ou não funciona para beliscar na escala mínima?
Tim
O primeiro. Ele centraliza a imagem para beliscar em qualquer escala que não seja a escala mínima de zoom e é interrompida se você tentar beliscar novamente quando estiver na escala mínima. Além disso, quando atinge a escala mínima de zoom na primeira vez, o conteúdo é animado brevemente como se fosse de cima, o que causa um breve efeito estranho. Isso acontece pelo menos no simulador 3.0.
Hpique
1

Uma maneira elegante de centralizar o conteúdo UISCrollViewé essa.

Adicione um observador ao contentSize do seu UIScrollView, para que esse método seja chamado sempre que o conteúdo for alterado ...

[myScrollView addObserver:delegate 
               forKeyPath:@"contentSize"
                  options:(NSKeyValueObservingOptionNew) 
                  context:NULL];

Agora no seu método de observador:

- (void)observeValueForKeyPath:(NSString *)keyPath   ofObject:(id)object   change:(NSDictionary *)change   context:(void *)context { 

    // Correct Object Class.
    UIScrollView *pointer = object;

    // Calculate Center.
    CGFloat topCorrect = ([pointer bounds].size.height - [pointer viewWithTag:100].bounds.size.height * [pointer zoomScale])  / 2.0 ;
            topCorrect = ( topCorrect < 0.0 ? 0.0 : topCorrect );

    topCorrect = topCorrect - (  pointer.frame.origin.y - imageGallery.frame.origin.y );

    // Apply Correct Center.
    pointer.center = CGPointMake(pointer.center.x,
                                 pointer.center.y + topCorrect ); }
  • Você deve mudar o [pointer viewWithTag:100]. Substitua pela sua exibição de conteúdo UIView.

    • Mude também imageGalleryapontando para o tamanho da sua janela.

Isso corrigirá o centro do conteúdo sempre que seu tamanho for alterado.

NOTA: A única maneira de esse conteúdo não funcionar muito bem é com a funcionalidade de zoom padrão do UIScrollView.

Equipe de Desenvolvimento SEQOY
fonte
Não foi possível fazer isso funcionar. Parece que você está centralizando o scrollView e não o conteúdo. Por que o tamanho da janela é importante? O código não deveria corrigir também a posição x?
Hpique
1

Esta é a minha solução para esse problema, que funciona muito bem para qualquer tipo de exibição dentro de uma exibição de rolagem.

-(void)scrollViewDidZoom:(__unused UIScrollView *)scrollView 
    {
    CGFloat top;
    CGFloat left;
    CGFloat bottom;
    CGFloat right;

    if (_scrollView.contentSize.width < scrollView.bounds.size.width) {
        DDLogInfo(@"contentSize %@",NSStringFromCGSize(_scrollView.contentSize));

        CGFloat width = (_scrollView.bounds.size.width-_scrollView.contentSize.width)/2.0;

        left = width;
        right = width;


    }else {
        left = kInset;
        right = kInset;
    }

    if (_scrollView.contentSize.height < scrollView.bounds.size.height) {

        CGFloat height = (_scrollView.bounds.size.height-_scrollView.contentSize.height)/2.0;

        top = height;
        bottom = height;

    }else {
        top = kInset;
        right = kInset;
    }

    _scrollView.contentInset = UIEdgeInsetsMake(top, left, bottom, right);



  if ([self.tiledScrollViewDelegate respondsToSelector:@selector(tiledScrollViewDidZoom:)])
  {
        [self.tiledScrollViewDelegate tiledScrollViewDidZoom:self];
  }
}
beat843796
fonte
1

Existem muitas soluções aqui, mas eu arriscaria colocar aqui a minha. É bom por dois motivos: não atrapalha a experiência de zoom, como faria com a atualização do quadro de exibição de imagem em andamento, e também respeita as inserções de exibição de rolagem originais (por exemplo, definidas no xib ou no storyboard para manuseio gracioso de barras de ferramentas semitransparentes etc.) .

Primeiro, defina um pequeno auxiliar:

CGSize CGSizeWithAspectFit(CGSize containerSize, CGSize contentSize) {
    CGFloat containerAspect = containerSize.width / containerSize.height,
            contentAspect = contentSize.width / contentSize.height;

    CGFloat scale = containerAspect > contentAspect
                    ? containerSize.height / contentSize.height
                    : containerSize.width / contentSize.width;

    return CGSizeMake(contentSize.width * scale, contentSize.height * scale);
}

Para manter inserções originais, campo definido:

UIEdgeInsets originalScrollViewInsets;

E em algum lugar no viewDidLoad preencha:

originalScrollViewInsets = self.scrollView.contentInset;

Para colocar o UIImageView no UIScrollView (assumindo que o próprio UIImage esteja em loadedImage var):

CGSize containerSize = self.scrollView.bounds.size;
containerSize.height -= originalScrollViewInsets.top + originalScrollViewInsets.bottom;
containerSize.width -= originalScrollViewInsets.left + originalScrollViewInsets.right;

CGSize contentSize = CGSizeWithAspectFit(containerSize, loadedImage.size);

UIImageView *imageView = [[UIImageView alloc] initWithFrame:(CGRect) { CGPointZero, contentSize }];
imageView.autoresizingMask = UIViewAutoresizingNone;
imageView.contentMode = UIViewContentModeScaleAspectFit;
imageView.image = loadedImage;

[self.scrollView addSubview:imageView];
self.scrollView.contentSize = contentSize;

[self centerImageViewInScrollView];

scrollViewDidZoom: de UIScrollViewDelegate para essa visualização de rolagem:

- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
    if (scrollView == self.scrollView) {
        [self centerImageViewInScrollView];
    }
}

Um finalmente, centralizando-se:

- (void)centerImageViewInScrollView {
    CGFloat excessiveWidth = MAX(0.0, self.scrollView.bounds.size.width - self.scrollView.contentSize.width),
            excessiveHeight = MAX(0.0, self.scrollView.bounds.size.height - self.scrollView.contentSize.height),
            insetX = excessiveWidth / 2.0,
            insetY = excessiveHeight / 2.0;

    self.scrollView.contentInset = UIEdgeInsetsMake(
            MAX(insetY, originalScrollViewInsets.top),
            MAX(insetX, originalScrollViewInsets.left),
            MAX(insetY, originalScrollViewInsets.bottom),
            MAX(insetX, originalScrollViewInsets.right)
    );
}

Ainda não testei a mudança de orientação (ou seja, a reação adequada para redimensionar o próprio UIScrollView), mas a correção deve ser relativamente fácil.

jazzcat
fonte
1

Você descobrirá que a solução publicada pela Erdemus funciona, mas… Existem alguns casos em que o método scrollViewDidZoom não é chamado e sua imagem fica presa no canto superior esquerdo. Uma solução simples é chamar explicitamente o método quando você exibe inicialmente uma imagem, assim:

[self scrollViewDidZoom: scrollView];

Em muitos casos, você pode invocar esse método duas vezes, mas esta é uma solução mais limpa do que algumas das outras respostas neste tópico.

russes
fonte
1

O Photo Scroller Example da Apple faz exatamente o que você está procurando. Coloque isso na sua subclasse UIScrollView e altere _zoomView para ser seu UIImageView.

-(void)layoutSubviews{
  [super layoutSubviews];
  // center the zoom view as it becomes smaller than the size of the screen
  CGSize boundsSize = self.bounds.size;
  CGRect frameToCenter = self.imageView.frame;
  // center horizontally
  if (frameToCenter.size.width < boundsSize.width){
     frameToCenter.origin.x = (boundsSize.width - frameToCenter.size.width) / 2;
  }else{
    frameToCenter.origin.x = 0;
  }
  // center vertically
  if (frameToCenter.size.height < boundsSize.height){
     frameToCenter.origin.y = (boundsSize.height - frameToCenter.size.height) / 2;
  }else{
    frameToCenter.origin.y = 0;
  }
  self.imageView.frame = frameToCenter; 
}

Código de exemplo do Photo Scroller da Apple

Korey Hinton
fonte
1

Para fazer a animação fluir bem, defina

self.scrollview.bouncesZoom = NO;

e use esta função (localizando o centro usando o método nesta resposta )

- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale {
    [UIView animateWithDuration:0.2 animations:^{
        float offsetX = MAX((scrollView.bounds.size.width-scrollView.contentSize.width)/2, 0);
        float offsetY = MAX((scrollView.bounds.size.height-scrollView.contentSize.height)/2, 0);
        self.imageCoverView.center = CGPointMake(scrollView.contentSize.width*0.5+offsetX, scrollView.contentSize.height*0.5+offsetY);
    }];
}

Isso cria o efeito de salto, mas não envolve movimentos bruscos de antemão.

ldanilek
fonte
1

Apenas a resposta aprovada rapidamente, mas sem subclasse usando o delegado

func centerScrollViewContents(scrollView: UIScrollView) {
    let contentSize = scrollView.contentSize
    let scrollViewSize = scrollView.frame.size;
    var contentOffset = scrollView.contentOffset;

    if (contentSize.width < scrollViewSize.width) {
        contentOffset.x = -(scrollViewSize.width - contentSize.width) / 2.0
    }

    if (contentSize.height < scrollViewSize.height) {
        contentOffset.y = -(scrollViewSize.height - contentSize.height) / 2.0
    }

    scrollView.setContentOffset(contentOffset, animated: false)
}

// UIScrollViewDelegate    
func scrollViewDidZoom(scrollView: UIScrollView) {
    centerScrollViewContents(scrollView)
}
Homem leve
fonte
1

Caso o seu imageView interno tenha uma largura inicial específica (por exemplo, 300) e você queira centralizar sua largura apenas no zoom menor que a largura inicial, isso também poderá ajudá-lo.

 func scrollViewDidZoom(scrollView: UIScrollView){
    if imageView.frame.size.width < 300{
        imageView.center.x = self.view.frame.width/2
    }
  }
dejix
fonte
0

Aqui está a maneira atual de fazer esse trabalho. É melhor, mas ainda não é perfeito. Tente configurar:

 myScrollView.bouncesZoom = YES; 

para corrigir o problema com a visualização não centralizada quando em minZoomScale.

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGSize screenSize = [[self view] bounds].size;//[[UIScreen mainScreen] bounds].size;//
CGSize photoSize = [yourImage size];
CGFloat topInset = (screenSize.height - photoSize.height * [myScrollView zoomScale]) / 2.0;
CGFloat sideInset = (screenSize.width - photoSize.width * [myScrollView zoomScale]) / 2.0;

if (topInset < 0.0)
{ topInset = 0.0; }
if (sideInset < 0.0)
{ sideInset = 0.0; } 
[myScrollView setContentInset:UIEdgeInsetsMake(topInset, sideInset, -topInset, -sideInset)];
ApplicationDelegate *appDelegate = (ApplicationDelegate *)[[UIApplication sharedApplication] delegate];

CGFloat scrollViewHeight; //Used later to calculate the height of the scrollView
if (appDelegate.navigationController.navigationBar.hidden == YES) //If the NavBar is Hidden, set scrollViewHeight to 480
{ scrollViewHeight = 480; }
if (appDelegate.navigationController.navigationBar.hidden == NO) //If the NavBar not Hidden, set scrollViewHeight to 360
{ scrollViewHeight = 368; }

imageView.frame = CGRectMake(0, 0, CGImageGetWidth(yourImage)* [myScrollView zoomScale], CGImageGetHeight(yourImage)* [myScrollView zoomScale]);

[imageView setContentMode:UIViewContentModeCenter];
}

Além disso, faço o seguinte para impedir que a imagem fique do lado após diminuir o zoom.

- (void) scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale {
myScrollView.frame = CGRectMake(0, 0, 320, 420);
 //put the correct parameters for your scroll view width and height above
}
Jonah
fonte
Oi Jonah. Parece que estamos lidando com o mesmo problema há algum tempo. Irá verificar suas duas soluções e responder em breve.
Hpique
Oi Jonah, tentei sua solução mais recente. No entanto, o que é temporaryImage? Eu tentei colocar temporaryImage = imageView.image; no entanto, uma vez que amplio, a imagem desaparece. Obrigado, Pannag
psanketi
Pannag, temporaryImage foi mal nomeado. Deve ser chamado de myImage, pois é exatamente a imagem que você está usando. Desculpe pela confusão.
Jonah
0

Ok, acho que encontrei uma solução muito boa para esse problema. O truque é reajustar constantemente o imageView'squadro. Acho que isso funciona muito melhor do que ajustar constantemente o contentInsetsou contentOffSets. Eu tive que adicionar um pouco de código extra para acomodar as imagens retrato e paisagem.

Aqui está o código:

- (void) scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale {

CGSize screenSize = [[self view] bounds].size;

if (myScrollView.zoomScale <= initialZoom +0.01) //This resolves a problem with the code not working correctly when zooming all the way out.
{
    imageView.frame = [[self view] bounds];
    [myScrollView setZoomScale:myScrollView.zoomScale +0.01];
}

if (myScrollView.zoomScale > initialZoom)
{
    if (CGImageGetWidth(temporaryImage.CGImage) > CGImageGetHeight(temporaryImage.CGImage)) //If the image is wider than tall, do the following...
    {
        if (screenSize.height >= CGImageGetHeight(temporaryImage.CGImage) * [myScrollView zoomScale]) //If the height of the screen is greater than the zoomed height of the image do the following...
        {
            imageView.frame = CGRectMake(0, 0, 320*(myScrollView.zoomScale), 368);
        }
        if (screenSize.height < CGImageGetHeight(temporaryImage.CGImage) * [myScrollView zoomScale]) //If the height of the screen is less than the zoomed height of the image do the following...
        {
            imageView.frame = CGRectMake(0, 0, 320*(myScrollView.zoomScale), CGImageGetHeight(temporaryImage.CGImage) * [myScrollView zoomScale]);
        }
    }
    if (CGImageGetWidth(temporaryImage.CGImage) < CGImageGetHeight(temporaryImage.CGImage)) //If the image is taller than wide, do the following...
    {
        CGFloat portraitHeight;
        if (CGImageGetHeight(temporaryImage.CGImage) * [myScrollView zoomScale] < 368)
        { portraitHeight = 368;}
        else {portraitHeight = CGImageGetHeight(temporaryImage.CGImage) * [myScrollView zoomScale];}

        if (screenSize.width >= CGImageGetWidth(temporaryImage.CGImage) * [myScrollView zoomScale]) //If the width of the screen is greater than the zoomed width of the image do the following...
        {
            imageView.frame = CGRectMake(0, 0, 320, portraitHeight);
        }
        if (screenSize.width < CGImageGetWidth (temporaryImage.CGImage) * [myScrollView zoomScale]) //If the width of the screen is less than the zoomed width of the image do the following...
        {
            imageView.frame = CGRectMake(0, 0, CGImageGetWidth(temporaryImage.CGImage) * [myScrollView zoomScale], portraitHeight);
        }
    }
    [myScrollView setZoomScale:myScrollView.zoomScale -0.01];
}
Jonah
fonte
0

Apenas desative a paginação, para que funcione bem:

scrollview.pagingEnabled = NO;
Nagaraj
fonte
0

Eu tive o mesmo problema. Aqui está como eu resolvi

Esse código deve ser chamado como resultado de scrollView:DidScroll:

CGFloat imageHeight = self.imageView.frame.size.width * self.imageView.image.size.height / self.imageView.image.size.width;
BOOL imageSmallerThanContent = (imageHeight < self.scrollview.frame.size.height) ? YES : NO;
CGFloat topOffset = (self.imageView.frame.size.height - imageHeight) / 2;

// If image is not large enough setup content offset in a way that image is centered and not vertically scrollable
if (imageSmallerThanContent) {
     topOffset = topOffset - ((self.scrollview.frame.size.height - imageHeight)/2);
}

self.scrollview.contentInset = UIEdgeInsetsMake(topOffset * -1, 0, topOffset * -1, 0);
aryaxt
fonte
0

Embora a questão seja um pouco antiga, o problema ainda existe. Eu o resolvi no Xcode 7 criando a restrição de espaço vertical do item mais alto (neste caso, o topLabel) para os superViews (the scrollView) top an IBOutlete depois recalculando sua constante sempre que o conteúdo muda, dependendo da altura das subvisões do scrollView ( topLabele bottomLabel)

class MyViewController: UIViewController {

    @IBOutlet weak var scrollView: UIScrollView!
    @IBOutlet weak var topLabel: UILabel!
    @IBOutlet weak var bottomLabel: UILabel!
    @IBOutlet weak var toTopConstraint: NSLayoutConstraint!

    override func viewDidLayoutSubviews() {
        let heightOfScrollViewContents = (topLabel.frame.origin.y + topLabel.frame.size.height - bottomLabel.frame.origin.y)
        // In my case abs() delivers the perfect result, but you could also check if the heightOfScrollViewContents is greater than 0.
        toTopConstraint.constant = abs((scrollView.frame.height - heightOfScrollViewContents) / 2)
    }

    func refreshContents() {
        // Set the label's text …

        self.view.layoutIfNeeded()
    }
}
Nick Podratz
fonte