Paginação UIScrollView em incrementos menores que o tamanho do quadro

87

Eu tenho uma visualização de rolagem que tem a largura da tela, mas apenas cerca de 70 pixels de altura. Ele contém muitos ícones 50 x 50 (com espaço ao redor) que desejo que o usuário possa escolher. Mas eu sempre quero que a visualização de rolagem se comporte de maneira paginada, sempre parando com um ícone exatamente no centro.

Se os ícones tivessem a largura da tela, isso não seria um problema porque a paginação do UIScrollView cuidaria disso. Mas como meus pequenos ícones são muito menores que o tamanho do conteúdo, isso não funciona.

Já vi esse comportamento antes em uma chamada de aplicativo AllRecipes. Só não sei como fazer.

Como faço para que a paginação por ícone funcione?

Henning
fonte

Respostas:

123

Tente tornar a sua visualização em rolagem menor que o tamanho da tela (largura), mas desmarque a caixa de seleção "Clip subvisualizações" no IB. Em seguida, sobreponha uma visão transparente, userInteractionEnabled = NO em cima dela (na largura total), que substitui hitTest: withEvent: para retornar sua visualização de rolagem. Isso deve dar a você o que você está procurando. Veja esta resposta para mais detalhes.

Ben Gottlieb
fonte
1
Não tenho certeza do que você quer dizer, vou olhar para a resposta que você aponta.
Henning
8
Esta é uma bela solução. Eu não tinha pensado em substituir a função hitTest: e isso praticamente elimina a única desvantagem dessa abordagem. Bem feito.
Ben Gotow de
Ótima solução! Realmente me ajudou.
DevDevDev
8
Isso funciona muito bem, mas eu recomendo, em vez de simplesmente retornar o scrollView, retornar [scrollView hitTest: point withEvent: event] para que os eventos passem corretamente. Por exemplo, em uma exibição de rolagem cheia de UIButtons, os botões ainda funcionarão dessa maneira.
greenisus
2
observe que isso NÃO funcionará com tableview ou collectionView, pois não renderizará coisas fora do quadro. Veja minha resposta abaixo
vish
63

Há também outra solução que provavelmente é um pouco melhor do que sobrepor a visualização de rolagem com outra visualização e substituir hitTest.

Você pode criar uma subclasse de UIScrollView e substituir seu pointInside. Então, a visualização de rolagem pode responder a toques fora de seu quadro. Claro que o resto é o mesmo.

@interface PagingScrollView : UIScrollView {

    UIEdgeInsets responseInsets;
}

@property (nonatomic, assign) UIEdgeInsets responseInsets;

@end


@implementation PagingScrollView

@synthesize responseInsets;

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    CGPoint parentLocation = [self convertPoint:point toView:[self superview]];
    CGRect responseRect = self.frame;
    responseRect.origin.x -= responseInsets.left;
    responseRect.origin.y -= responseInsets.top;
    responseRect.size.width += (responseInsets.left + responseInsets.right);
    responseRect.size.height += (responseInsets.top + responseInsets.bottom);

    return CGRectContainsPoint(responseRect, parentLocation);
}

@end
Dividido
fonte
1
sim! além disso, se os limites de sua visualização de rolagem forem iguais aos de seu pai, você pode simplesmente retornar sim do método pointInside. obrigado
cocoatoucher
5
Gosto desta solução, embora tenha de alterar parentLocation para "[self convertPoint: point toView: self.superview]" para que funcione corretamente.
amrox de
Você está certo, convertPoint é muito melhor do que o que escrevi lá.
Divida
6
diretamente return [self.superview pointInside:[self convertPoint:point toView:self.superview] withEvent:event];@amrox @Split, você não precisa do responseInsets se tiver uma supervisualização que atua como a visualização do recorte.
Cœur
Essa solução parecia falhar quando eu chegasse ao final da minha lista de itens se a última página de itens não preenchesse completamente a página. É meio difícil de explicar, mas se estou mostrando 3,5 células por página e tenho 5 itens, a segunda página mostra 2 células com espaço vazio à direita ou mostra 3 células com um célula parcial. O problema era se eu estava no estado em que ele estava me mostrando um espaço vazio e eu selecionava uma célula então a paginação se corrigia durante a transição de nav ... Vou postar minha solução abaixo.
tyler
18

