Vista con desplazamiento continuo; tanto horizontal como vertical


He estado luchando con esta tarea desde hace bastante tiempo. Lo que me gustaría desarrollar es un scrollview o collectionview que se desplaza continuamente tanto vertical como horizontal.

Aquí hay una imagen de cómo creo que debería verse esto. Las cajas transparentes son las vistas / celdas que se vuelven a cargar desde la memoria. Tan pronto como una vista/celda sale de la pantalla, debe reutilizarse para la próxima celda nueva.. así como funciona un UITableViewController.

Desplazamiento continuo

I sepa que un UICollectionView solo se puede hacer para desplazamiento infinito horizontal O vertical, no ambos. Sin embargo, no se como hacer esto usando un UIScrollView. Probé el código adjunto a una respuesta a esta pregunta y puedo hacer que vuelva a crear vistas (por ejemplo, % 20), pero eso no es realmente lo que necesito.. además, no es continuo.

Sé que es posible, porque la aplicación HBO Go hace esto.. Quiero exactamente la misma funcionalidad.

Mi pregunta: ¿Cómo puedo lograr mi objetivo? Hay guías / tutoriales que me pueden mostrar cómo? No encuentro ninguno.

Author: Community, 2013-03-21

4 answers

Puede obtener desplazamiento infinito, utilizando la técnica de volver a centrar el UIScrollView después de obtener una cierta distancia del centro. Primero, necesitas hacer el contentSize lo suficientemente grande como para que puedas desplazarte un poco, así que devuelvo 4 veces el número de elementos en mis secciones y 4 veces el número de secciones, y uso el operador mod en el método cellForItemAtIndexPath para obtener el índice correcto en mi matriz. A continuación, tiene que anular layoutSubviews en una subclase de UICollectionView para hacer el re-centrado (esto se demuestra en el WWDC 2011 video, "Advanced Scroll View Techniques"). Aquí está la clase de controlador que tiene la vista de colección (configurada en IB) como una subview:

#import "ViewController.h"
#import "MultpleLineLayout.h"
#import "DataCell.h"

@interface ViewController ()
@property (weak,nonatomic) IBOutlet UICollectionView *collectionView;
@property (strong,nonatomic) NSArray *theData;
@end

@implementation ViewController

- (void)viewDidLoad {
    self.theData = @[@[@"1",@"2",@"3",@"4",@"5"], @[@"6",@"7",@"8",@"9",@"10"],@[@"11",@"12",@"13",@"14",@"15"],@[@"16",@"17",@"18",@"19",@"20"]];
    MultpleLineLayout *layout = [[MultpleLineLayout alloc] init];
    self.collectionView.collectionViewLayout = layout;
    self.collectionView.showsHorizontalScrollIndicator = NO;
    self.collectionView.showsVerticalScrollIndicator = NO;
    layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
    self.view.backgroundColor = [UIColor blackColor];
    [self.collectionView registerClass:[DataCell class] forCellWithReuseIdentifier:@"DataCell"];
    [self.collectionView reloadData];
}


- (NSInteger)collectionView:(UICollectionView *)view numberOfItemsInSection:(NSInteger)section {
    return 20;
}

- (NSInteger)numberOfSectionsInCollectionView: (UICollectionView *)collectionView {
    return 16;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView  cellForItemAtIndexPath:(NSIndexPath *)indexPath {

    DataCell *cell = [collectionView  dequeueReusableCellWithReuseIdentifier:@"DataCell" forIndexPath:indexPath];
    cell.label.text = self.theData[indexPath.section %4][indexPath.row %5];
    return cell;
}

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
   // UICollectionViewCell *item = [collectionView cellForItemAtIndexPath:indexPath];
    NSLog(@"%@",indexPath);

}

Aquí está la subclase UICollectionViewFlowLayout:

#define space 5
#import "MultpleLineLayout.h"

@implementation MultpleLineLayout { // a subclass of UICollectionViewFlowLayout
    NSInteger itemWidth;
    NSInteger itemHeight;
}

-(id)init {
    if (self = [super init]) {
        itemWidth = 60;
        itemHeight = 60;
    }
    return self;
}

