Paginazione di UIScrollView in incrementi inferiori alle dimensioni del frame


87

Ho una visualizzazione a scorrimento che è la larghezza dello schermo ma alta solo circa 70 pixel. Contiene molte icone 50 x 50 (con spazio intorno) tra le quali desidero che l'utente possa scegliere. Ma voglio sempre che la visualizzazione a scorrimento si comporti in modo a pagine, fermandosi sempre con un'icona al centro esatto.

Se le icone fossero la larghezza dello schermo questo non sarebbe un problema perché il paging di UIScrollView si occuperebbe di esso. Ma poiché le mie piccole icone sono molto inferiori alla dimensione del contenuto, non funziona.

Ho già visto questo comportamento in un'app chiamata AllRecipes. Non so come farlo.

Come faccio a far funzionare il paging in base alle dimensioni delle icone?

Risposte:


123

Prova a rendere la tua visualizzazione a scorrimento inferiore alla dimensione dello schermo (in larghezza), ma deseleziona la casella di controllo "Clip Subviews" in IB. Quindi, sovrapponi una vista trasparente, userInteractionEnabled = NO su di essa (a tutta larghezza), che sovrascrive hitTest: withEvent: per restituire la visualizzazione a scorrimento. Questo dovrebbe darti quello che stai cercando. Vedi questa risposta per maggiori dettagli.


1
Non sono del tutto sicuro di cosa intendi, guarderò la risposta a cui indichi.
Henning,

8
Questa è una bella soluzione. Non avevo pensato di sovrascrivere la funzione hitTest: e questo praticamente elimina l'unico aspetto negativo di questo approccio. Ben fatto.
Ben Gotow

Bella soluzione! Mi ha davvero aiutato.
DevDevDev

8
Funziona molto bene, ma ti consiglio invece di restituire semplicemente scrollView, restituisci [scrollView hitTest: point withEvent: event] in modo che gli eventi passino correttamente. Ad esempio, in una visualizzazione a scorrimento piena di UIButtons, i pulsanti continueranno a funzionare in questo modo.
greenisus

2
nota, questo NON funzionerà con una tableview o collectionView poiché non renderà le cose al di fuori del frame. Vedi la mia risposta di seguito
vish

63

C'è anche un'altra soluzione che è probabilmente un po 'meglio che sovrapporre la visualizzazione a scorrimento con un'altra vista e sovrascrivere hitTest.

È possibile creare una sottoclasse UIScrollView e sovrascrivere il relativo pointInside. Quindi la visualizzazione a scorrimento può rispondere ai tocchi al di fuori della sua cornice. Ovviamente il resto è lo stesso.

@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

1
sì! inoltre, se i limiti del tuo scrollview sono gli stessi del suo genitore, puoi semplicemente restituire yes dal metodo pointInside. grazie
cocoatoucher

5
Mi piace questa soluzione, anche se devo cambiare parentLocation in "[self convertPoint: point toView: self.superview]" affinché funzioni correttamente.
amrox

Hai ragione convertPoint è molto meglio di quello che ho scritto lì.
Spalato

6
direttamente return [self.superview pointInside:[self convertPoint:point toView:self.superview] withEvent:event];@amrox @Split non hai bisogno di responseInsets se hai una superview che funge da cliping view.
Cœur

Questa soluzione sembrava fallire quando sarei arrivato alla fine del mio elenco di elementi se l'ultima pagina di elementi non riempiva completamente la pagina. È un po 'difficile da spiegare, ma se mostro 3,5 celle per pagina e ho 5 elementi, la seconda pagina mi mostrerebbe 2 celle con spazio vuoto a destra, oppure si aggancerebbe per mostrare 3 celle con una parte iniziale cella parziale. Il problema era se fossi nello stato in cui mi mostrava uno spazio vuoto e selezionassi una cella, il paging si sarebbe corretto da solo durante la transizione di navigazione ... Pubblicherò la mia soluzione di seguito.
tyler

18

Vedo molte soluzioni, ma sono molto complesse. Un modo molto più semplice per avere pagine piccole ma mantenere comunque tutta l'area scorrevole, è ridurre lo scorrimento e spostare la scrollView.panGestureRecognizervisualizzazione genitore. Questi sono i passaggi:

  1. Riduci le dimensioni di scrollViewLa dimensione di ScrollView è inferiore a quella principale

  2. Assicurati che la tua visualizzazione a scorrimento sia impaginata e non ritagli la vista secondaria inserisci qui la descrizione dell'immagine

  3. Nel codice, sposta il gesto di panoramica con scorrimento nella visualizzazione del contenitore padre che è a larghezza intera:

    override func viewDidLoad() {
        super.viewDidLoad()
        statsView.addGestureRecognizer(statsScrollView.panGestureRecognizer)
    }

Questa è una soluzione eccezionale. Molto intelligente.
superpuccio

Questo è effettivamente quello che voglio!
LYM

Molto intelligente! Mi hai risparmiato ore. Grazie!
Erik

6

La risposta accettata è molto buona, ma funzionerà solo per la UIScrollViewclasse e per nessuno dei suoi discendenti. Ad esempio, se hai molte visualizzazioni e converti in una UICollectionView, non sarai in grado di utilizzare questo metodo, perché la visualizzazione della raccolta rimuoverà le visualizzazioni che ritiene "non visibili" (quindi anche se non sono ritagliate, saranno scomparire).

Il commento al riguardo scrollViewWillEndDragging:withVelocity:targetContentOffset:è, a mio parere, la risposta corretta.

Quello che puoi fare è, all'interno di questo metodo delegato, calcolare la pagina / indice corrente. Quindi si decide se la velocità e l'offset del target meritano un movimento "pagina successiva". Puoi avvicinarti abbastanza al pagingEnabledcomportamento.