Vejo muitas soluções, mas são muito complexas. Uma maneira muito mais fácil de ter páginas pequenas, mas ainda manter toda a área rolável, é tornar a rolagem menor e movê-la scrollView.panGestureRecognizerpara a visualização pai. Estas são as etapas:

  1. Reduza o tamanho do scrollViewO tamanho do ScrollView é menor do que o pai

  2. Certifique-se de que sua visualização de rolagem seja paginada e não recorte a subvisualização insira a descrição da imagem aqui

  3. No código, mova o gesto panorâmico de scrollview para a visualização do contêiner pai com largura total:

    override func viewDidLoad() {
        super.viewDidLoad()
        statsView.addGestureRecognizer(statsScrollView.panGestureRecognizer)
    }
Angel G. Olloqui
fonte
Esta é uma ótima solução. Muito esperto.
superpuccio de
Isso é realmente o que eu quero!
LYM
Muito esperto! Você me salvou horas. Obrigado!
Erik
6

A resposta aceita é muito boa, mas só funcionará para a UIScrollViewclasse, e nenhum de seus descendentes. Por exemplo, se você tiver muitas visualizações e converter para um UICollectionView, não será capaz de usar este método, porque a visualização da coleção removerá as visualizações que pensa serem "não visíveis" (então, mesmo que não sejam cortadas, elas irão desaparecer).

O comentário sobre essa menção scrollViewWillEndDragging:withVelocity:targetContentOffset:é, na minha opinião, a resposta correta.

O que você pode fazer é, dentro desse método delegado, calcular a página / índice atual. Então você decide se a velocidade e o deslocamento do alvo merecem um movimento de "próxima página". Você pode chegar bem perto do pagingEnabledcomportamento.

nota: eu normalmente sou um desenvolvedor de RubyMotion atualmente, então alguém por favor verifique este código Obj-C para correção. Desculpe pela mistura de camelCase e snake_case, eu copiei e colei muito deste código.

- (void) scrollViewWillEndDragging:(UIScrollView *)scrollView
         withVelocity:(CGPoint)velocity
         targetContentOffset:(inout CGPoint *)targetOffset
{
    CGFloat x = targetOffset->x;
    int index = [self convertXToIndex: x];
    CGFloat w = 300f;  // this is your custom page width
    CGFloat current_x = w * [self convertXToIndex: scrollView.contentOffset.x];

    // even if the velocity is low, if the offset is more than 50% past the halfway
    // point, proceed to the next item.
    if ( velocity.x < -0.5 || (current_x - x) > w / 2 ) {
      index -= 1
    } 
    else if ( velocity.x > 0.5 || (x - current_x) > w / 2 ) {
      index += 1;
    }

    if ( index >= 0 || index < self.items.length ) {
      CGFloat new_x = [self convertIndexToX: index];
      targetOffset->x = new_x;
    }
} 
colinta
fonte
haha agora eu me pergunto se eu não deveria ter mencionado o RubyMotion - está me deixando na votação! Não, na verdade, eu gostaria que os votos negativos também incluíssem alguns comentários, eu adoraria saber o que as pessoas acham de errado com minha explicação.
colinta de
Você pode incluir a definição de convertXToIndex:e convertIndexToX:?
mr5 de
Incompleto. Quando você arrasta lentamente e levanta a mão com velocityzero, ele vai voltar, o que é estranho. Portanto, tenho uma resposta melhor.
DawnSong de
4
- (void) scrollViewWillEndDragging:(UIScrollView *)scrollView
         withVelocity:(CGPoint)velocity
         targetContentOffset:(inout CGPoint *)targetOffset
{
    static CGFloat previousIndex;
    CGFloat x = targetOffset->x + kPageOffset;
    int index = (x + kPageWidth/2)/kPageWidth;
    if(index<previousIndex - 1){
        index = previousIndex - 1;
    }else if(index > previousIndex + 1){
        index = previousIndex + 1;
    }

    CGFloat newTarget = index * kPageWidth;
    targetOffset->x = newTarget - kPageOffset;
    previousIndex = index;
} 

