J'ai une vue de défilement qui est la largeur de l'écran mais seulement environ 70 pixels de haut. Il contient de nombreuses icônes 50 x 50 (avec un espace autour d'elles) que je souhaite que l'utilisateur puisse choisir. Mais je veux toujours que la vue de défilement se comporte de manière paginée, en s'arrêtant toujours avec une icône au centre exact.
Si les icônes avaient la largeur de l'écran, ce ne serait pas un problème car la pagination de UIScrollView s'en chargerait. Mais comme mes petites icônes sont bien inférieures à la taille du contenu, cela ne fonctionne pas.
J'ai déjà vu ce comportement dans un appel d'application AllRecipes. Je ne sais tout simplement pas comment faire.
Comment faire fonctionner la pagination sur une base de taille par icône?
la source
Il existe également une autre solution qui est probablement un peu meilleure que de superposer une vue de défilement avec une autre vue et de remplacer hitTest.
Vous pouvez sous-classer UIScrollView et remplacer son pointInside. Ensuite, la vue de défilement peut répondre aux touches en dehors de son cadre. Bien sûr, le reste est le même.
@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
la source
return [self.superview pointInside:[self convertPoint:point toView:self.superview] withEvent:event];
@amrox @Split vous n'avez pas besoin de responseInsets si vous avez un superview qui sert de vue de découpage.Je vois beaucoup de solutions, mais elles sont très complexes. Un moyen beaucoup plus simple d'avoir de petites pages tout en gardant toutes les zones défilantes consiste à réduire le défilement et à déplacer le
scrollView.panGestureRecognizer
vers votre vue parent. Voici les étapes:Réduisez votre taille de scrollView
Assurez-vous que votre vue de défilement est paginée et ne coupe pas la sous-vue
Dans le code, déplacez le mouvement de panoramique scrollview vers la vue du conteneur parent pleine largeur:
override func viewDidLoad() { super.viewDidLoad() statsView.addGestureRecognizer(statsScrollView.panGestureRecognizer) }
la source
La réponse acceptée est très bonne, mais elle ne fonctionnera que pour la
UIScrollView
classe et aucun de ses descendants. Par exemple, si vous avez beaucoup de vues et que vous convertissez en unUICollectionView
, vous ne pourrez pas utiliser cette méthode, car la vue de collection supprimera vues qu'elle pense ne pas être visibles (donc même si elles ne sont pas coupées, elles disparaître).Le commentaire à ce sujet mentionne
scrollViewWillEndDragging:withVelocity:targetContentOffset:
est, à mon avis, la bonne réponse.Ce que vous pouvez faire est, à l'intérieur de cette méthode déléguée, vous calculez la page / l'index en cours. Ensuite, vous décidez si la vitesse et le décalage cible méritent un mouvement de "page suivante". Vous pouvez vous rapprocher du
pagingEnabled
comportement.Remarque: je suis généralement un développeur RubyMotion ces jours-ci, donc quelqu'un s'il vous plaît prouver que ce code Obj-C est correct. Désolé pour le mélange de camelCase et de snake_case, j'ai copié et collé une grande partie de ce code.
- (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; } }
la source
convertXToIndex:
etconvertIndexToX:
?velocity
zéro, elle rebondira, ce qui est étrange. J'ai donc une meilleure réponse.- (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 est la largeur que vous souhaitez que votre page soit. kPageOffset est si vous ne voulez pas que les cellules soient alignées à gauche (c'est-à-dire si vous voulez qu'elles soient alignées au centre, définissez-le sur la moitié de la largeur de votre cellule). Sinon, il devrait être zéro.
Cela permettra également de ne faire défiler qu'une page à la fois.
la source
Jetez un œil à la
-scrollView:didEndDragging:willDecelerate:
méthode surUIScrollViewDelegate
. Quelque chose comme:- (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)]; }
Ce n'est pas parfait - la dernière fois que j'ai essayé ce code, j'ai eu un comportement moche (sauter, si je me souviens bien) en revenant d'un rouleau élastique. Vous pourrez peut-être éviter cela en définissant simplement la
bounces
propriété de la vue de défilement surNO
.la source
Comme je ne semble pas encore autorisé à commenter, j'ajouterai mes commentaires à la réponse de Noah ici.
J'ai réussi à y parvenir par la méthode décrite par Noah Witherspoon. J'ai contourné le comportement de saut en n'appelant simplement pas la
setContentOffset:
méthode lorsque la vue de défilement a dépassé ses bords.- (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; ... }
J'ai également constaté que je devais implémenter la
-scrollViewWillBeginDecelerating:
méthodeUIScrollViewDelegate
pour attraper tous les cas.la source
J'ai essayé la solution ci-dessus qui superposait une vue transparente avec pointInside: withEvent: overridden. Cela a plutôt bien fonctionné pour moi, mais a échoué dans certains cas - voir mon commentaire. J'ai fini par implémenter la pagination moi-même avec une combinaison de scrollViewDidScroll pour suivre l'index de la page actuelle et scrollViewWillEndDragging: withVelocity: targetContentOffset et scrollViewDidEndDragging: willDecelerate pour s'aligner sur la page appropriée. Notez que la méthode will-end n'est disponible que sur iOS5 +, mais elle est assez douce pour cibler un décalage particulier si la vitesse! = 0. Plus précisément, vous pouvez indiquer à l'appelant où vous voulez que la vue de défilement atterrisse avec une animation s'il y a de la vitesse dans un direction particulière.
la source
Lors de la création de la vue de défilement, assurez-vous de définir ceci:
scrollView.showsHorizontalScrollIndicator = false; scrollView.showsVerticalScrollIndicator = false; scrollView.pagingEnabled = true;
Ajoutez ensuite vos sous-vues au défilement à un décalage égal à leur index * hauteur du défilement. C'est pour un scroller vertical:
UIView * sub = [UIView new]; sub.frame = CGRectMake(0, index * h, w, subViewHeight); [scrollView addSubview:sub];
Si vous l'exécutez maintenant, les vues sont espacées et avec la pagination activée, elles défilent une à la fois.
Alors mettez ceci dans votre méthode 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); }
Les images des sous-vues sont toujours espacées, nous les déplaçons simplement ensemble via une transformation lorsque l'utilisateur fait défiler.
De plus, vous avez accès à la variable p ci-dessus, que vous pouvez utiliser pour d'autres choses, comme l'alpha ou les transformations dans les sous-vues. Lorsque p == 1, cette page est entièrement affichée, ou plutôt elle tend vers 1.
la source
Essayez d'utiliser la propriété contentInset de scrollView:
scrollView.pagingEnabled = YES; [scrollView setContentSize:CGSizeMake(height, pageWidth * 3)]; double leftContentOffset = pageWidth - kSomeOffset; scrollView.contentInset = UIEdgeInsetsMake(0, leftContentOffset, 0, 0);
Il faudra peut-être jouer avec vos valeurs pour obtenir la pagination souhaitée.
J'ai trouvé que cela fonctionnait plus proprement par rapport aux alternatives affichées. Le problème avec l'utilisation de la
scrollViewWillEndDragging:
méthode déléguée est que l'accélération pour les films lents n'est pas naturelle.la source
C'est la seule vraie solution au problème.
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 } } }
la source
Voici ma réponse. Dans mon exemple, un
collectionView
qui a un en-tête de section est le scrollView que nous voulons lui donner unisPagingEnabled
effet personnalisé , et la hauteur de la cellule est une valeur 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 }
la source
Version Swift raffinée de la
UICollectionView
solution: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) }
la source
Ancien fil, mais il convient de mentionner mon point de vue à ce sujet:
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 } }
De cette façon, vous pouvez a) utiliser toute la largeur de la vue de défilement pour effectuer un panoramique / glisser et b) pouvoir interagir avec les éléments qui sont hors des limites d'origine de la vue de défilement
la source
Pour le problème UICollectionView (qui pour moi était un UITableViewCell d'une collection de cartes à défilement horizontal avec des "tickers" de la carte à venir / précédente), j'ai juste dû renoncer à utiliser la pagination native d'Apple. La solution github de Damien a fonctionné à merveille pour moi. Vous pouvez modifier la taille du tickler en augmentant la largeur de l'en-tête et en la redimensionnant dynamiquement à zéro lors du premier index afin de ne pas vous retrouver avec une grande marge vide
la source