Vue avec défilement continu; à la fois horizontal et vertical

J'ai eu du mal avec cette mission depuis un certain temps maintenant. Ce que je voudrais développer est un scrollview ou collectionview qui défile continuellement à la fois vertical et horizontal.

Voici une image de la façon dont je pense que cela devrait ressembler. Les boîtes transparentes sont les vues / cellules qui sont rechargées à partir de la mémoire. Dès qu'une vue / cellule sort de l'écran, elle doit être réutilisée pour la nouvelle cellule à venir.. tout comme Comment fonctionne un UITableViewController.

Continue de défilement

Je sachez qu'un UICollectionView ne peut être fait pour défiler à l'infini horizontal ou vertical, pas les deux. Cependant, je ne sais pas comment faire cela en utilisant un UIScrollView. J'ai essayé le code attaché à une réponse à cette question et je peux l'obtenir pour recréer des vues (par exemple % 20) mais ce n'est pas vraiment ce dont j'ai besoin.. par ailleurs, il est pas continue.

Je sais que c'est possible, car L'application HBO Go le fait.. Je veux exactement la même fonctionnalité.

Ma Question: Comment puis-je atteindre mon objectif? Il n'existe aucun guides / tutoriels qui peuvent me montrer comment? Je ne peux pas en trouver.

27
demandé sur Community 2013-03-21 17:42:31

4 réponses

Vous pouvez obtenir un défilement infini, en utilisant la technique de recentrage du UIScrollView Après avoir obtenu une certaine distance du centre. Tout d'abord, vous devez faire le contentSize Assez grand pour que vous puissiez faire défiler un peu, donc je retourne 4 fois le nombre d'éléments dans Mes sections et 4 fois le nombre de sections, et utiliser l'opérateur mod dans la méthode cellForItemAtIndexPath pour obtenir le bon index dans mon tableau. Vous devez ensuite remplacer layoutSubviews dans une sous-classe de UICollectionView pour effectuer le recentrage (ceci est démontré dans le WWDC 2011 vidéo,"Advanced scroll View Techniques"). Voici la classe controller qui a la vue collection (configurée dans IB) en tant que sous-vue:

#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);

}

Voici la sous-classe 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;
}

Et enfin, voici la sous-classe 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
répondu rdelmar 2015-05-05 13:34:04

@mis à jour pour swift 3 et changé la façon dont le maxRow est calculé sinon la dernière colonne est cutoff et peut provoquer des erreurs

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
répondu TreeBucket 2017-02-22 05:21:20

La réponse de@rdelmar a fonctionné comme un charme, mais j'avais besoin de le faire dans swift. Voici la conversion:)

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
répondu BadPirate 2015-01-04 22:13:40

Réinitialiser le contentOffset est probablement la meilleure solution trouvée jusqu'à présent. défilement infini résultat final

Quelques mesures devraient être prises pour y parvenir:

  1. Pad éléments supplémentaires à la fois le côté gauche et droit de l'ensemble de données d'origine pour atteindre plus grande surface de défilement; cela est similaire à avoir un grand ensemble de données dupliquées, mais la différence est le montant;
  2. au début, contentOffset de la vue collection est calculé pour afficher uniquement l'ensemble de données d'origine (dessiné en noir les rectangles);
  3. lorsque l'utilisateur défile à droite et que contentOffset atteint la valeur de déclenchement, nous réinitialisons contentOffset pour afficher les mêmes résultats visuels; mais en fait des données différentes; lorsque l'utilisateur défile à gauche, la même logique est utilisée.

entrez la description de l'image ici

Donc, le levage lourd est dans le calcul du nombre d'articles doivent être rembourrés à la fois sur le côté gauche et à droite. Si vous jetez un oeil à l'illustration, vous trouverez qu'au minimum un écran supplémentaire d'éléments doit être rembourré sur les gauche et aussi, un autre écran supplémentaire sur la droite. Le montant exact rembourré dépend du nombre d'articles dans l'ensemble de données d'origine et de la taille de votre article.

J'ai écrit un post sur cette solution:

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

1
répondu Jiang Wang 2018-03-24 12:04:13