web-dev-qa-db-fra.com

Comment déterminer l'espacement entre les cellules dans UICollectionView flowLayout

J'ai un UICollectionView avec une disposition de flux et chaque cellule est un carré. Comment puis-je déterminer l'espacement entre chaque cellule sur chaque ligne? Je n'arrive pas à trouver les réglages appropriés pour cela. Je vois qu'il y a des attributs d'espacement min dans le fichier nib pour une vue de collection, mais je règle ceci à 0 et les cellules ne collent même pas.

enter image description here

Une autre idée?

46
adit

Mise à jour: Version Swift de cette réponse: https://github.com/fanpyi/UICollectionViewLeftAlignedLayout-Swift


Prendre l’avance de @ matt j’ai modifié son code pour s’assurer que les articles sont TOUJOURS alignés. J'ai découvert que si un élément finissait sur une ligne, il serait centré par la disposition du flux. J'ai apporté les modifications suivantes pour résoudre ce problème.

Cette situation ne se produirait que si les cellules dont la largeur varie sont différentes, ce qui pourrait donner une présentation similaire à celle-ci. La dernière ligne toujours laissée s'aligne en raison du comportement de UICollectionViewFlowLayout, le problème réside dans les éléments qui se trouvent eux-mêmes dans n'importe quelle ligne sauf la dernière.

Je voyais avec le code de @ matt.

enter image description here

Dans cet exemple, nous voyons que les cellules se centrent si elles se retrouvent seules sur la ligne. Le code ci-dessous assure que votre vue de collection ressemble à ceci.

enter image description here

#import "CWDLeftAlignedCollectionViewFlowLayout.h"

const NSInteger kMaxCellSpacing = 9;

@implementation CWDLeftAlignedCollectionViewFlowLayout

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    NSArray* attributesToReturn = [super layoutAttributesForElementsInRect:rect];
    for (UICollectionViewLayoutAttributes* attributes in attributesToReturn) {
        if (nil == attributes.representedElementKind) {
            NSIndexPath* indexPath = attributes.indexPath;
            attributes.frame = [self layoutAttributesForItemAtIndexPath:indexPath].frame;
        }
    }
    return attributesToReturn;
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewLayoutAttributes* currentItemAttributes =
    [super layoutAttributesForItemAtIndexPath:indexPath];

    UIEdgeInsets sectionInset = [(UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout sectionInset];

    if (indexPath.item == 0) { // first item of section
        CGRect frame = currentItemAttributes.frame;
        frame.Origin.x = sectionInset.left; // first item of the section should always be left aligned
        currentItemAttributes.frame = frame;

        return currentItemAttributes;
    }

    NSIndexPath* previousIndexPath = [NSIndexPath indexPathForItem:indexPath.item-1 inSection:indexPath.section];
    CGRect previousFrame = [self layoutAttributesForItemAtIndexPath:previousIndexPath].frame;
    CGFloat previousFrameRightPoint = previousFrame.Origin.x + previousFrame.size.width + kMaxCellSpacing;

    CGRect currentFrame = currentItemAttributes.frame;
    CGRect strecthedCurrentFrame = CGRectMake(0,
                                              currentFrame.Origin.y,
                                              self.collectionView.frame.size.width,
                                              currentFrame.size.height);

    if (!CGRectIntersectsRect(previousFrame, strecthedCurrentFrame)) { // if current item is the first item on the line
        // the approach here is to take the current frame, left align it to the Edge of the view
        // then stretch it the width of the collection view, if it intersects with the previous frame then that means it
        // is on the same line, otherwise it is on it's own new line
        CGRect frame = currentItemAttributes.frame;
        frame.Origin.x = sectionInset.left; // first item on the line should always be left aligned
        currentItemAttributes.frame = frame;
        return currentItemAttributes;
    }

    CGRect frame = currentItemAttributes.frame;
    frame.Origin.x = previousFrameRightPoint;
    currentItemAttributes.frame = frame;
    return currentItemAttributes;
}

@end
68
Chris Wagner

Pour obtenir un espacement maximal entre les éléments, sous-classe UICollectionViewFlowLayout et remplacez layoutAttributesForElementsInRect: et layoutAttributesForItemAtIndexPath:.