kPageWidth é a largura que você deseja que a sua página tenha. kPageOffset é se você não quiser que as células sejam alinhadas à esquerda (ou seja, se você quiser que elas fiquem alinhadas ao centro, defina para metade da largura da sua célula). Caso contrário, deve ser zero.

Isso também permitirá rolar apenas uma página por vez.

vish
fonte
3

Dê uma olhada no -scrollView:didEndDragging:willDecelerate:método em UIScrollViewDelegate. Algo como:

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
    int x = scrollView.contentOffset.x;
    int xOff = x % 50;
    if(xOff < 25)
        x -= xOff;
    else
        x += 50 - xOff;

    int halfW = scrollView.contentSize.width / 2; // the width of the whole content view, not just the scroll view
    if(x > halfW)
        x = halfW;

    [scrollView setContentOffset:CGPointMake(x,scrollView.contentOffset.y)];
}

Não é perfeito - a última vez que tentei esse código, tive um comportamento feio (pular, se bem me lembro) ao retornar de um pergaminho com elástico. Você pode evitar isso simplesmente definindo a bouncespropriedade da visualização de rolagem como NO.

Noah Witherspoon
fonte
3

Já que eu não tenho permissão para comentar ainda, adicionarei meus comentários à resposta de Noah aqui.

Eu consegui isso com sucesso pelo método que Noah Witherspoon descreveu. Eu contornei o comportamento de salto simplesmente não chamando o setContentOffset:método quando o scrollview passou de suas bordas.

         - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
         {      
             // Don't snap when at the edges because that will override the bounce mechanic
             if (self.contentOffset.x < 0 || self.contentOffset.x + self.bounds.size.width > self.contentSize.width)
                 return;

             ...
         }

Também descobri que precisava implementar o -scrollViewWillBeginDecelerating:método em UIScrollViewDelegatepara capturar todos os casos.

Jacob Persson
fonte
Quais casos adicionais precisam ser detectados?
chrish
o caso em que não houve nenhum arrastamento. O usuário levanta o dedo e a rolagem para imediatamente.
Jess Bowers
3

Experimentei a solução acima que sobrepõe uma visão transparente com pointInside: withEvent: overridden. Isso funcionou muito bem para mim, mas não funcionou em certos casos - veja meu comentário. Acabei implementando a paginação sozinho com uma combinação de scrollViewDidScroll para rastrear o índice da página atual e scrollViewWillEndDragging: withVelocity: targetContentOffset e scrollViewDidEndDragging: willDecelerate para ajustar à página apropriada. Observe, o método will-end só está disponível iOS5 +, mas é muito bom para atingir um deslocamento específico se a velocidade! = 0. Especificamente, você pode dizer ao chamador onde deseja que a visualização de rolagem pouse com animação se houver velocidade em um direção particular.

Tyler
fonte
0

Ao criar a visualização de rolagem, certifique-se de definir isto:

scrollView.showsHorizontalScrollIndicator = false;
scrollView.showsVerticalScrollIndicator = false;
scrollView.pagingEnabled = true;

Em seguida, adicione suas subvisualizações ao scroller em um deslocamento igual ao índice * altura do scroller. Isso é para um scroller vertical:

UIView * sub = [UIView new];
sub.frame = CGRectMake(0, index * h, w, subViewHeight);
[scrollView addSubview:sub];

Se você executá-lo agora, as visualizações são espaçadas e, com a paginação ativada, elas rolam uma de cada vez.

Então, coloque isso em seu método viewDidScroll:

    //set vars
    int index = scrollView.contentOffset.y / h; //current index
    float y = scrollView.contentOffset.y; //actual offset
    float p = (y / h)-index; //percentage of page scroll complete (0.0-1.0)
    int subViewHeight = h-240; //height of the view
    int spacing = 30; //preferred spacing between views (if any)

    NSArray * array = scrollView.subviews;

    //cycle through array
    for (UIView * sub in array){

        //subview index in array
        int subIndex = (int)[array indexOfObject:sub];

        //moves the subs up to the top (on top of each other)
        float transform = (-h * subIndex);

        //moves them back down with spacing
        transform += (subViewHeight + spacing) * subIndex;

        //adjusts the offset during scroll
        transform += (h - subViewHeight - spacing) * p;

        //adjusts the offset for the index
        transform += index * (h - subViewHeight - spacing);

        //apply transform
        sub.transform = CGAffineTransformMakeTranslation(0, transform);
    }

