Paginação horizontal UIScrollView como guias do Mobile Safari

88

O Mobile Safari permite que você alterne as páginas inserindo uma espécie de visualização de paginação horizontal UIScrollView com um controle de página na parte inferior.

Estou tentando replicar esse comportamento específico em que um UIScrollView rolável horizontalmente mostra parte do conteúdo do próximo modo de exibição.

O exemplo fornecido pela Apple: PageControl mostra como usar um UIScrollView para paginação horizontal, mas todas as visualizações ocupam toda a largura da tela.

Como faço para que um UIScrollView mostre algum conteúdo da próxima exibição como o Safari móvel faz?

primeiro respondedor
fonte
2
Apenas um pensamento ... tente fazer os limites da visualização de rolagem menores do que a tela e tente fazer com que as visualizações sejam exibidas corretamente. (e definir clipsToBounds da visualização de rolagem para NÃO)
mjhoy
Eu queria ter páginas maiores que a largura do uiscrollview (rolagem horizontal). E o pensamento de mjhoy realmente me ajudou!
Sasho

Respostas:

263

Um UIScrollViewcom paginação habilitada irá parar em múltiplos de sua largura (ou altura) de quadro. Portanto, a primeira etapa é descobrir a largura que você deseja que suas páginas tenham. Faça com que seja a largura do UIScrollView. Em seguida, defina os tamanhos de sua subvisualização com o tamanho que você precisa e defina seus centros com base nos múltiplos da UIScrollViewlargura de.

Então, desde que você quiser ver as outras páginas, é claro, definir clipsToBoundsa NOcomo mhjoy afirmou. O truque agora é fazer com que ele role quando o usuário começar a arrastar fora do intervalo do UIScrollViewquadro de. Minha solução (quando tive que fazer isso muito recentemente) foi a seguinte:

Crie uma UIViewsubclasse (ou seja ClipView) que conterá as UIScrollViewsubvisões e suas. Essencialmente, deve ter a estrutura que você presumiria UIScrollViewque teria em circunstâncias normais. Coloque o UIScrollViewno centro do ClipView. Certifique-se de que o ClipView's clipsToBoundsesteja definido como YESse sua largura for menor que a de sua visualização pai. Além disso, o ClipViewprecisa de uma referência ao UIScrollView.

A etapa final é substituir - (UIView *)hitTest:withEvent:dentro do ClipView.

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
  return [self pointInside:point withEvent:event] ? scrollView : nil;
}

Isso basicamente expande a área de toque do UIScrollViewpara o quadro da visão de seu pai, exatamente o que você precisa.

Outra opção seria criar uma subclasse UIScrollViewe sobrescrever seu - (BOOL)pointInside:(CGPoint) point withEvent:(UIEvent *) eventmétodo; entretanto, você ainda precisará de uma visualização de contêiner para fazer o recorte, e pode ser difícil determinar quando retornar com YESbase apenas no UIScrollViewquadro de.

NOTA: Você também deve dar uma olhada no hitTest: withEvent: modificação de Juri Pakaste se estiver tendo problemas com a interação do usuário de subvisualização.

Ed Marty
fonte
3
Obrigado Ed. Fui com uma UIScrollViewsubclasse para carregamento lento de visualizações (in layoutSubviews) e usei um clippingRectpara fazer o pointInside:withEvent:teste. Funciona muito bem e não requer visualização de contêiner adicional.
ohhorob 01 de
1
Ótima resposta. Eu usei uma UIScrollViewsubclasse como @ohhorob mas com uma visão recipiente para recorte e retorno [super pointInside:CGPointMake(self.contentOffset.x + self.frame.size.width/2, self.contentOffset.y + self.frame.size.height/2) withEvent:event];em pointInside:withEvent:. Isso funciona muito bem e não há problemas de interação com o usuário mencionados na resposta de @JuriPakaste.
cduck
1
Uau, essa é uma solução muito boa. Só uma coisa, porém, na minha configuração, as páginas que estou adicionando têm UIButtons. Quando eu substituo o método hitTest, ele não me permite tocar nesses botões. Posso rolar, mas nenhuma ação pode ser selecionada em qualquer página.
A Salcedo de
8
Para aqueles que descobriram isso mais recentemente, estejam cientes de que - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffsetadicionado ao UIScrollViewDelegate no iOS5 significa que você não precisa mais passar por esse palavrão.
Mike Pollard
1
@MikePollard o efeito não é o mesmo silencioso, targetContentOffset não se encaixa bem nesta situação.
Kyle Fang
70