Par exemple, un problème courant est le suivant: les lignes d'une vue de collection sont justifiées à droite et à gauche, à l'exception de la dernière ligne qui est justifiée à gauche. Disons que nous voulons toutes les lignes doivent être justifiées à gauche, de sorte que l'espace entre elles soit, disons, de 10 points. Voici un moyen simple (dans votre sous-classe UICollectionViewFlowLayout):

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    NSArray* arr = [super layoutAttributesForElementsInRect:rect];
    for (UICollectionViewLayoutAttributes* atts in arr) {
        if (nil == atts.representedElementKind) {
            NSIndexPath* ip = atts.indexPath;
            atts.frame = [self layoutAttributesForItemAtIndexPath:ip].frame;
        }
    }
    return arr;
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewLayoutAttributes* atts =
    [super layoutAttributesForItemAtIndexPath:indexPath];

    if (indexPath.item == 0) // degenerate case 1, first item of section
        return atts;

    NSIndexPath* ipPrev =
    [NSIndexPath indexPathForItem:indexPath.item-1 inSection:indexPath.section];

    CGRect fPrev = [self layoutAttributesForItemAtIndexPath:ipPrev].frame;
    CGFloat rightPrev = fPrev.Origin.x + fPrev.size.width + 10;
    if (atts.frame.Origin.x <= rightPrev) // degenerate case 2, first item of line
        return atts;

    CGRect f = atts.frame;
    f.Origin.x = rightPrev;
    atts.frame = f;
    return atts;
}

La raison pour laquelle cela est si facile, c'est que nous n'effectuons pas vraiment la lourde tâche de la mise en page; Nous tirons parti du travail de mise en page qu'UICollectionViewFlowLayout a déjà effectué pour nous. Il a déjà décidé du nombre d'éléments à inclure dans chaque ligne. nous ne faisons que lire ces lignes et regrouper les éléments ensemble, si vous voyez ce que je veux dire.

32
matt

Il y a quelques points à considérer:

  1. Essayez de modifier l'espacement minimum dans IB, mais laissez le curseur dans ce champ. Notez que Xcode ne marque pas immédiatement le document comme modifié. Cependant, lorsque vous cliquez dans un autre champ, Xcode constate que le document a été modifié et le marque dans le navigateur de fichiers. Assurez-vous donc de cliquer ou de cliquer sur un autre champ après avoir apporté une modification.

  2. Enregistrez votre fichier de storyboard/xib après avoir apporté une modification et veillez à reconstruire l'application. Il n’est pas difficile de passer à côté de cette étape et vous vous retrouvez à vous demander pourquoi vos modifications ne semblent pas avoir eu d’effet.

  3. UICollectionViewFlowLayout a une propriété minimumInteritemSpacing, qui correspond à ce que vous définissez dans IB. Mais le délégué de la collection peut aussi avoir une méthode pour déterminer l'espacement entre les articles . Cette méthode est la propriété de la mise en page, donc si vous l'implémentez dans votre délégué, la propriété de votre mise en page ne sera pas utilisée.

  4. N'oubliez pas que l'espacement est de minimum espacement. La présentation utilisera ce nombre (qu'il provienne de la propriété ou de la méthode déléguée) comme le plus petit espace autorisé, mais il peut utiliser un plus grand espace s'il reste de l'espace sur la ligne. Ainsi, si, par exemple, vous définissez un espacement minimal de 0, il est toujours possible de voir quelques pixels entre les éléments. Si vous voulez plus de contrôle sur la façon dont les éléments sont espacés, vous devriez probablement utiliser une mise en page différente (éventuellement une de vos propres créations).

16
Caleb

Un peu de maths fait le tour plus facilement. Le code écrit par Chris Wagner est horrible car il appelle les attributs de présentation de chaque élément précédent. Donc, plus vous faites défiler, plus c'est lent ...

Utilisez simplement modulo comme ceci (j'utilise aussi ma valeur minimumInteritemSpacing comme valeur max):

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewLayoutAttributes* currentItemAttributes = [super layoutAttributesForItemAtIndexPath:indexPath];
    NSInteger numberOfItemsPerLine = floor([self collectionViewContentSize].width / [self itemSize].width);

    if (indexPath.item % numberOfItemsPerLine != 0)
    {
        NSInteger cellIndexInLine = (indexPath.item % numberOfItemsPerLine);

        CGRect itemFrame = [currentItemAttributes frame];
        itemFrame.Origin.x = ([self itemSize].width * cellIndexInLine) + ([self minimumInteritemSpacing] * cellIndexInLine);
        currentItemAttributes.frame = itemFrame;
    }

    return currentItemAttributes;
}
4
ldesroziers

Un moyen facile de justifier à gauche consiste à modifier layoutAttributesForElementsInRect: dans votre sous-classe de UICollectionViewFlowLayout:

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    NSArray *allLayoutAttributes = [super layoutAttributesForElementsInRect:rect];
    CGRect prevFrame = CGRectMake(-FLT_MAX, -FLT_MAX, 0, 0);
    for (UICollectionViewLayoutAttributes *layoutAttributes in allLayoutAttributes)
    {
        //fix blur
        CGRect theFrame = CGRectIntegral(layoutAttributes.frame);

        //left justify
        if(prevFrame.Origin.x > -FLT_MAX &&
           prevFrame.Origin.y >= theFrame.Origin.y &&
           prevFrame.Origin.y <= theFrame.Origin.y) //workaround for float == warning
        {
            theFrame.Origin.x = prevFrame.Origin.x +
                                prevFrame.size.width + 
                                EXACT_SPACE_BETWEEN_ITEMS;
        }
        prevFrame = theFrame;

        layoutAttributes.frame = theFrame;
    }
    return allLayoutAttributes;
}
3
xytor