-(CGSize)collectionViewContentSize {
    NSInteger xSize = [self.collectionView numberOfItemsInSection:0] * (itemWidth + space); // "space" is for spacing between cells.
    NSInteger ySize = [self.collectionView numberOfSections] * (itemHeight + space);
    return CGSizeMake(xSize, ySize);
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)path {
    UICollectionViewLayoutAttributes* attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:path];
    attributes.size = CGSizeMake(itemWidth,itemHeight);
    int xValue = itemWidth/2 + path.row * (itemWidth + space);
    int yValue = itemHeight + path.section * (itemHeight + space);
    attributes.center = CGPointMake(xValue, yValue);
    return attributes;
}


-(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect {
    NSInteger minRow =  (rect.origin.x > 0)?  rect.origin.x/(itemWidth + space) : 0; // need to check because bounce gives negative values  for x.
    NSInteger maxRow = rect.size.width/(itemWidth + space) + minRow;
    NSMutableArray* attributes = [NSMutableArray array];
    for(NSInteger i=0 ; i < self.collectionView.numberOfSections; i++) {
        for (NSInteger j=minRow ; j < maxRow; j++) {
            NSIndexPath* indexPath = [NSIndexPath indexPathForItem:j inSection:i];
            [attributes addObject:[self layoutAttributesForItemAtIndexPath:indexPath]];
        }
    }
    return attributes;
}

Y finalmente, aquí está la subclase de UICollectionView:

-(void)layoutSubviews {
    [super layoutSubviews];
    CGPoint currentOffset = self.contentOffset;
    CGFloat contentWidth = self.contentSize.width;
    CGFloat contentHeight = self.contentSize.height;
    CGFloat centerOffsetX = (contentWidth - self.bounds.size.width)/ 2.0;
    CGFloat centerOffsetY = (contentHeight - self.bounds.size.height)/ 2.0;
    CGFloat distanceFromCenterX = fabsf(currentOffset.x - centerOffsetX);
    CGFloat distanceFromCenterY = fabsf(currentOffset.y - centerOffsetY);

    if (distanceFromCenterX > contentWidth/4.0) { // this number of 4.0 is arbitrary
        self.contentOffset = CGPointMake(centerOffsetX, currentOffset.y);
    }
    if (distanceFromCenterY > contentHeight/4.0) {
        self.contentOffset = CGPointMake(currentOffset.x, centerOffsetY);
    }
}
 29
Author: rdelmar,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-05-05 13:34:04

@actualizado para swift 3 y cambiado cómo se calcula el maxRow de lo contrario la última columna se corta y puede causar errores

import UIKit

class NodeMap : UICollectionViewController {
    var rows = 10
    var cols = 10

    override func viewDidLoad(){
        self.collectionView!.collectionViewLayout = NodeLayout(itemWidth: 400.0, itemHeight: 300.0, space: 5.0)
    }

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return rows
    }

    override func numberOfSections(in collectionView: UICollectionView) -> Int {
        return cols
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        return collectionView.dequeueReusableCell(withReuseIdentifier: "node", for: indexPath)
    }
}

class NodeLayout : UICollectionViewFlowLayout {
    var itemWidth : CGFloat
    var itemHeight : CGFloat
    var space : CGFloat
    var columns: Int{
        return self.collectionView!.numberOfItems(inSection: 0)
    }
    var rows: Int{
        return self.collectionView!.numberOfSections
    }

    init(itemWidth: CGFloat, itemHeight: CGFloat, space: CGFloat) {
        self.itemWidth = itemWidth
        self.itemHeight = itemHeight
        self.space = space
        super.init()
    }

    required init(coder aDecoder: NSCoder) {
        self.itemWidth = 50
        self.itemHeight = 50
        self.space = 3
        super.init()
    }

    override var collectionViewContentSize: CGSize{
        let w : CGFloat = CGFloat(columns) * (itemWidth + space)
        let h : CGFloat = CGFloat(rows) * (itemHeight + space)
        return CGSize(width: w, height: h)
    }

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
        let x : CGFloat = CGFloat(indexPath.row) * (itemWidth + space)
        let y : CGFloat = CGFloat(indexPath.section) + CGFloat(indexPath.section) * (itemHeight + space)
        attributes.frame = CGRect(x: x, y: y, width: itemWidth, height: itemHeight)
        return attributes
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let minRow : Int = (rect.origin.x > 0) ? Int(floor(rect.origin.x/(itemWidth + space))) : 0
        let maxRow : Int = min(columns - 1, Int(ceil(rect.size.width / (itemWidth + space)) + CGFloat(minRow)))
        var attributes : Array<UICollectionViewLayoutAttributes> = [UICollectionViewLayoutAttributes]()
        for i in 0 ..< rows {
            for j in minRow ... maxRow {
                attributes.append(self.layoutAttributesForItem(at: IndexPath(item: j, section: i))!)
            }
        }
        return attributes
    }
}
 7