A ClipViewsolução acima funcionou para mim, mas tive que fazer uma -[UIView hitTest:withEvent:]implementação diferente . A versão de Ed Marty não conseguiu interação do usuário trabalhando com scrollviews verticais que tenho dentro do horizontal.

A seguinte versão funcionou para mim:

-(UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
    UIView* child = nil;
    if ((child = [super hitTest:point withEvent:event]) == self)
        return self.scrollView;     
    return child;
}
Juri Pakaste
fonte
Isso também ajuda a fazer com que botões e outras subvisões com a interação do usuário funcionem. :) obrigado
Thomas Clayson
4
Obrigado por este código, como posso usar essa modificação para obter eventos de subvisualizações (botões) não contidos nos limites de scrollview? Funciona se um botão, por exemplo, estiver dentro dos limites da visualização de rolagem central, mas não fora.
DreamOfMirrors
4
Isso realmente ajudou. Deve ser incluído como uma modificação da resposta selecionada.
Pacu
2
Sim! Isso também faz com que a interação do usuário dentro do scrollview funcione novamente! Eu tive que definir userInteractionEnabled como YES no ClipView para que isso funcionasse.
Tom van Zummeren
Tive que iterar por meio de self.scrollView.subviews e executar hitTest primeiro.
Bao Lei
8

Defina o tamanho do quadro de scrollView como o tamanho das suas páginas seria:

[self.view addSubview:scrollView];
[self.view addGestureRecognizer:mainScrollView.panGestureRecognizer];

Agora você pode fazer panorâmica self.viewe o conteúdo em scrollView será rolado.
Use também scrollView.clipsToBounds = NO;para evitar o corte do conteúdo.

Nikolay Tabunchenko
fonte
5

Acabei optando pelo UIScrollView personalizado, pois era o método mais rápido e simples que me parecia. No entanto, não vi nenhum código exato, então decidi compartilhar. Minhas necessidades eram de um UIScrollView que tinha conteúdo pequeno e, portanto, o próprio UIScrollView era pequeno para atingir o efeito de paginação. Como afirma a postagem, você não pode deslizar. Mas agora você pode.

Crie uma classe CustomScrollView e uma subclasse UIScrollView. Então, tudo o que você precisa fazer é adicionar isso ao arquivo .m:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
  return (point.y >= 0 && point.y <= self.frame.size.height);
}

Isso permite que você role de um lado para o outro (horizontal). Ajuste os limites de acordo para definir sua área de toque de deslizar / rolagem. Aproveitar!

Djneely
fonte
4

Fiz outra implementação que pode retornar o scrollview automaticamente. Portanto, não é necessário ter um IBOutlet que limitará a reutilização no projeto.

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if ([self pointInside:point withEvent:event]) {
        for (id view in [self subviews]) {
            if ([view isKindOfClass:[UIScrollView class]]) {
                return view;
            }
        }
    }
    return nil;
}
Alvinhu
fonte
1
Este é o único a ser usado se você já tiver um xib / storyboard. Caso contrário, não tenho certeza de como você obteria a referência para Paging / Scrollview. Alguma ideia?
mskw
3

Tenho outra modificação potencialmente útil para a implementação de hitTest do ClipView. Não gostei de ter que fornecer uma referência UIScrollView ao ClipView. Minha implementação abaixo permite que você reutilize a classe ClipView para expandir a área de teste de impacto de qualquer coisa, e não precisa fornecer uma referência.

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if (([self pointInside:point withEvent:event]) && (self.subviews.count >= 1))
    {
        // An extended-hit view should only have one sub-view, or make sure the
        // first subview is the one you want to expand the hit test for.
        return [self.subviews objectAtIndex:0];
    }

    return nil;
}
Rod Reddekopp
fonte
3

Implementei a sugestão votada acima, mas UICollectionVieweu estava usando considerava qualquer coisa fora do quadro como fora da tela. Isso fazia com que as células próximas fossem renderizadas fora dos limites apenas quando o usuário rolasse em direção a elas, o que não era o ideal.

O que acabei fazendo foi emular o comportamento de um scrollview adicionando o método abaixo ao delegado (ou UICollectionViewLayout).

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
{    
  if (velocity.x > 0) {
    proposedContentOffset.x = ceilf(self.collectionView.contentOffset.x / pageSize) * pageSize;
  }
  else {
    proposedContentOffset.x = floorf(self.collectionView.contentOffset.x / pageSize) * pageSize;
  }

  return proposedContentOffset;
}