La version Swift de la solution Chris.

class PazLeftAlignedCollectionViewFlowLayout : UICollectionViewFlowLayout {
    var maxCellSpacing = 14.0
    override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
        if var attributesToReturn = super.layoutAttributesForElementsInRect(rect) as? Array<UICollectionViewLayoutAttributes> {
            for attributes in attributesToReturn {
                if attributes.representedElementKind == nil {
                    let indexPath = attributes.indexPath
                    attributes.frame = self.layoutAttributesForItemAtIndexPath(indexPath).frame;
                }
            }
            return attributesToReturn;
        }
        return super.layoutAttributesForElementsInRect(rect)
    }

    override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
        let currentItemAttributes = super.layoutAttributesForItemAtIndexPath(indexPath)

        if let collectionViewFlowLayout = self.collectionView?.collectionViewLayout as? UICollectionViewFlowLayout {
            let sectionInset = collectionViewFlowLayout.sectionInset
            if (indexPath.item == 0) { // first item of section
                var frame = currentItemAttributes.frame;
                frame.Origin.x = sectionInset.left; // first item of the section should always be left aligned
                currentItemAttributes.frame = frame;

                return currentItemAttributes;
            }

            let previousIndexPath = NSIndexPath(forItem:indexPath.item-1, inSection:indexPath.section)
            let previousFrame = self.layoutAttributesForItemAtIndexPath(previousIndexPath).frame;
            let previousFrameRightPoint = Double(previousFrame.Origin.x) + Double(previousFrame.size.width) + self.maxCellSpacing

            let currentFrame = currentItemAttributes.frame
            var width : CGFloat = 0.0
            if let collectionViewWidth = self.collectionView?.frame.size.width {
                width = collectionViewWidth
            }
            let strecthedCurrentFrame = CGRectMake(0,
                currentFrame.Origin.y,
                width,
                currentFrame.size.height);

            if (!CGRectIntersectsRect(previousFrame, strecthedCurrentFrame)) { // if current item is the first item on the line
                // the approach here is to take the current frame, left align it to the Edge of the view
                // then stretch it the width of the collection view, if it intersects with the previous frame then that means it
                // is on the same line, otherwise it is on it's own new line
                var frame = currentItemAttributes.frame;
                frame.Origin.x = sectionInset.left; // first item on the line should always be left aligned
                currentItemAttributes.frame = frame;
                return currentItemAttributes;
            }

            var frame = currentItemAttributes.frame;
            frame.Origin.x = CGFloat(previousFrameRightPoint)
            currentItemAttributes.frame = frame;
        }
        return currentItemAttributes;
    }
}

