Tengo una vista de desplazamiento que tiene el ancho de la pantalla, pero solo unos 70 píxeles de alto. Contiene muchos iconos de 50 x 50 (con espacio alrededor) entre los que quiero que el usuario pueda elegir. Pero siempre quiero que la vista de desplazamiento se comporte de manera paginada, siempre deteniéndose con un icono en el centro exacto.
Si los iconos fueran del ancho de la pantalla, esto no sería un problema porque la paginación de UIScrollView se encargaría de ello. Pero debido a que mis pequeños íconos son mucho menores que el tamaño del contenido, no funciona.
He visto este comportamiento antes en una aplicación llamada AllRecipes. Simplemente no sé cómo hacerlo.
¿Cómo puedo hacer que funcione la paginación por tamaño de icono?
fuente
También hay otra solución que probablemente sea un poco mejor que superponer la vista de desplazamiento con otra vista y anular hitTest.
Puede crear una subclase de UIScrollView y anular su pointInside. Luego, la vista de desplazamiento puede responder a los toques fuera de su marco. Por supuesto que el resto es igual.
@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
fuente
return [self.superview pointInside:[self convertPoint:point toView:self.superview] withEvent:event];
@amrox @Split no necesita responseInsets si tiene una supervista que actúa como vista de recorte.Veo muchas soluciones, pero son muy complejas. Una forma mucho más fácil de tener páginas pequeñas pero mantener toda el área desplazable es hacer que el desplazamiento sea más pequeño y moverlo
scrollView.panGestureRecognizer
a la vista principal. Estos son los pasos:Reducir el tamaño de scrollView
Asegúrese de que su vista de desplazamiento esté paginada y no recorte la subvista
En el código, mueva el gesto de panorámica de la vista de desplazamiento a la vista del contenedor principal que es de ancho completo:
override func viewDidLoad() { super.viewDidLoad() statsView.addGestureRecognizer(statsScrollView.panGestureRecognizer) }
fuente
La respuesta aceptada es muy buena, pero solo funcionará para la
UIScrollView
clase y ninguno de sus descendientes. Por ejemplo, si tiene muchas vistas y convierte a unaUICollectionView
, no podrá utilizar este método, porque la vista de colección eliminará las vistas que cree que "no son visibles" (por lo que, aunque no estén recortadas, lo harán desaparecer).El comentario sobre lo que menciona
scrollViewWillEndDragging:withVelocity:targetContentOffset:
es, en mi opinión, la respuesta correcta.Lo que puede hacer es, dentro de este método delegado, calcular la página / índice actual. Luego, decide si la velocidad y el desplazamiento objetivo merecen un movimiento de "página siguiente". Puedes acercarte bastante al
pagingEnabled
comportamiento.nota: por lo general soy un devoto de RubyMotion en estos días, así que alguien compruebe que este código de Obj-C es correcto. Perdón por la combinación de camelCase y snake_case, copié y pegué gran parte de este 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; } }
fuente
convertXToIndex:
yconvertIndexToX:
?velocity
cero, se recuperará, lo cual es extraño. Entonces tengo una mejor respuesta.- (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 es el ancho que desea que tenga su página. kPageOffset es si no desea que las celdas queden alineadas a la izquierda (es decir, si desea que estén alineadas al centro, establezca esto en la mitad del ancho de su celda). De lo contrario, debería ser cero.
Esto también solo permitirá desplazarse de una página a la vez.
fuente
Eche un vistazo al
-scrollView:didEndDragging:willDecelerate:
métodoUIScrollViewDelegate
. 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)]; }
No es perfecto; la última vez que probé este código, obtuve un comportamiento feo (saltando, según recuerdo) cuando regresaba de un pergamino con bandas de goma. Es posible que pueda evitarlo simplemente estableciendo la
bounces
propiedad de la vista de desplazamiento enNO
.fuente
Como parece que aún no se me permite comentar, agregaré mis comentarios a la respuesta de Noah aquí.
Lo logré con éxito mediante el método que describió Noah Witherspoon. Trabajé alrededor del comportamiento de salto simplemente no llamando al
setContentOffset:
método cuando la vista de desplazamiento ha pasado sus bordes.- (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; ... }
También descubrí que necesitaba implementar el
-scrollViewWillBeginDecelerating:
métodoUIScrollViewDelegate
para detectar todos los casos.fuente
Probé la solución anterior que superponía una vista transparente con pointInside: withEvent: overridden. Esto funcionó bastante bien para mí, pero se rompió en ciertos casos; vea mi comentario. Terminé simplemente implementando la paginación con una combinación de scrollViewDidScroll para rastrear el índice de la página actual y scrollViewWillEndDragging: withVelocity: targetContentOffset y scrollViewDidEndDragging: willDecelerate para ajustar a la página apropiada. Tenga en cuenta que el método will-end solo está disponible en iOS5 +, pero es bastante bueno para apuntar a un desplazamiento particular si la velocidad! = 0. Específicamente, puede decirle a la persona que llama dónde desea que aterrice la vista de desplazamiento con animación si hay velocidad en un dirección particular.
fuente
Al crear la vista de desplazamiento, asegúrese de configurar esto:
scrollView.showsHorizontalScrollIndicator = false; scrollView.showsVerticalScrollIndicator = false; scrollView.pagingEnabled = true;
Luego, agregue sus subvistas al desplazador con un desplazamiento igual a su índice * altura del desplazador. Esto es para un desplazador vertical:
UIView * sub = [UIView new]; sub.frame = CGRectMake(0, index * h, w, subViewHeight); [scrollView addSubview:sub];
Si lo ejecuta ahora, las vistas están espaciadas y, con la paginación habilitada, se desplazan de una en una.
Entonces pon esto en tu 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); }
Los fotogramas de las subvistas todavía están espaciados, solo los estamos moviendo juntos a través de una transformación a medida que el usuario se desplaza.
Además, tiene acceso a la variable p anterior, que puede usar para otras cosas, como alfa o transformaciones dentro de las subvistas. Cuando p == 1, esa página se muestra completamente, o más bien tiende a 1.
fuente
Intente usar la propiedad contentInset de scrollView:
scrollView.pagingEnabled = YES; [scrollView setContentSize:CGSizeMake(height, pageWidth * 3)]; double leftContentOffset = pageWidth - kSomeOffset; scrollView.contentInset = UIEdgeInsetsMake(0, leftContentOffset, 0, 0);
Puede ser necesario jugar con sus valores para lograr la paginación deseada.
He descubierto que esto funciona de manera más limpia en comparación con las alternativas publicadas. El problema con el uso del
scrollViewWillEndDragging:
método delegado es que la aceleración de los movimientos lentos no es natural.fuente
Ésta es la única solución real 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 } } }
fuente
Esta es mi respuesta. En mi ejemplo, un
collectionView
que tiene un encabezado de sección es el scrollView que queremos que tenga unisPagingEnabled
efecto personalizado , y la altura de la celda es un 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 }
fuente
Versión Swift refinada de la
UICollectionView
solución: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) }
fuente
Hilo antiguo, pero vale la pena mencionar mi opinión sobre esto:
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 esta manera, puede a) usar todo el ancho de la vista de desplazamiento para desplazarse / deslizarse yb) poder interactuar con los elementos que están fuera de los límites originales de la vista de desplazamiento
fuente
Para el problema de UICollectionView (que para mí era un UITableViewCell de una colección de tarjetas de desplazamiento horizontal con "tickers" de la tarjeta anterior / próxima), tuve que renunciar al uso de la paginación nativa de Apple. La solución github de Damien funcionó de maravilla para mí. Puede ajustar el tamaño del tickler aumentando el ancho del encabezado y dimensionándolo dinámicamente a cero cuando esté en el primer índice para que no termine con un gran margen en blanco
fuente