Os frames das subvisualizações ainda estão espaçados, estamos apenas movendo-os juntos por meio de uma transformação conforme o usuário rola.

Além disso, você tem acesso à variável p acima, que pode ser usada para outras coisas, como alfa ou transformações nas subvisões. Quando p == 1, essa página está sendo exibida na íntegra, ou melhor, tende para 1.

Johnny Rockex
fonte
0

Tente usar a propriedade contentInset de scrollView:

scrollView.pagingEnabled = YES;

[scrollView setContentSize:CGSizeMake(height, pageWidth * 3)];
double leftContentOffset = pageWidth - kSomeOffset;
scrollView.contentInset = UIEdgeInsetsMake(0, leftContentOffset, 0, 0);

Pode ser necessário brincar com seus valores para obter a paginação desejada.

Eu descobri que isso funciona de forma mais limpa em comparação com as alternativas publicadas. O problema de usar o scrollViewWillEndDragging:método delegado é que a aceleração para movimentos lentos não é natural.

Vlad
fonte
0

Esta é a única solução real para o problema.

import UIKit

class TestScrollViewController: UIViewController, UIScrollViewDelegate {

    var scrollView: UIScrollView!

    var cellSize:CGFloat!
    var inset:CGFloat!
    var preX:CGFloat=0
    let pages = 8

    override func viewDidLoad() {
        super.viewDidLoad()

        cellSize = (self.view.bounds.width-180)
        inset=(self.view.bounds.width-cellSize)/2
        scrollView=UIScrollView(frame: self.view.bounds)
        self.view.addSubview(scrollView)

        for i in 0..<pages {
            let v = UIView(frame: self.view.bounds)
            v.backgroundColor=UIColor(red: CGFloat(CGFloat(i)/CGFloat(pages)), green: CGFloat(1 - CGFloat(i)/CGFloat(pages)), blue: CGFloat(CGFloat(i)/CGFloat(pages)), alpha: 1)
            v.frame.origin.x=CGFloat(i)*cellSize
            v.frame.size.width=cellSize
            scrollView.addSubview(v)
        }

        scrollView.contentSize.width=cellSize*CGFloat(pages)
        scrollView.isPagingEnabled=false
        scrollView.delegate=self
        scrollView.contentInset.left=inset
        scrollView.contentOffset.x = -inset
        scrollView.contentInset.right=inset

    }

    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        preX = scrollView.contentOffset.x
    }

    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {

        let originalIndex = Int((preX+cellSize/2)/cellSize)

        let targetX = targetContentOffset.pointee.x
        var targetIndex = Int((targetX+cellSize/2)/cellSize)

        if targetIndex > originalIndex + 1 {
            targetIndex=originalIndex+1
        }
        if targetIndex < originalIndex - 1 {
            targetIndex=originalIndex - 1
        }

        if velocity.x == 0 {
            let currentIndex = Int((scrollView.contentOffset.x+self.view.bounds.width/2)/cellSize)
            let tx=CGFloat(currentIndex)*cellSize-(self.view.bounds.width-cellSize)/2
            scrollView.setContentOffset(CGPoint(x:tx,y:0), animated: true)
            return
        }

        let tx=CGFloat(targetIndex)*cellSize-(self.view.bounds.width-cellSize)/2
        targetContentOffset.pointee.x=scrollView.contentOffset.x

        UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: velocity.x, options: [UIViewAnimationOptions.curveEaseOut, UIViewAnimationOptions.allowUserInteraction], animations: {
            scrollView.contentOffset=CGPoint(x:tx,y:0)
        }) { (b:Bool) in

        }

    }


}
TimWhiting
fonte
0