Pour l'utiliser, procédez comme suit:

    override func viewDidLoad() {
    super.viewDidLoad()
    self.collectionView.collectionViewLayout = self.layout
}
var layout : PazLeftAlignedCollectionViewFlowLayout {
    var layout = PazLeftAlignedCollectionViewFlowLayout()
    layout.itemSize = CGSizeMake(220.0, 230.0)
    layout.minimumLineSpacing = 12.0
    return layout
}
2
zirinisp

Vous pouvez le faire de deux manières.

Tout d’abord, faites quelques modifications dans layoutAttributesForItem,

récupère la disposition des attributs actuels via le layoutAttributesForItem(at: IndexPath(item: indexPath.item - 1, section: indexPath.section))?.frame précédent.

layoutAttributesForItem (at :): cette méthode fournit des informations de présentation à la demande à la vue de collection. Vous devez le remplacer et renvoyer les attributs de présentation de l'élément à la propriété indexPath demandée.

Deuxièmement, créez de nouveaux attributs via UICollectionViewLayoutAttributes(forCellWith: indexPath), et organisez-les où vous le souhaitez.

Et le calcul est un peu plus grand, car il effectue le levage lourd de la mise en page.

layoutAttributesForElements (in :): dans cette méthode, vous devez renvoyer les attributs de présentation de tous les éléments du rectangle donné. Vous renvoyez les attributs à la vue de collection sous forme de tableau de UICollectionViewLayoutAttributes.


Vous pouvez vérifier My repo

  • vous pouvez mettre les éléments laissés alignés

  • vous pouvez mettre les articles bien alignés

  • vous pouvez mettre les éléments bien alignés et inversés

2
dengApro

Solution Clean Swift, issue d'une histoire d'évolution:

  1. il y avait réponse mate
  2. il y avait Chris Wagner seuls articles résoudre
  3. il y avait mokagio sectionInset et minimumInteritemSpacing amélioration
  4. il y avait version fanpyi Swift
  5. maintenant voici une version simplifiée et propre de la mienne:
open class UICollectionViewLeftAlignedLayout: UICollectionViewFlowLayout {
    open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        return super.layoutAttributesForElements(in: rect)?.map { $0.representedElementKind == nil ? layoutAttributesForItem(at: $0.indexPath)! : $0 }
    }

    open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        guard let currentItemAttributes = super.layoutAttributesForItem(at: indexPath)?.copy() as? UICollectionViewLayoutAttributes,
            collectionView != nil else {
            // should never happen
            return nil
        }

        // if the current frame, once stretched to the full row intersects the previous frame then they are on the same row
        if indexPath.item != 0,
            let previousFrame = layoutAttributesForItem(at: IndexPath(item: indexPath.item - 1, section: indexPath.section))?.frame,
            currentItemAttributes.frame.intersects(CGRect(x: -.infinity, y: previousFrame.Origin.y, width: .infinity, height: previousFrame.size.height)) {
            // the next item on a line
            currentItemAttributes.frame.Origin.x = previousFrame.Origin.x + previousFrame.size.width + evaluatedMinimumInteritemSpacingForSection(at: indexPath.section)
        } else {
            // the first item on a line
            currentItemAttributes.frame.Origin.x = evaluatedSectionInsetForSection(at: indexPath.section).left
        }
        return currentItemAttributes
    }

    func evaluatedMinimumInteritemSpacingForSection(at section: NSInteger) -> CGFloat {
        return (collectionView?.delegate as? UICollectionViewDelegateFlowLayout)?.collectionView?(collectionView!, layout: self, minimumInteritemSpacingForSectionAt: section) ?? minimumInteritemSpacing
    }

    func evaluatedSectionInsetForSection(at index: NSInteger) -> UIEdgeInsets {
        return (collectionView?.delegate as? UICollectionViewDelegateFlowLayout)?.collectionView?(collectionView!, layout: self, insetForSectionAt: index) ?? sectionInset
    }
}

Utilisation: l'espacement entre les éléments est déterminé par la fonction collectionView (_:layout:minimumInteritemSpacingForSectionAt:) du délégué.