Isso evita a delegação da ação de furto inteiramente, o que também foi um bônus. O UIScrollViewDelegatetem um método semelhante chamado scrollViewWillEndDragging:withVelocity:targetContentOffset:que pode ser usado para paginar UITableViews e UIScrollViews.

Kyle
fonte
2
Dave, eu estava lutando para alcançar esse tipo de comportamento usando UICollectionView também e acabei usando a mesma abordagem que você descreve aqui. No entanto, a experiência de rolagem não é tão boa quanto era uma visualização de rolagem com paginação ativada. Quando deslizei com maior velocidade, várias páginas foram roladas e ele parou na página proposta. O que desejo alcançar é salvar o comportamento como visualização de rolagem normal com paginação habilitada - qualquer que seja a velocidade que eu usar, obterei apenas mais 1 página. Você tem alguma ideia de como fazer isso?
Peter Stajger
3

Habilite o disparo de eventos de toque em visualizações secundárias da visualização de rolagem ao mesmo tempo em que apóia a técnica desta pergunta SO. Usa uma referência à visualização de rolagem (self.scrollView) para facilitar a leitura.

- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    UIView *hitView = nil;
    NSArray *childViews = [self.scrollView subviews];
    for (NSUInteger i = 0; i < childViews.count; i++) {
        CGRect childFrame = [[childViews objectAtIndex:i] frame];
        CGRect scrollFrame = self.scrollView.frame;
        CGPoint contentOffset = self.scrollView.contentOffset;
        if (childFrame.origin.x + scrollFrame.origin.x < point.x + contentOffset.x &&
            point.x + contentOffset.x < childFrame.origin.x + scrollFrame.origin.x + childFrame.size.width &&
            childFrame.origin.y + scrollFrame.origin.y < point.y + contentOffset.y &&
            point.y + contentOffset.y < childFrame.origin.y + scrollFrame.origin.y + childFrame.size.height
        ){
            hitView = [childViews objectAtIndex:i];
            return hitView;
        }
    }
    hitView = [super hitTest:point withEvent:event];
    if (hitView == self)
        return self.scrollView;
    return hitView;
}

Adicione isso à visualização do seu filho para capturar o evento de toque:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event

(Esta é uma variação da solução do usuário 1856273. Limpo para facilitar a leitura e incorporou a correção do bug de Bartserk. Pensei em editar a resposta do usuário 1856273, mas era uma mudança muito grande para fazer.)

David James
fonte
Para fazer o toque funcionar em um UIButton incorporado na subvisão, substituí o código hitView = [childViews objectAtIndex: i]; com // encontre o UIButton para (UIView * paneView em [[childViews objectAtIndex: i] subviews]) {if ([paneView isKindOfClass: [UIButton class]]) return paneView; }
CharlesA
2

minha versão Pressionando o botão deitado no pergaminho - trabalho =)

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView* child = nil;
    for (int i=0; i<[[[[self subviews] objectAtIndex:0] subviews] count];i++) {

        CGRect myframe =[[[[[self subviews] objectAtIndex:0] subviews]objectAtIndex:i] frame];
        CGRect myframeS =[[[self subviews] objectAtIndex:0] frame];
        CGPoint myscroll =[[[self subviews] objectAtIndex:0] contentOffset];
        if (myframe.origin.x < point.x && point.x < myframe.origin.x+myframe.size.width &&
            myframe.origin.y+myframeS.origin.y < point.y+myscroll.y && point.y+myscroll.y < myframe.origin.y+myframeS.origin.y +myframe.size.height){
            child = [[[[self subviews] objectAtIndex:0] subviews]objectAtIndex:i];
            return child;
        }


    }

    child = [super hitTest:point withEvent:event];
    if (child == self)
        return [[self subviews] objectAtIndex:0];
    return child;
    }

mas apenas [[subvisualizações próprias] objectAtIndex: 0] deve ser um pergaminho

Kolbasek
fonte
Isso resolveu meu problema :) Apenas observe que você não está adicionando o deslocamento de rolagem ao point.x quando está verificando os limites, e isso faz com que o código não funcione bem em rolagens horizontais. Depois de adicionar isso, funcionou muito bem!
Bartserk de
2

Aqui está uma resposta do Swift baseada na resposta de Ed Marty, mas também incluindo a modificação de Juri Pakaste para permitir toques nos botões, etc., dentro do scrollview.

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    let view = super.hitTest(point, with: event)
    return view == self ? scrollView : view
}
Ben Packard
fonte
Obrigado por atualizar isso :)
Liam Bolling