Author: TreeBucket,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-02-22 05:21:20

La respuesta de@rdelmar funcionó como un encanto, pero necesitaba hacerlo en swift. Aquí está la conversión:)

class NodeMap : UICollectionViewController {
    @IBOutlet var activateNodeButton : UIBarButtonItem?
    var rows = 10
    var cols = 10
    override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return rows
    }
    override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
        return cols
    }
    override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        return collectionView.dequeueReusableCellWithReuseIdentifier("node", forIndexPath: indexPath)
    }
    override func viewDidLoad() {
        self.collectionView!.collectionViewLayout = NodeLayout(itemWidth: 100.0, itemHeight: 100.0, space: 5.0)
    }
}

class NodeLayout : UICollectionViewFlowLayout {
    var itemWidth : CGFloat
    var itemHeight : CGFloat
    var space : CGFloat
    init(itemWidth: CGFloat, itemHeight: CGFloat, space: CGFloat) {
        self.itemWidth = itemWidth
        self.itemHeight = itemHeight
        self.space = space
        super.init()
    }
    required init(coder aDecoder: NSCoder) {
        self.itemWidth = 50
        self.itemHeight = 50
        self.space = 3
        super.init()
    }
    override func collectionViewContentSize() -> CGSize {
        let w : CGFloat = CGFloat(self.collectionView!.numberOfItemsInSection(0)) * (itemWidth + space)
        let h : CGFloat = CGFloat(self.collectionView!.numberOfSections()) * (itemHeight + space)
        return CGSizeMake(w, h)
    }
    override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
        let attributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
        let x : CGFloat = CGFloat(indexPath.row) * (itemWidth + space)
        let y : CGFloat = CGFloat(indexPath.section) + CGFloat(indexPath.section) * (itemHeight + space)
        attributes.frame = CGRectMake(x, y, itemWidth, itemHeight)
        return attributes
    }
    override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
        let minRow : Int = (rect.origin.x > 0) ? Int(floor(rect.origin.x/(itemWidth + space))) : 0
        let maxRow : Int = Int(floor(rect.size.width/(itemWidth + space)) + CGFloat(minRow))
        var attributes : Array<UICollectionViewLayoutAttributes> = [UICollectionViewLayoutAttributes]()
        for i in 0...self.collectionView!.numberOfSections()-1 {
            for j in minRow...maxRow {
                attributes.append(self.layoutAttributesForItemAtIndexPath(NSIndexPath(forItem: j, inSection: i)))
            }
        }
        return attributes
    }
}
 2
Author: BadPirate,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-01-04 22:13:40

Restablecer el contentOffset probablemente sea la mejor solución calculada hasta ahora. resultado final de desplazamiento infinito

Se deben tomar algunas medidas para lograr esto:

  1. Rellene elementos adicionales tanto en el lado izquierdo como en el derecho del conjunto de datos original para lograr un área desplazable más grande; Esto es similar a tener un gran conjunto de datos duplicado, pero la diferencia es la cantidad;
  2. Al inicio, el contentOffset de la vista de colección se calcula para mostrar solo el conjunto de datos original (dibujado en negro rectángulos);
  3. Cuando el usuario se desplaza a la derecha y contentOffset alcanza el valor del disparador, restablecemos contentOffset para mostrar los mismos resultados visuales; pero en realidad datos diferentes; Cuando el usuario se desplaza a la izquierda, se utiliza la misma lógica.

introduzca la descripción de la imagen aquí

Por lo tanto, el levantamiento pesado está en el cálculo de cuántos elementos deben ser acolchados tanto en el lado izquierdo como en el derecho. Si echa un vistazo a la ilustración, encontrará que un mínimo de una pantalla adicional de artículos debe ser rellenado en izquierda y también, otra pantalla extra a la derecha. La cantidad exacta acolchada depende de cuántos artículos hay en el conjunto de datos original y qué tan grande es el tamaño de su artículo.

Escribí un post sobre esta solución:

Http://www.awsomejiang.com/2018/03/24/Infinite-Scrolling-and-the-Tiling-Logic /

 1
Author: Jiang Wang,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-03-24 12:04:13