Je l'ai mis sur github, https://github.com/Coeur/UICollectionViewLeftAlignedLayout , où j'ai en fait ajouté une fonctionnalité de prise en charge des deux directions de défilement (horizontale et verticale).

1
Cœur

Une version plus propre de Swift pour les personnes intéressées, basée sur la réponse de Chris Wagner:

class AlignLeftFlowLayout: UICollectionViewFlowLayout {

    var maximumCellSpacing = CGFloat(9.0)

    override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
        let attributesToReturn = super.layoutAttributesForElementsInRect(rect) as? [UICollectionViewLayoutAttributes]

        for attributes in attributesToReturn ?? [] {
            if attributes.representedElementKind == nil {
                attributes.frame = self.layoutAttributesForItemAtIndexPath(attributes.indexPath).frame
            }
        }

        return attributesToReturn
    }

    override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
        let curAttributes = super.layoutAttributesForItemAtIndexPath(indexPath)
        let sectionInset = (self.collectionView?.collectionViewLayout as UICollectionViewFlowLayout).sectionInset

        if indexPath.item == 0 {
            let f = curAttributes.frame
            curAttributes.frame = CGRectMake(sectionInset.left, f.Origin.y, f.size.width, f.size.height)
            return curAttributes
        }

        let prevIndexPath = NSIndexPath(forItem: indexPath.item-1, inSection: indexPath.section)
        let prevFrame = self.layoutAttributesForItemAtIndexPath(prevIndexPath).frame
        let prevFrameRightPoint = prevFrame.Origin.x + prevFrame.size.width + maximumCellSpacing

        let curFrame = curAttributes.frame
        let stretchedCurFrame = CGRectMake(0, curFrame.Origin.y, self.collectionView!.frame.size.width, curFrame.size.height)

        if CGRectIntersectsRect(prevFrame, stretchedCurFrame) {
            curAttributes.frame = CGRectMake(prevFrameRightPoint, curFrame.Origin.y, curFrame.size.width, curFrame.size.height)
        } else {
            curAttributes.frame = CGRectMake(sectionInset.left, curFrame.Origin.y, curFrame.size.width, curFrame.size.height)
        }

        return curAttributes
    }
}
1
Kevin R

La voici pour NSCollectionViewFlowLayout

class LeftAlignedCollectionViewFlowLayout: NSCollectionViewFlowLayout {

    var maximumCellSpacing = CGFloat(2.0)

    override func layoutAttributesForElementsInRect(rect: NSRect) -> [NSCollectionViewLayoutAttributes] {

        let attributesToReturn = super.layoutAttributesForElementsInRect(rect)

        for attributes in attributesToReturn ?? [] {
            if attributes.representedElementKind == nil {
                attributes.frame = self.layoutAttributesForItemAtIndexPath(attributes.indexPath!)!.frame
            }
        }
        return attributesToReturn
    }

    override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> NSCollectionViewLayoutAttributes? {

        let curAttributes = super.layoutAttributesForItemAtIndexPath(indexPath)
        let sectionInset = (self.collectionView?.collectionViewLayout as! NSCollectionViewFlowLayout).sectionInset

        if indexPath.item == 0 {
            let f = curAttributes!.frame
            curAttributes!.frame = CGRectMake(sectionInset.left, f.Origin.y, f.size.width, f.size.height)
            return curAttributes
        }

        let prevIndexPath = NSIndexPath(forItem: indexPath.item-1, inSection: indexPath.section)
        let prevFrame = self.layoutAttributesForItemAtIndexPath(prevIndexPath)!.frame
        let prevFrameRightPoint = prevFrame.Origin.x + prevFrame.size.width + maximumCellSpacing

        let curFrame = curAttributes!.frame
        let stretchedCurFrame = CGRectMake(0, curFrame.Origin.y, self.collectionView!.frame.size.width, curFrame.size.height)

        if CGRectIntersectsRect(prevFrame, stretchedCurFrame) {
            curAttributes!.frame = CGRectMake(prevFrameRightPoint, curFrame.Origin.y, curFrame.size.width, curFrame.size.height)
        } else {
            curAttributes!.frame = CGRectMake(sectionInset.left, curFrame.Origin.y, curFrame.size.width, curFrame.size.height)
        }

        return curAttributes
    }
}
0
Chris