J'utilise UICollectionView dans mon projet, où il y a plusieurs cellules de différentes largeurs sur une ligne. Selon: https://developer.Apple.com/library/content/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/UsingtheFlowLayout/UsingtheFlowLayout.html
il répartit les cellules sur la ligne avec un remplissage égal. Cela se passe comme prévu, sauf que je veux justifier à gauche, et coder en dur une largeur de remplissage.
Je suppose que je dois sous-classer UICollectionViewFlowLayout. Cependant, après la lecture de certains tutoriels en ligne, etc., il me semble que je ne comprends pas comment cela fonctionne.
Les autres solutions présentées ici ne fonctionnent pas correctement lorsque la ligne est composée d’un seul élément ou est trop compliquée.
Sur la base de l'exemple donné par Ryan, j'ai modifié le code pour détecter une nouvelle ligne en inspectant la position Y du nouvel élément. Très simple et rapide en performance.
Rapide:
class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
var leftMargin = sectionInset.left
var maxY: CGFloat = -1.0
attributes?.forEach { layoutAttribute in
if layoutAttribute.frame.Origin.y >= maxY {
leftMargin = sectionInset.left
}
layoutAttribute.frame.Origin.x = leftMargin
leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
maxY = max(layoutAttribute.frame.maxY , maxY)
}
return attributes
}
}
Si vous souhaitez que les vues supplémentaires conservent leur taille, ajoutez ce qui suit en haut de la fermeture dans l'appel forEach
:
guard layoutAttribute.representedElementCategory == .cell else {
return
}
Objectif c:
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *attributes = [super layoutAttributesForElementsInRect:rect];
CGFloat leftMargin = self.sectionInset.left; //initalized to silence compiler, and actaully safer, but not planning to use.
CGFloat maxY = -1.0f;
//this loop assumes attributes are in IndexPath order
for (UICollectionViewLayoutAttributes *attribute in attributes) {
if (attribute.frame.Origin.y >= maxY) {
leftMargin = self.sectionInset.left;
}
attribute.frame = CGRectMake(leftMargin, attribute.frame.Origin.y, attribute.frame.size.width, attribute.frame.size.height);
leftMargin += attribute.frame.size.width + self.minimumInteritemSpacing;
maxY = MAX(CGRectGetMaxY(attribute.frame), maxY);
}
return attributes;
}
Les réponses à cette question contiennent de nombreuses bonnes idées. Cependant, la plupart d’entre eux présentent des inconvénients:
La plupart des solutions ne font que remplacer la fonction layoutAttributesForElements(in rect: CGRect)
. Ils ne remplacent pas layoutAttributesForItem(at indexPath: IndexPath)
. Il s'agit d'un problème car la vue de collection appelle périodiquement cette dernière fonction pour extraire les attributs de présentation d'un chemin d'index particulier. Si vous ne renvoyez pas les attributs appropriés à partir de cette fonction, vous rencontrerez probablement toutes sortes de bugs visuels, par exemple. lors de l'insertion et de la suppression d'animations de cellules ou lors de l'utilisation de cellules à taille automatique en définissant la structure de la vue de la collection dans estimatedItemSize
name__. Les documents Apple indiquent:
Chaque objet de présentation personnalisé doit implémenter la méthode
layoutAttributesForItemAtIndexPath:
.
De nombreuses solutions supposent également le paramètre rect
qui est transmis à la fonction layoutAttributesForElements(in rect: CGRect)
. Par exemple, beaucoup sont basés sur l'hypothèse que rect
commence toujours au début d'une nouvelle ligne, ce qui n'est pas nécessairement le cas.
Donc en d'autres termes:
Afin de résoudre ces problèmes, j'ai créé une sous-classe UICollectionViewFlowLayout
qui suit une idée similaire à celle suggérée par matt et Chris Wagner dans leurs réponses à une question similaire. Il peut soit aligner les cellules
⬅︎ à gauche :
ou ➡︎ right :
et offre en plus des options pour verticalement aligner les cellules dans leurs rangées respectives (si leur hauteur varie).
Vous pouvez simplement le télécharger ici:
L'utilisation est simple et expliquée dans le fichier README. En gros, vous créez une instance de AlignedCollectionViewFlowLayout
name__, spécifiez l'alignement souhaité et affectez-la à la propriété collectionViewLayout
de votre vue de collection:
let alignedFlowLayout = AlignedCollectionViewFlowLayout(horizontalAlignment: .left,
verticalAlignment: .top)
yourCollectionView.collectionViewLayout = alignedFlowLayout
(Il est également disponible sur Cocoapods .)
Le concept ici est de s'appuyer uniquement sur la fonction layoutAttributesForItem(at indexPath: IndexPath)
. Dans la layoutAttributesForElements(in rect: CGRect)
, nous obtenons simplement les chemins d'index de toutes les cellules dans la rect
name__, puis appelons la première fonction de chaque chemin d'index pour récupérer les trames correctes:
override public func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// We may not change the original layout attributes
// or UICollectionViewFlowLayout might complain.
let layoutAttributesObjects = copy(super.layoutAttributesForElements(in: rect))
layoutAttributesObjects?.forEach({ (layoutAttributes) in
if layoutAttributes.representedElementCategory == .cell { // Do not modify header views etc.
let indexPath = layoutAttributes.indexPath
// Retrieve the correct frame from layoutAttributesForItem(at: indexPath):
if let newFrame = layoutAttributesForItem(at: indexPath)?.frame {
layoutAttributes.frame = newFrame
}
}
})
return layoutAttributesObjects
}
(La fonction copy()
crée simplement une copie détaillée de tous les attributs de présentation du tableau. Vous pouvez consulter le code source pour connaître son implémentation.)
Il ne reste donc maintenant qu’à implémenter correctement la fonction layoutAttributesForItem(at indexPath: IndexPath)
. La super classe UICollectionViewFlowLayout
place déjà le nombre correct de cellules dans chaque ligne, il ne reste donc plus qu'à les déplacer à gauche dans leur ligne respective. La difficulté réside dans le calcul de la quantité d’espace nécessaire pour décaler chaque cellule de gauche.
Comme nous voulons avoir un espacement fixe entre les cellules, l’idée principale est de supposer que la cellule précédente (la cellule de gauche de la cellule actuellement agencée) est déjà correctement positionnée. Ensuite, il suffit d'ajouter l'espacement des cellules à la valeur maxX
du cadre de la cellule précédente. Il s'agit de la valeur Origin.x
du cadre de la cellule actuelle.
Maintenant, nous avons seulement besoin de savoir quand nous avons atteint le début d'une ligne pour ne pas aligner une cellule à côté d'une cellule dans la ligne précédente. (Cela entraînerait non seulement une mise en page incorrecte, mais aussi une latence extrême.) Nous avons donc besoin d'une ancre de récursivité. L'approche que j'utilise pour trouver cette ancre de récursion est la suivante:
+---------+----------------------------------------------------------------+---------+
| | | |
| | +------------+ | |
| | | | | |
| section |- - -|- - - - - - |- - - - +---------------------+ - - - - - - -| section |
| inset | |intersection| | | line rect | inset |
| |- - -|- - - - - - |- - - - +---------------------+ - - - - - - -| |
| (left) | | | current item | (right) |
| | +------------+ | |
| | previous item | |
+---------+----------------------------------------------------------------+---------+
... Je "dessine" un rectangle autour de la cellule actuelle et l'étire sur la largeur de la vue de la collection entière. Comme UICollectionViewFlowLayout
centre toutes les cellules verticalement, chaque cellule de la même ligne doit se croiser avec ce rectangle.
Ainsi, je vérifie simplement si la cellule avec index i-1 intersecte avec ce rectangle linéaire créé à partir de la cellule avec index i .
Si elle se croise, la cellule d'indice i n'est pas la cellule la plus à gauche de la ligne.
→ Obtenir le cadre de la cellule précédente (avec l'index i − 1 ) et déplacer la cellule actuelle à côté.
Si elle ne se croise pas, la cellule d'indice i est la cellule la plus à gauche de la ligne.
→ Déplacez la cellule vers le bord gauche de la vue de collection (sans changer sa position verticale).
Je ne posterai pas ici l'implémentation réelle de la fonction layoutAttributesForItem(at indexPath: IndexPath)
car je pense que le plus important est de comprendre l'idée et vous pouvez toujours vérifier mon implémentation dans le code source . (C’est un peu plus compliqué que ce qui est expliqué ici, car j’autorise également l’alignement .right
et diverses options d’alignement vertical. Toutefois, il suit la même idée.)
Wow, je suppose que c'est la réponse la plus longue que j'ai jamais écrite sur Stackoverflow. J'espère que ça aide. ????
La question a été posée il y a longtemps, mais il n'y a pas de réponse et c'est une bonne question. La solution consiste à remplacer une méthode de la sous-classe UICollectionViewFlowLayout:
@implementation MYFlowLayoutSubclass
//Note, the layout's minimumInteritemSpacing (default 10.0) should not be less than this.
#define ITEM_SPACING 10.0f
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *attributesForElementsInRect = [super layoutAttributesForElementsInRect:rect];
NSMutableArray *newAttributesForElementsInRect = [[NSMutableArray alloc] initWithCapacity:attributesForElementsInRect.count];
CGFloat leftMargin = self.sectionInset.left; //initalized to silence compiler, and actaully safer, but not planning to use.
//this loop assumes attributes are in IndexPath order
for (UICollectionViewLayoutAttributes *attributes in attributesForElementsInRect) {
if (attributes.frame.Origin.x == self.sectionInset.left) {
leftMargin = self.sectionInset.left; //will add outside loop
} else {
CGRect newLeftAlignedFrame = attributes.frame;
newLeftAlignedFrame.Origin.x = leftMargin;
attributes.frame = newLeftAlignedFrame;
}
leftMargin += attributes.frame.size.width + ITEM_SPACING;
[newAttributesForElementsInRect addObject:attributes];
}
return newAttributesForElementsInRect;
}
@end
Conformément aux recommandations de Apple, vous obtenez les attributs de présentation de super et les parcourez par-dessus. Si c'est le premier de la ligne (défini par son Origin.x situé dans la marge de gauche), vous le laissez seul et réinitialisez le x à zéro. Ensuite, pour la première cellule et chaque cellule, vous ajoutez la largeur de cette cellule plus une marge. Ceci est passé à l'élément suivant de la boucle. S'il ne s'agit pas du premier élément, définissez Origin.x sur la marge calculée en cours d'exécution et ajoutez de nouveaux éléments au tableau.
J'ai eu le même problème, Donnez le Cocoapod UICollectionViewLeftAlignedLayout a essayer. Il suffit de l'inclure dans votre projet et de l'initialiser comme ceci:
UICollectionViewLeftAlignedLayout *layout = [[UICollectionViewLeftAlignedLayout alloc] init];
UICollectionView *leftAlignedCollectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:layout];
En me basant sur La réponse de Michael Sand , j'ai créé une bibliothèque UICollectionViewFlowLayout
sous-classée pour effectuer une justification horizontale gauche, droite ou complète (par défaut); elle vous permet également de définir la distance absolue entre chaque cellule. Je prévois d'y ajouter également une justification centrale horizontale et une justification verticale.
En rapide. Selon Michaels répondre
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let oldAttributes = super.layoutAttributesForElementsInRect(rect) else {
return super.layoutAttributesForElementsInRect(rect)
}
let spacing = CGFloat(50) // REPLACE WITH WHAT SPACING YOU NEED
var newAttributes = [UICollectionViewLayoutAttributes]()
var leftMargin = self.sectionInset.left
for attributes in oldAttributes {
if (attributes.frame.Origin.x == self.sectionInset.left) {
leftMargin = self.sectionInset.left
} else {
var newLeftAlignedFrame = attributes.frame
newLeftAlignedFrame.Origin.x = leftMargin
attributes.frame = newLeftAlignedFrame
}
leftMargin += attributes.frame.width + spacing
newAttributes.append(attributes)
}
return newAttributes
}
Voici la réponse originale dans Swift. Cela fonctionne toujours très bien la plupart du temps.
class LeftAlignedFlowLayout: UICollectionViewFlowLayout {
private override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElementsInRect(rect)
var leftMargin = sectionInset.left
attributes?.forEach { layoutAttribute in
if layoutAttribute.frame.Origin.x == sectionInset.left {
leftMargin = sectionInset.left
}
else {
layoutAttribute.frame.Origin.x = leftMargin
}
leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
}
return attributes
}
}
Il y a une grande exception malheureusement. Si vous utilisez UICollectionViewFlowLayout
's estimatedItemSize
. En interne, UICollectionViewFlowLayout
change un peu les choses. Je ne l'ai pas encore entièrement repéré, mais il est clair que d'autres méthodes sont appelées après layoutAttributesForElementsInRect
tout en dimensionnant les cellules. D'après mes essais, j'ai trouvé qu'il semble appeler layoutAttributesForItemAtIndexPath
pour chaque cellule individuellement pendant l'autosizing plus souvent. Cette mise à jour LeftAlignedFlowLayout
fonctionne très bien avec estimatedItemSize
. Cela fonctionne également avec les cellules de taille statique, mais les appels de mise en page supplémentaires me conduisent à utiliser la réponse originale chaque fois que je n'ai pas besoin de cellules à taille automatique.
class LeftAlignedFlowLayout: UICollectionViewFlowLayout {
private override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
let layoutAttribute = super.layoutAttributesForItemAtIndexPath(indexPath)?.copy() as? UICollectionViewLayoutAttributes
// First in a row.
if layoutAttribute?.frame.Origin.x == sectionInset.left {
return layoutAttribute
}
// We need to align it to the previous item.
let previousIndexPath = NSIndexPath(forItem: indexPath.item - 1, inSection: indexPath.section)
guard let previousLayoutAttribute = self.layoutAttributesForItemAtIndexPath(previousIndexPath) else {
return layoutAttribute
}
layoutAttribute?.frame.Origin.x = previousLayoutAttribute.frame.maxX + self.minimumInteritemSpacing
return layoutAttribute
}
}
Sur la base de toutes les réponses, je change un peu et cela fonctionne bien pour moi
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
var leftMargin = sectionInset.left
var maxY: CGFloat = -1.0
attributes?.forEach { layoutAttribute in
if layoutAttribute.frame.Origin.y >= maxY
|| layoutAttribute.frame.Origin.x == sectionInset.left {
leftMargin = sectionInset.left
}
if layoutAttribute.frame.Origin.x == sectionInset.left {
leftMargin = sectionInset.left
}
else {
layoutAttribute.frame.Origin.x = leftMargin
}
leftMargin += layoutAttribute.frame.width
maxY = max(layoutAttribute.frame.maxY, maxY)
}
return attributes
}
Merci pour le réponse de Michael Sand . Je l'ai modifié pour une solution de plusieurs lignes (le même alignement en haut de chaque ligne) qui correspond à l'alignement de gauche, même espacement pour chaque élément.
static CGFloat const ITEM_SPACING = 10.0f;
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
CGRect contentRect = {CGPointZero, self.collectionViewContentSize};
NSArray *attributesForElementsInRect = [super layoutAttributesForElementsInRect:contentRect];
NSMutableArray *newAttributesForElementsInRect = [[NSMutableArray alloc] initWithCapacity:attributesForElementsInRect.count];
CGFloat leftMargin = self.sectionInset.left; //initalized to silence compiler, and actaully safer, but not planning to use.
NSMutableDictionary *leftMarginDictionary = [[NSMutableDictionary alloc] init];
for (UICollectionViewLayoutAttributes *attributes in attributesForElementsInRect) {
UICollectionViewLayoutAttributes *attr = attributes.copy;
CGFloat lastLeftMargin = [[leftMarginDictionary valueForKey:[[NSNumber numberWithFloat:attributes.frame.Origin.y] stringValue]] floatValue];
if (lastLeftMargin == 0) lastLeftMargin = leftMargin;
CGRect newLeftAlignedFrame = attr.frame;
newLeftAlignedFrame.Origin.x = lastLeftMargin;
attr.frame = newLeftAlignedFrame;
lastLeftMargin += attr.frame.size.width + ITEM_SPACING;
[leftMarginDictionary setObject:@(lastLeftMargin) forKey:[[NSNumber numberWithFloat:attributes.frame.Origin.y] stringValue]];
[newAttributesForElementsInRect addObject:attr];
}
return newAttributesForElementsInRect;
}
Édité la réponse d'Angel García Olloqui à respecter minimumInteritemSpacing
de la fonction collectionView(_:layout:minimumInteritemSpacingForSectionAt:)
du délégué, si celle-ci est implémentée.
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
var leftMargin = sectionInset.left
var maxY: CGFloat = -1.0
attributes?.forEach { layoutAttribute in
if layoutAttribute.frame.Origin.y >= maxY {
leftMargin = sectionInset.left
}
layoutAttribute.frame.Origin.x = leftMargin
let delegate = collectionView?.delegate as? UICollectionViewDelegateFlowLayout
let spacing = delegate?.collectionView?(collectionView!, layout: self, minimumInteritemSpacingForSectionAt: 0) ?? minimumInteritemSpacing
leftMargin += layoutAttribute.frame.width + spacing
maxY = max(layoutAttribute.frame.maxY , maxY)
}
return attributes
}
Le problème avec UICollectionView est qu’il essaie d’ajuster automatiquement les cellules dans la zone disponible ..___ Je l’ai fait en définissant d’abord le nombre de lignes et de colonnes, puis la taille de cellule pour cette ligne et cette colonne
1) Pour définir les sections (lignes) de mon UICollectionView:
(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
2) Définir le nombre d'éléments dans une section. Vous pouvez définir un nombre différent d'éléments pour chaque section. vous pouvez obtenir le numéro de section en utilisant le paramètre 'section'.
(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
3) Définir la taille de cellule pour chaque section et chaque rangée séparément. Vous pouvez obtenir le numéro de section et le numéro de ligne en utilisant le paramètre 'indexPath', par exemple,[indexPath section]
pour le numéro de section et[indexPath row]
pour le numéro de ligne.
(CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
4) Ensuite, vous pouvez afficher vos cellules en lignes et en sections en utilisant:
(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
REMARQUE: Dans UICollectionView
Section == Row
IndexPath.Row == Column
La réponse de Mike Sand est bonne mais j'avais rencontré quelques problèmes avec ce code (comme une longue cellule coupée). Et nouveau code:
#define ITEM_SPACE 7.0f
@implementation LeftAlignedCollectionViewFlowLayout
- (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 + ITEM_SPACE;
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;
}
Le code ci-dessus fonctionne pour moi. Je voudrais partager le code respectif Swift 3.0.
class SFFlowLayout: UICollectionViewFlowLayout {
let itemSpacing: CGFloat = 3.0
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attriuteElementsInRect = super.layoutAttributesForElements(in: rect)
var newAttributeForElement: Array<UICollectionViewLayoutAttributes> = []
var leftMargin = self.sectionInset.left
for tempAttribute in attriuteElementsInRect! {
let attribute = tempAttribute
if attribute.frame.Origin.x == self.sectionInset.left {
leftMargin = self.sectionInset.left
}
else {
var newLeftAlignedFrame = attribute.frame
newLeftAlignedFrame.Origin.x = leftMargin
attribute.frame = newLeftAlignedFrame
}
leftMargin += attribute.frame.size.width + itemSpacing
newAttributeForElement.append(attribute)
}
return newAttributeForElement
}
}
Sur la base des réponses fournies ici, mais des plantages corrigés et des problèmes d’alignement lorsque votre vue de collection contient également des en-têtes ou des pieds de page. Aligner les cellules restantes:
class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
var leftMargin = sectionInset.left
var prevMaxY: CGFloat = -1.0
attributes?.forEach { layoutAttribute in
guard layoutAttribute.representedElementCategory == .cell else {
return
}
if layoutAttribute.frame.Origin.y >= prevMaxY {
leftMargin = sectionInset.left
}
layoutAttribute.frame.Origin.x = leftMargin
leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
prevMaxY = layoutAttribute.frame.maxY
}
return attributes
}
}