iphone - doesn - Localizar UIScrollView en incrementos menores que el tamaño del marco
uiscrollview autolayout (12)
Al crear la vista de desplazamiento, asegúrese de configurar esto:
scrollView.showsHorizontalScrollIndicator = false;
scrollView.showsVerticalScrollIndicator = false;
scrollView.pagingEnabled = true;
A continuación, agregue sus subvistas a la barra de desplazamiento en un desplazamiento igual a su altura de índice * del desplazador. Esto es para un desplazamiento 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 a una por vez.
Entonces ponga esto en su 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 marcos de las subvistas aún 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 de arriba, 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 hacia 1.
Tengo una vista de desplazamiento que es el ancho de la pantalla, pero con solo unos 70 píxeles de alto. Contiene muchos iconos de 50 x 50 (con espacio alrededor) de los que quiero que el usuario pueda elegir. Pero siempre quiero que la vista de desplazamiento se comporte de forma paginativa, deteniéndose siempre con un ícono en el centro exacto.
Si los íconos tuvieran el ancho de la pantalla, esto no sería un problema porque el paginado de UIScrollView se ocuparía de ello. Pero como mis iconos pequeños son mucho menos 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.
¿Alguna idea sobre cómo hacer que la paginación funcione según el tamaño de cada ícono?
Aquí está mi respuesta. En mi ejemplo, un collectionView
que tiene un encabezado de sección es scrollView que queremos que tenga el efecto personalizado isPagingEnabled
, y el alto 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
}
Como parece que todavía no puedo comentar, agregaré mis comentarios a la respuesta de Noah aquí.
Lo logré exitosamente con el método que describió Noah Witherspoon. Trabajé alrededor del comportamiento de salto simplemente sin llamar al método setContentOffset:
cuando la vista de desplazamiento está más allá de 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 encontré que necesitaba implementar el método -scrollViewWillBeginDecelerating:
en UIScrollViewDelegate
para detectar todos los casos.
Eche un vistazo al -scrollView:didEndDragging:willDecelerate:
en 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)];
}
No es perfecto; la última vez que probé este código, obtuve un comportamiento desagradable (saltando, según recuerdo) al regresar de un rollo de goma. Es posible que pueda evitar eso simplemente configurando la propiedad de bounces
la vista de desplazamiento en NO
.
Esta 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
}
}
}
Intenta hacer que la vista de desplazamiento sea menor que el tamaño de la pantalla (en lo que respecta al ancho), pero desmarca la casilla de verificación "Subvistas de clip" en IB. Luego, superponga una vista transparente, userInteractionEnabled = NO en la parte superior (con el ancho completo), que anula hitTest: withEvent: para devolver la vista de desplazamiento. Eso debería darte lo que estás buscando. Vea esta respuesta para más detalles.
Intente utilizar 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 tomar algunos juegos con sus valores para lograr la paginación deseada.
He encontrado que esto funciona más limpiamente en comparación con las alternativas publicadas. Problema al usar scrollViewWillEndDragging:
método de delegado es la aceleración para películas lentas no es natural.
La respuesta aceptada es muy buena, pero solo funcionará para la clase UIScrollView
y ninguno de sus descendientes. Por ejemplo, si tiene muchas vistas y convierte a UICollectionView
, no podrá usar este método, porque la vista de colección eliminará las vistas que cree que son "no visibles" (por lo tanto, aunque no estén recortadas, desaparecerá).
El comentario sobre eso 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, usted decide si la velocidad y la compensación del objetivo merecen un movimiento de "siguiente página". Puede acercarse bastante al comportamiento pagingEnabled
.
Nota: Por lo general, soy un desarrollador de RubyMotion estos días, por lo que alguien pruebe este código de Obj-C para ver si es correcto. Lo siento por la mezcla 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;
}
}
Probé la solución anterior que superpuso una vista transparente con pointInside: withEvent: anulado. Esto funcionó bastante bien para mí, pero se rompió en ciertos casos - ver mi comentario. Terminé implementando la paginación con una combinación de scrollViewDidScroll para seguir el índice de la página actual y scrollViewWillEndDragging: conVelocity: targetContentOffset y scrollViewDidEndDragging: desacelerará para ajustarse a la página adecuada. Tenga en cuenta que el método Will-End solo está disponible para iOS5 +, pero es bastante bueno para seleccionar un desplazamiento particular si la velocidad es igual a 0. Específicamente, puede decirle a la persona que llama dónde desea que la vista de desplazamiento aterrice con animación si hay velocidad en un dirección particular.
También hay otra solución que probablemente sea un poco mejor que superponer la vista de desplazamiento con otra vista y reemplazar a hitTest.
Puede subclase UIScrollView y anular su pointInside. Luego, la vista de desplazamiento puede responder por toques fuera de su marco. Por supuesto, el resto es lo mismo.
@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
Veo muchas soluciones, pero son muy complejas. Una forma mucho más sencilla de tener páginas pequeñas pero manteniendo toda el área desplazable, es hacer que el desplazamiento sea más pequeño y mover el scrollView.panGestureRecognizer
a su vista principal. Estos son los pasos:
Asegúrese de que su vista de desplazamiento esté paginada y no recorta la vista secundaria
En el código, mueva el gesto de panoramización de desplazamiento hacia la vista del contenedor primario que es de ancho completo:
override func viewDidLoad() {
super.viewDidLoad()
statsView.addGestureRecognizer(statsScrollView.panGestureRecognizer)
}
- (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 quiere que sea su página. kPageOffset es si no desea que las celdas se alineen a la izquierda (es decir, si desea alinearlas en el centro, ajústelas a la mitad del ancho de su celda). De lo contrario, debería ser cero.
Esto también solo permitirá desplazarse una página a la vez.