Paginación de UIScrollView en incrementos menores que el tamaño del marco

87

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?

Henning
fuente

Respuestas:

123

Intente hacer que su vista de desplazamiento sea menor que el tamaño de la pantalla (a lo ancho), pero desmarque la casilla de verificación "Subvistas del clip" en IB. Luego, superponga una vista transparente, userInteractionEnabled = NO encima de ella (a todo su ancho), que anula hitTest: withEvent: para devolver su vista de desplazamiento. Eso debería darte lo que estás buscando. Consulte esta respuesta para obtener más detalles.

Ben Gottlieb
fuente
1
No estoy muy seguro de lo que quiere decir, miraré la respuesta que señala.
Henning
8
Esta es una hermosa solución. No había pensado en anular la función hitTest: y eso prácticamente elimina el único inconveniente de este enfoque. Bien hecho.
Ben Gotow
¡Buena solución! Realmente me ayudó.
DevDevDev
8
Esto funciona muy bien, pero recomiendo que en lugar de simplemente devolver scrollView, devuelva [scrollView hitTest: point withEvent: event] para que los eventos pasen correctamente. Por ejemplo, en una vista de desplazamiento llena de UIButtons, los botones seguirán funcionando de esta manera.
greenisus
2
tenga en cuenta que esto NO funcionará con una vista de tabla o una vista de colección, ya que no representará cosas fuera del marco. Vea mi respuesta a continuación
vish
63

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
División
fuente
1
¡si! además, si los límites de su scrollview son los mismos que los de su padre, puede devolver sí desde el método pointInside. gracias
cocoatoucher
5
Me gusta esta solución, aunque tengo que cambiar parentLocation a "[self convertPoint: point toView: self.superview]" para que funcione correctamente.
amrox
Tienes razón, convertPoint es mucho mejor de lo que escribí allí.
Split
6
directamente 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.
Cœur
Esta solución parecía fallar cuando llegaba al final de mi lista de elementos si la última página de elementos no llenaba completamente la página. Es un poco difícil de explicar, pero si muestro 3.5 celdas por página y tengo 5 elementos, la segunda página me mostrará 2 celdas con un espacio vacío a la derecha, o mostrará 3 celdas con un encabezado celda parcial. El problema era que si estaba en el estado en el que me mostraba un espacio vacío y seleccionaba una celda, la paginación se corregiría sola durante la transición de navegación ... Publicaré mi solución a continuación.
Tyler
18

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.panGestureRecognizera la vista principal. Estos son los pasos:

  1. Reducir el tamaño de scrollViewEl tamaño de ScrollView es más pequeño que el principal

  2. Asegúrese de que su vista de desplazamiento esté paginada y no recorte la subvista ingrese la descripción de la imagen aquí

  3. 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)
    }
Ángel G. Olloqui
fuente
Esta es una solución ecológica. Muy inteligente.
superpuccio
¡Esto es realmente lo que quiero!
LYM
¡Muy inteligente! Me ahorraste horas. ¡Gracias!
Erik
6

La respuesta aceptada es muy buena, pero solo funcionará para la UIScrollViewclase y ninguno de sus descendientes. Por ejemplo, si tiene muchas vistas y convierte a una UICollectionView, 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 pagingEnabledcomportamiento.

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;
    }
} 
colinta
fuente
jaja, ahora me pregunto si no debería haber mencionado RubyMotion, ¡me está haciendo perder la votación! No, en realidad, deseo que los votos negativos también incluyan algún comentario, me encantaría saber qué es lo que la gente encuentra mal en mi explicación.
colinta
¿Puede incluir la definición de convertXToIndex:y convertIndexToX:?
mr5
Sin completar. Cuando arrastra lentamente y levanta la mano con velocitycero, se recuperará, lo cual es extraño. Entonces tengo una mejor respuesta.
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 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.

vish
fuente
3

Eche un vistazo al -scrollView:didEndDragging:willDecelerate:método 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 feo (saltando, según recuerdo) cuando regresaba de un pergamino con bandas de goma. Es posible que pueda evitarlo simplemente estableciendo la bouncespropiedad de la vista de desplazamiento en NO.

Noah Witherspoon
fuente
3

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étodo UIScrollViewDelegatepara detectar todos los casos.

Jacob Persson
fuente
¿Qué casos adicionales deben detectarse?
2011
el caso en el que no hubo arrastre en absoluto. El usuario levanta el dedo y el desplazamiento se detiene inmediatamente.
Jess Bowers
3

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.

Tyler
fuente
0

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.

Johnny Rockex
fuente
0

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.

Vlad
fuente
0

É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

        }

    }


}
TimWhiting
fuente
0

Esta es mi respuesta. En mi ejemplo, un collectionViewque tiene un encabezado de sección es el scrollView que queremos que tenga un isPagingEnabledefecto 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
}
DawnSong
fuente
0

Versión Swift refinada de la UICollectionViewsolución:

  • Límites a una página por deslizamiento
  • Garantiza un ajuste rápido a la página, incluso si su desplazamiento fue 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)
}
Patrick Pijnappel
fuente
0

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

basvk
fuente
0

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

David
fuente