nota: di solito sono un dev RubyMotion in questi giorni, quindi qualcuno per favore prova questo codice Obj-C per la correttezza. Ci scusiamo per il mix di camelCase e snake_case, ho copiato e incollato gran parte di questo codice.

- (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;
    }
} 

haha ora mi chiedo se non avrei dovuto menzionare RubyMotion - mi sta facendo downvoting! no in realtà vorrei che i downvotes includessero anche qualche commento, mi piacerebbe sapere cosa le persone trovano sbagliato nella mia spiegazione.
colinta

Puoi includere la definizione di convertXToIndex:e convertIndexToX:?
mr5

Non completo. Quando trascini lentamente e sollevi la mano con velocityzero, rimbalzerà indietro, il che è strano. Quindi ho una risposta migliore.
DawnSong,

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 è la larghezza che vuoi che sia la tua pagina. kPageOffset è se non vuoi che le celle siano allineate a sinistra (cioè se vuoi che siano allineate al centro, imposta questo a metà della larghezza della tua cella). Altrimenti, dovrebbe essere zero.

Ciò consentirà anche di scorrere solo una pagina alla volta.


3

Dai un'occhiata al -scrollView:didEndDragging:willDecelerate:metodo UIScrollViewDelegate. Qualcosa di simile a:

- (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)];
}

Non è perfetto - l'ultima volta che ho provato questo codice ho avuto un brutto comportamento (saltando, se ricordo bene) quando tornavo da una pergamena elastica. Potresti essere in grado di evitarlo semplicemente impostando la bouncesproprietà della visualizzazione di scorrimento su NO.


3

Dal momento che non mi sembra ancora consentito di commentare, aggiungerò i miei commenti alla risposta di Noah qui.

Sono riuscito a raggiungere questo obiettivo con il metodo descritto da Noah Witherspoon. Ho aggirato il comportamento del salto semplicemente non chiamando il setContentOffset:metodo quando lo scrollview è oltre i suoi bordi.

         - (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;

             ...
         }

Ho anche scoperto che avevo bisogno di implementare il -scrollViewWillBeginDecelerating:metodo UIScrollViewDelegateper catturare tutti i casi.


Quali casi aggiuntivi devono essere catturati?
Chrish

il caso in cui non ci fosse affatto trascinamento. L'utente solleva il dito e lo scorrimento si interrompe immediatamente.
Jess Bowers

3

Ho provato la soluzione sopra che ha sovrapposto una vista trasparente con pointInside: withEvent: overridden. Questo ha funzionato abbastanza bene per me, ma si è interrotto in alcuni casi - vedi il mio commento. Ho finito per implementare io stesso il paging con una combinazione di scrollViewDidScroll per tenere traccia dell'indice della pagina corrente e scrollViewWillEndDragging: withVelocity: targetContentOffset e scrollViewDidEndDragging: willDecelerate per agganciarsi alla pagina appropriata. Nota, il metodo will-end è disponibile solo iOS5 +, ma è abbastanza dolce per mirare a un particolare offset se la velocità! = 0. In particolare, puoi dire al chiamante dove vuoi che la visualizzazione a scorrimento atterri con l'animazione se c'è velocità in un direzione particolare.


0

Quando crei la scrollview, assicurati di impostare questo:

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

Quindi aggiungi le tue viste secondarie allo scroller con un offset uguale al loro indice * altezza dello scroller. Questo è per uno scroller verticale:

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

Se lo esegui ora, le viste sono distanziate e con il paging abilitato scorrono una alla volta.

Quindi mettilo nel tuo metodo 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);
    }

I fotogrammi delle viste secondarie sono ancora distanziati, li stiamo semplicemente spostando insieme tramite una trasformazione mentre l'utente scorre.

Inoltre, hai accesso alla variabile p sopra, che puoi usare per altre cose, come alfa o trasformazioni all'interno delle viste secondarie. Quando p == 1, quella pagina viene mostrata completamente, o meglio tende verso 1.


0

Prova a utilizzare la proprietà contentInset di scrollView:

scrollView.pagingEnabled = YES;

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

Potrebbe essere necessario giocare con i propri valori per ottenere il paging desiderato.

Ho scoperto che funziona in modo più pulito rispetto alle alternative pubblicate. Il problema con l'utilizzo del scrollViewWillEndDragging:metodo delegato è che l'accelerazione per i movimenti lenti non è naturale.


0

Questa è l'unica vera soluzione al 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

        }

    }


}

0

Ecco la mia risposta. Nel mio esempio, un collectionViewche ha un'intestazione di sezione è lo scrollView che vogliamo che abbia un isPagingEnabledeffetto personalizzato e l'altezza della cella è un valore costante.

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
}

0

Raffinata versione Swift della UICollectionViewsoluzione:

  • Limiti a una pagina per scorrimento
  • Assicura uno snap veloce alla pagina, anche se lo scorrimento è stato lento
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)
}

0

Vecchio thread, ma vale la pena menzionare la mia opinione su questo:

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
    }
}

In questo modo puoi a) usare l'intera larghezza della scrollview per fare una panoramica / scorrere eb) essere in grado di interagire con gli elementi che sono fuori dai limiti originali della scrollview


0

Per il numero di UICollectionView (che per me era un UITableViewCell di una raccolta di carte a scorrimento orizzontale con "ticker" della carta imminente / precedente), ho dovuto rinunciare all'utilizzo del paging nativo di Apple. La soluzione GitHub di Damien ha funzionato meravigliosamente per me. Puoi modificare la dimensione del tickler aumentando la larghezza dell'intestazione e ridimensionandola dinamicamente a zero quando sei al primo indice in modo da non finire con un grande margine vuoto

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.