Aqui está minha resposta. No meu exemplo, um collectionViewque possui um cabeçalho de seção é o scrollView que queremos que tenha isPagingEnabledefeito personalizado , e a altura da célula é um valor constante.

var isScrollingDown = false // in my example, scrollDirection is vertical
var lastScrollOffset = CGPoint.zero

func scrollViewDidScroll(_ sv: UIScrollView) {
    isScrollingDown = sv.contentOffset.y > lastScrollOffset.y
    lastScrollOffset = sv.contentOffset
}

// 实现 isPagingEnabled 效果
func scrollViewWillEndDragging(_ sv: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {

    let realHeaderHeight = headerHeight + collectionViewLayout.sectionInset.top
    guard realHeaderHeight < targetContentOffset.pointee.y else {
        // make sure that user can scroll to make header visible.
        return // 否则无法手动滚到顶部
    }

    let realFooterHeight: CGFloat = 0
    let realCellHeight = cellHeight + collectionViewLayout.minimumLineSpacing
    guard targetContentOffset.pointee.y < sv.contentSize.height - realFooterHeight else {
        // make sure that user can scroll to make footer visible
        return // 若有footer,不在此处 return 会导致无法手动滚动到底部
    }

    let indexOfCell = (targetContentOffset.pointee.y - realHeaderHeight) / realCellHeight
    // velocity.y can be 0 when lifting your hand slowly
    let roundedIndex = isScrollingDown ? ceil(indexOfCell) : floor(indexOfCell) // 如果松手时滚动速度为 0,则 velocity.y == 0,且 sv.contentOffset == targetContentOffset.pointee
    let y = realHeaderHeight + realCellHeight * roundedIndex - collectionViewLayout.minimumLineSpacing
    targetContentOffset.pointee.y = y
}
DawnSong
fonte
0

Versão Swift refinada da UICollectionViewsolução:

  • Limite de uma página por deslize
  • Garante um ajuste rápido à página, mesmo se a rolagem for lenta
override func viewDidLoad() {
    super.viewDidLoad()
    collectionView.decelerationRate = .fast
}

private var dragStartPage: CGPoint = .zero

func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
    dragStartOffset = scrollView.contentOffset
}

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    // Snap target offset to current or adjacent page
    let currentIndex = pageIndexForContentOffset(dragStartOffset)
    var targetIndex = pageIndexForContentOffset(targetContentOffset.pointee)
    if targetIndex != currentIndex {
        targetIndex = currentIndex + (targetIndex - currentIndex).signum()
    } else if abs(velocity.x) > 0.25 {
        targetIndex = currentIndex + (velocity.x > 0 ? 1 : 0)
    }
    // Constrain to valid indices
    if targetIndex < 0 { targetIndex = 0 }
    if targetIndex >= items.count { targetIndex = max(items.count-1, 0) }
    // Set new target offset
    targetContentOffset.pointee.x = contentOffsetForCardIndex(targetIndex)
}
Patrick Pijnappel
fonte
0

Tópico antigo, mas vale a pena mencionar minha opinião sobre isso:

import Foundation
import UIKit

class PaginatedCardScrollView: UIScrollView {

    convenience init() {
        self.init(frame: CGRect.zero)
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        _setup()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        _setup()
    }

    private func _setup() {
        isPagingEnabled = true
        isScrollEnabled = true
        clipsToBounds = false
        showsHorizontalScrollIndicator = false
    }

    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        // Asume the scrollview extends uses the entire width of the screen
        return point.y >= frame.origin.y && point.y <= frame.origin.y + frame.size.height
    }
}

Desta forma, você pode a) usar toda a largura da scrollview para mover / deslizar eb) ser capaz de interagir com os elementos que estão fora dos limites originais da scrollview

basvk
fonte
0

Para o problema UICollectionView (que para mim era um UITableViewCell de uma coleção de cartões de rolagem horizontal com "tickers" do próximo cartão anterior), eu simplesmente tive que desistir de usar paginação nativa da Apple. A solução do github de Damien funcionou incrivelmente para mim. Você pode ajustar o tamanho do tickler aumentando a largura do cabeçalho e dimensionando-o dinamicamente para zero quando no primeiro índice para que você não termine com uma grande margem em branco

David
fonte