Allinea le celle a sinistra in UICollectionView


96

Sto usando un UICollectionView nel mio progetto, dove ci sono più celle di diverse larghezze su una linea. Secondo: https://developer.apple.com/library/content/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/UsingtheFlowLayout/UsingtheFlowLayout.html

diffonde le celle lungo la linea con uguale imbottitura. Questo accade come previsto, tranne che voglio giustificarli a sinistra e codificare una larghezza di riempimento.

Immagino di dover sottoclassare UICollectionViewFlowLayout, tuttavia dopo aver letto alcuni dei tutorial, ecc. In linea, non mi sembra di capire come funzioni.


Risposte:


169

Le altre soluzioni in questo thread non funzionano correttamente, quando la riga è composta da un solo elemento o sono troppo complicate.

Sulla base dell'esempio fornito da Ryan, ho cambiato il codice per rilevare una nuova riga controllando la posizione Y del nuovo elemento. Molto semplice e veloce nelle prestazioni.

Swift:

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
    }
}

Se desideri che le visualizzazioni supplementari mantengano le loro dimensioni, aggiungi quanto segue all'inizio della chiusura nella forEachchiamata:

guard layoutAttribute.representedElementCategory == .cell else {
    return
}

Obiettivo-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;
}

4
Grazie! Stavo ricevendo un avviso, che viene corretto facendo una copia degli attributi nell'array let attributes = super.layoutAttributesForElementsInRect(rect)?.map { $0.copy() as! UICollectionViewLayoutAttributes }
tkuichooseyou

2
Per chiunque si sia imbattuto in questo alla ricerca di una versione allineata al centro come ho fatto io, ho pubblicato una versione modificata della risposta di Angel qui: stackoverflow.com/a/38254368/2845237
Alex Koshy

Ho provato questa soluzione ma alcune delle celle che si trovano a destra della vista della raccolta superano i limiti della vista della raccolta. Qualcun altro l'ha riscontrato?
Happiehappie

@Happiehappie sì, anch'io ho quel comportamento. qualche correzione?
John Baum

Ho provato questa soluzione ma non ho potuto associare il layout a collectionView nel pannello Interface Builder Utilies. Ho finito per farlo tramite codice. Qualche indizio sul perché?
acrespo

63

Ci sono molte grandi idee incluse nelle risposte a questa domanda. Tuttavia, la maggior parte di loro presenta alcuni inconvenienti:

  • Le soluzioni che non controllano il valore y della cella funzionano solo per i layout a riga singola . Non riescono per i layout di visualizzazione della raccolta con più righe.
  • Soluzioni che fanno controllare la y valore come la risposta di Angel Garcia Olloqui solo lavoro se tutte le cellule hanno la stessa altezza . Non riescono per le celle con un'altezza variabile.
  • La maggior parte delle soluzioni sostituisce solo la layoutAttributesForElements(in rect: CGRect)funzione. Non hanno la precedenza layoutAttributesForItem(at indexPath: IndexPath). Questo è un problema perché la vista della raccolta chiama periodicamente quest'ultima funzione per recuperare gli attributi di layout per un particolare percorso di indice. Se non si restituiscono gli attributi corretti da quella funzione, è probabile che si verifichino tutti i tipi di bug visivi, ad esempio durante l'inserimento e l'eliminazione delle animazioni delle celle o quando si utilizzano celle a ridimensionamento automatico impostando il layout della vista raccolta estimatedItemSize. I documenti Apple affermano:

    Ogni oggetto di layout personalizzato dovrebbe implementare il layoutAttributesForItemAtIndexPath:metodo.

  • Molte soluzioni fanno anche ipotesi sul rectparametro passato alla layoutAttributesForElements(in rect: CGRect)funzione. Ad esempio, molti si basano sul presupposto che rectinizi sempre all'inizio di una nuova riga, il che non è necessariamente il caso.

Quindi in altre parole:

La maggior parte delle soluzioni suggerite in questa pagina funzionano per alcune applicazioni specifiche, ma non funzionano come previsto in ogni situazione.


AlignedCollectionViewFlowLayout

Per affrontare questi problemi ho creato una UICollectionViewFlowLayoutsottoclasse che segue un'idea simile suggerita da matt e Chris Wagner nelle loro risposte a una domanda simile. Può allineare le celle

⬅︎ a sinistra :

Layout allineato a sinistra

o ➡︎ destra :

Layout allineato a destra

e offre inoltre opzioni per allineare verticalmente le celle nelle rispettive righe (nel caso in cui variano in altezza).

Puoi semplicemente scaricarlo qui:

https://github.com/mischa-hildebrand/AlignedCollectionViewFlowLayout

L'utilizzo è semplice e spiegato nel file README. Fondamentalmente crei un'istanza di AlignedCollectionViewFlowLayout, specifichi l'allineamento desiderato e assegnalo alla collectionViewLayoutproprietà della vista della raccolta :

 let alignedFlowLayout = AlignedCollectionViewFlowLayout(horizontalAlignment: .left, 
                                                         verticalAlignment: .top)

 yourCollectionView.collectionViewLayout = alignedFlowLayout

(È disponibile anche su Cocoapods .)


Come funziona (per celle allineate a sinistra):

Il concetto qui è di fare affidamento esclusivamente sulla layoutAttributesForItem(at indexPath: IndexPath)funzione . In layoutAttributesForElements(in rect: CGRect)otteniamo semplicemente i percorsi di indice di tutte le celle all'interno di recte quindi chiamiamo la prima funzione per ogni percorso di indice per recuperare i frame corretti:

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 copy()funzione crea semplicemente una copia completa di tutti gli attributi di layout nell'array. Puoi esaminare il codice sorgente per la sua implementazione.)

Quindi ora l'unica cosa che dobbiamo fare è implementare layoutAttributesForItem(at indexPath: IndexPath)correttamente la funzione. La super classe UICollectionViewFlowLayoutinserisce già il numero corretto di celle in ogni riga, quindi dobbiamo solo spostarle a sinistra all'interno della rispettiva riga. La difficoltà sta nel calcolare la quantità di spazio di cui abbiamo bisogno per spostare ogni cella a sinistra.

Dato che vogliamo avere una spaziatura fissa tra le celle, l'idea centrale è semplicemente presumere che la cella precedente (la cella a sinistra della cella che è attualmente disposta) sia già posizionata correttamente. Quindi dobbiamo solo aggiungere la spaziatura delle celle al maxXvalore del frame della cella precedente e questo è il origin.xvalore per il frame della cella corrente.

Ora abbiamo solo bisogno di sapere quando abbiamo raggiunto l'inizio di una riga, in modo da non allineare una cella accanto a una cella nella riga precedente. (Questo non solo risulterebbe in un layout errato, ma sarebbe anche estremamente lento). Quindi dobbiamo avere un'ancora di ricorsione. L'approccio che utilizzo per trovare quell'ancora di ricorsione è il seguente:

Per scoprire se la cella con indice i è sulla stessa riga della cella con indice i-1 ...

 +---------+----------------------------------------------------------------+---------+
 |         |                                                                |         |
 |         |     +------------+                                             |         |
 |         |     |            |                                             |         |
 | section |- - -|- - - - - - |- - - - +---------------------+ - - - - - - -| section |
 |  inset  |     |intersection|        |                     |   line rect  |  inset  |
 |         |- - -|- - - - - - |- - - - +---------------------+ - - - - - - -|         |
 | (left)  |     |            |             current item                    | (right) |
 |         |     +------------+                                             |         |
 |         |     previous item                                              |         |
 +---------+----------------------------------------------------------------+---------+

... "disegno" un rettangolo attorno alla cella corrente e lo allungo sulla larghezza dell'intera vista della raccolta. Poiché i UICollectionViewFlowLayoutcentri tutte le celle verticalmente, ogni cella della stessa linea deve intersecarsi con questo rettangolo.

Quindi, controllo semplicemente se la cella con indice i-1 si interseca con questo rettangolo di linea creato dalla cella con indice i .

  • Se si interseca, la cella con indice i non è la cella più a sinistra della riga.
    → Ottieni il frame della cella precedente (con indice i − 1 ) e sposta la cella corrente accanto ad esso.

  • Se non si interseca, la cella con indice i è la cella più a sinistra della riga.
    → Sposta la cella sul bordo sinistro della vista della raccolta (senza modificarne la posizione verticale).

Non posterò l'effettiva implementazione della layoutAttributesForItem(at indexPath: IndexPath)funzione qui perché penso che la parte più importante sia capire l' idea e puoi sempre controllare la mia implementazione nel codice sorgente . (È un po 'più complicato di quanto spiegato qui perché consento anche l' .rightallineamento e varie opzioni di allineamento verticale. Tuttavia, segue la stessa idea.)


Wow, immagino che questa sia la risposta più lunga che abbia mai scritto su Stackoverflow. Spero che aiuti. 😉


Applico con questo scenario stackoverflow.com/questions/56349052/… ma non funziona. mi diresti come posso raggiungere questo obiettivo
Nazmul Hasan

Anche con questo (che è un progetto davvero fantastico) ho esperienza di lampeggiare nella vista della raccolta a scorrimento verticale delle mie app. Ho 20 celle e al primo scorrimento, la prima cella è vuota e tutte le celle vengono spostate di una posizione a destra. Dopo uno scorrimento completo su e giù, le cose si allineano correttamente.
Parth Tamane

Fantastico progetto! Sarebbe bello vederlo abilitato per Cartagine.
pjuzeliunas

30

Con Swift 4.1 e iOS 11, in base alle tue esigenze, puoi scegliere una delle 2 seguenti implementazioni complete per risolvere il tuo problema.


# 1. A sinistra il ridimensionamento automatico allineamento UICollectionViewCells

L'implementazione seguente mostra come usare UICollectionViewLayout's layoutAttributesForElements(in:), UICollectionViewFlowLayout' s estimatedItemSizee UILabel'spreferredMaxLayoutWidth al fine di allineare il ridimensionamento automatico cellule sinistra in UICollectionView:

CollectionViewController.swift

import UIKit

class CollectionViewController: UICollectionViewController {

    let array = ["1", "1 2", "1 2 3 4 5 6 7 8", "1 2 3 4 5 6 7 8 9 10 11", "1 2 3", "1 2 3 4", "1 2 3 4 5 6", "1 2 3 4 5 6 7 8 9 10", "1 2 3 4", "1 2 3 4 5 6 7", "1 2 3 4 5 6 7 8 9", "1", "1 2 3 4 5", "1", "1 2 3 4 5 6"]

    let columnLayout = FlowLayout(
        minimumInteritemSpacing: 10,
        minimumLineSpacing: 10,
        sectionInset: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
    )

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView?.collectionViewLayout = columnLayout
        collectionView?.contentInsetAdjustmentBehavior = .always
        collectionView?.register(CollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
    }

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

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell
        cell.label.text = array[indexPath.row]
        return cell
    }

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        collectionView?.collectionViewLayout.invalidateLayout()
        super.viewWillTransition(to: size, with: coordinator)
    }

}

FlowLayout.swift

import UIKit

class FlowLayout: UICollectionViewFlowLayout {

    required init(minimumInteritemSpacing: CGFloat = 0, minimumLineSpacing: CGFloat = 0, sectionInset: UIEdgeInsets = .zero) {
        super.init()

        estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
        self.minimumInteritemSpacing = minimumInteritemSpacing
        self.minimumLineSpacing = minimumLineSpacing
        self.sectionInset = sectionInset
        sectionInsetReference = .fromSafeArea
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let layoutAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
        guard scrollDirection == .vertical else { return layoutAttributes }

        // Filter attributes to compute only cell attributes
        let cellAttributes = layoutAttributes.filter({ $0.representedElementCategory == .cell })

        // Group cell attributes by row (cells with same vertical center) and loop on those groups
        for (_, attributes) in Dictionary(grouping: cellAttributes, by: { ($0.center.y / 10).rounded(.up) * 10 }) {
            // Set the initial left inset
            var leftInset = sectionInset.left

            // Loop on cells to adjust each cell's origin and prepare leftInset for the next cell
            for attribute in attributes {
                attribute.frame.origin.x = leftInset
                leftInset = attribute.frame.maxX + minimumInteritemSpacing
            }
        }

        return layoutAttributes
    }

}

CollectionViewCell.swift

import UIKit

class CollectionViewCell: UICollectionViewCell {

    let label = UILabel()

    override init(frame: CGRect) {
        super.init(frame: frame)

        contentView.backgroundColor = .orange
        label.preferredMaxLayoutWidth = 120
        label.numberOfLines = 0

        contentView.addSubview(label)
        label.translatesAutoresizingMaskIntoConstraints = false
        contentView.layoutMarginsGuide.topAnchor.constraint(equalTo: label.topAnchor).isActive = true
        contentView.layoutMarginsGuide.leadingAnchor.constraint(equalTo: label.leadingAnchor).isActive = true
        contentView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: label.trailingAnchor).isActive = true
        contentView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: label.bottomAnchor).isActive = true
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

Risultato atteso:

inserisci qui la descrizione dell'immagine


# 2. Allinea a sinistra le UICollectionViewCells con dimensioni fisse

L'implementazione di seguito mostra come utilizzare UICollectionViewLayout" layoutAttributesForElements(in:)e UICollectionViewFlowLayout"itemSize per cellule allineare a sinistra con dimensione predefinita in UICollectionView:

CollectionViewController.swift

import UIKit

class CollectionViewController: UICollectionViewController {

    let columnLayout = FlowLayout(
        itemSize: CGSize(width: 140, height: 140),
        minimumInteritemSpacing: 10,
        minimumLineSpacing: 10,
        sectionInset: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
    )

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView?.collectionViewLayout = columnLayout
        collectionView?.contentInsetAdjustmentBehavior = .always
        collectionView?.register(CollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
    }

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

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell
        return cell
    }

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        collectionView?.collectionViewLayout.invalidateLayout()
        super.viewWillTransition(to: size, with: coordinator)
    }

}

FlowLayout.swift

import UIKit

class FlowLayout: UICollectionViewFlowLayout {

    required init(itemSize: CGSize, minimumInteritemSpacing: CGFloat = 0, minimumLineSpacing: CGFloat = 0, sectionInset: UIEdgeInsets = .zero) {
        super.init()

        self.itemSize = itemSize
        self.minimumInteritemSpacing = minimumInteritemSpacing
        self.minimumLineSpacing = minimumLineSpacing
        self.sectionInset = sectionInset
        sectionInsetReference = .fromSafeArea
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let layoutAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
        guard scrollDirection == .vertical else { return layoutAttributes }

        // Filter attributes to compute only cell attributes
        let cellAttributes = layoutAttributes.filter({ $0.representedElementCategory == .cell })

        // Group cell attributes by row (cells with same vertical center) and loop on those groups
        for (_, attributes) in Dictionary(grouping: cellAttributes, by: { ($0.center.y / 10).rounded(.up) * 10 }) {
            // Set the initial left inset
            var leftInset = sectionInset.left

            // Loop on cells to adjust each cell's origin and prepare leftInset for the next cell
            for attribute in attributes {
                attribute.frame.origin.x = leftInset
                leftInset = attribute.frame.maxX + minimumInteritemSpacing
            }
        }

        return layoutAttributes
    }

}

CollectionViewCell.swift

import UIKit

class CollectionViewCell: UICollectionViewCell {

    override init(frame: CGRect) {
        super.init(frame: frame)

        contentView.backgroundColor = .cyan
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

Risultato atteso:

inserisci qui la descrizione dell'immagine


Quando si raggruppano gli attributi delle celle per riga, si utilizza 10un numero arbitrario?
amariduran

25

La soluzione semplice nel 2019

Questa è una di quelle domande deprimenti in cui le cose sono cambiate molto nel corso degli anni. Adesso è facile.

Fondamentalmente fai solo questo:

    // as you move across one row ...
    a.frame.origin.x = x
    x += a.frame.width + minimumInteritemSpacing
    // and, obviously start fresh again each row

Tutto ciò di cui hai bisogno ora è il codice boilerplate:

override func layoutAttributesForElements(
                  in rect: CGRect)->[UICollectionViewLayoutAttributes]? {
    
    guard let att = super.layoutAttributesForElements(in: rect) else { return [] }
    var x: CGFloat = sectionInset.left
    var y: CGFloat = -1.0
    
    for a in att {
        if a.representedElementCategory != .cell { continue }
        
        if a.frame.origin.y >= y { x = sectionInset.left }
        
        a.frame.origin.x = x
        x += a.frame.width + minimumInteritemSpacing
        
        y = a.frame.maxY
    }
    return att
}

Basta copiarlo e incollarlo in un file UICollectionViewFlowLayout : il gioco è fatto.

Soluzione funzionante completa per copiare e incollare:

Questa è l'intera cosa:

class TagsLayout: UICollectionViewFlowLayout {
    
    required override init() {super.init(); common()}
    required init?(coder aDecoder: NSCoder) {super.init(coder: aDecoder); common()}
    
    private func common() {
        estimatedItemSize = UICollectionViewFlowLayout.automaticSize
        minimumLineSpacing = 10
        minimumInteritemSpacing = 10
    }
    
    override func layoutAttributesForElements(
                    in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        
        guard let att = super.layoutAttributesForElements(in:rect) else {return []}
        var x: CGFloat = sectionInset.left
        var y: CGFloat = -1.0
        
        for a in att {
            if a.representedElementCategory != .cell { continue }
            
            if a.frame.origin.y >= y { x = sectionInset.left }
            a.frame.origin.x = x
            x += a.frame.width + minimumInteritemSpacing
            y = a.frame.maxY
        }
        return att
    }
}

inserisci qui la descrizione dell'immagine

E infine...

Ringrazia @AlexShubin sopra che per primo ha chiarito questo!


Hey. Sono un po 'confuso. L'aggiunta di spazio per ogni stringa all'inizio e alla fine mostra ancora la mia parola nella cella dell'elemento senza lo spazio che ho aggiunto. Qualche idea su come farlo?
Mohamed Lee

Se hai intestazioni di sezione nella visualizzazione della raccolta, questo sembra ucciderle.
TylerJames,

22

La domanda è in sospeso da un po 'ma non c'è risposta ed è una buona domanda. La risposta è sovrascrivere un metodo nella sottoclasse 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

Come consigliato da Apple, ottieni gli attributi del layout da super e li ripeti. Se è il primo della riga (definito dal fatto che origin.x si trova sul margine sinistro), lo lasci da solo e reimposti la x a zero. Quindi per la prima cella e ogni cella, aggiungi la larghezza di quella cella più un margine. Questo viene passato all'elemento successivo nel ciclo. Se non è il primo elemento, imposti il ​​suo origin.x sul margine calcolato in esecuzione e aggiungi nuovi elementi all'array.


Penso che questo sia molto meglio della soluzione di Chris Wagner ( stackoverflow.com/a/15554667/482557 ) sulla domanda collegata: la tua implementazione non ha chiamate UICollectionViewLayout ripetitive.
Evan R

1
Non sembra che funzioni se la riga ha un singolo elemento perché UICollectionViewFlowLayout lo centra. Puoi controllare il centro per risolvere il problema. Qualcosa comeattribute.center.x == self.collectionView?.bounds.midX
Ryan Poolos

Ho implementato questo, ma per qualche motivo si interrompe per la prima sezione: la prima cella appare a sinistra della prima riga e la seconda cella (che non si adatta alla stessa riga) va alla riga successiva, ma non a sinistra allineato. Invece, la sua posizione x inizia dove finisce la prima cella.
Nicolas Miari

@NicolasMiari Il ciclo decide che si tratta di una nuova riga e azzera la leftMargincon il if (attributes.frame.origin.x == self.sectionInset.left) {segno di spunta. Ciò presuppone che il layout predefinito abbia allineato la cella della nuova riga per essere allineata con il file sectionInset.left. Se questo non è il caso, risulterebbe il comportamento che descrivi. Vorrei inserire un punto di interruzione all'interno del ciclo e controllare entrambi i lati per la cella della seconda riga appena prima del confronto. Buona fortuna.
Mike Sand

3
Grazie ... ho finito per usare UICollectionViewLeftAlignedLayout: github.com/mokagio/UICollectionViewLeftAlignedLayout . Ignora un altro paio di metodi.
Nicolas Miari

9

Ho avuto lo stesso problema, prova il Cocoapod UICollectionViewLeftAlignedLayout . Includilo nel tuo progetto e inizializzalo in questo modo:

UICollectionViewLeftAlignedLayout *layout = [[UICollectionViewLeftAlignedLayout alloc] init];
UICollectionView *leftAlignedCollectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:layout];

Manca una parentesi quadra di apertura nella tua inizializzazione del layout lì
RyanOfCourse

Ho testato le due risposte github.com/eroth/ERJustifiedFlowLayout e UICollectionViewLeftAlignedLayout. Solo il secondo ha prodotto il risultato corretto quando si utilizza stimatoItemSize (auto-dimensionamento).
EckhardN

5

Basandomi sulla risposta di Michael Sand , ho creato una UICollectionViewFlowLayoutlibreria sottoclasse per eseguire la giustificazione orizzontale a sinistra, a destra o completa (in pratica l'impostazione predefinita): consente anche di impostare la distanza assoluta tra ogni cella. Ho intenzione di aggiungere anche la giustificazione centrale orizzontale e verticale.

https://github.com/eroth/ERJustifiedFlowLayout


5

Ecco la risposta originale in Swift. Per lo più funziona ancora alla grande.

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
    }
}

Eccezione: ridimensionamento automatico delle celle

Purtroppo c'è una grande eccezione. Se stai usando UICollectionViewFlowLayoutil file estimatedItemSize. Internamente UICollectionViewFlowLayoutsta cambiando un po 'le cose. Non l'ho rintracciato del tutto, ma è chiaro che chiama altri metodi dopo aver layoutAttributesForElementsInRectauto ridimensionato le celle. Dalle mie prove ed errori ho scoperto che sembra chiamare layoutAttributesForItemAtIndexPathogni cella individualmente durante il dimensionamento automatico più spesso. Questo aggiornamento LeftAlignedFlowLayoutfunziona alla grande con estimatedItemSize. Funziona anche con celle di dimensioni statiche, tuttavia le chiamate di layout extra mi portano a utilizzare la risposta originale ogni volta che non ho bisogno di celle di dimensionamento automatico.

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
    }
}

Funziona per la prima sezione, tuttavia l'etichetta non viene ridimensionata nella seconda sezione finché la tabella non scorre. La vista della mia raccolta è nella cella della tabella.
EI Captain v2.0

5

In rapido. Secondo la risposta di Michaels

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
}

4

Sulla base di tutte le risposte, cambio un po 'e funziona bene per me

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
}

4

Se qualcuno di voi ha problemi, alcune delle celle che si trovano a destra della visualizzazione della raccolta superano i limiti della visualizzazione della raccolta . Quindi per favore usa questo -

class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let attributes = super.layoutAttributesForElements(in: rect)

        var leftMargin : CGFloat = sectionInset.left
        var maxY: CGFloat = -1.0
        attributes?.forEach { layoutAttribute in
            if Int(layoutAttribute.frame.origin.y) >= Int(maxY) {
                leftMargin = sectionInset.left
            }

            layoutAttribute.frame.origin.x = leftMargin

            leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
            maxY = max(layoutAttribute.frame.maxY , maxY)
        }
        return attributes
    }
}

Utilizzare INT al posto del confronto dei valori CGFloat .


3

Basato sulle risposte qui, ma risolti arresti anomali e problemi di allineamento quando la visualizzazione della raccolta includeva anche intestazioni o piè di pagina. Allineamento delle sole celle a sinistra:

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
    }
}

2

Grazie per la risposta di Michael Sand . L'ho modificato in una soluzione di più righe (lo stesso allineamento in alto y di ogni riga) che è Allineamento a sinistra, spaziatura uniforme per ogni elemento.

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;
}

1

Ecco il mio viaggio per trovare il codice migliore che funzioni con Swift 5. Ho unito un paio di risposte da questo thread e alcuni altri thread per risolvere avvisi e problemi che ho dovuto affrontare. Ho ricevuto un avviso e alcuni comportamenti anomali durante lo scorrimento della visualizzazione della raccolta. La console stampa quanto segue:

Ciò si verifica probabilmente perché il layout di flusso "xyz" modifica gli attributi restituiti da UICollectionViewFlowLayout senza copiarli.

Un altro problema che ho riscontrato è che alcune celle lunghe vengono ritagliate sul lato destro dello schermo. Inoltre, stavo impostando gli inserti di sezione e il minimumInteritemSpacing nelle funzioni delegate che risultavano in valori non riflessi nella classe personalizzata. La soluzione è stata l'impostazione di quegli attributi su un'istanza del layout prima di applicarlo alla vista della raccolta.

Ecco come ho utilizzato il layout per la visualizzazione della raccolta:

let layout = LeftAlignedCollectionViewFlowLayout()
layout.minimumInteritemSpacing = 5
layout.minimumLineSpacing = 7.5
layout.sectionInset = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
super.init(frame: frame, collectionViewLayout: layout)

Ecco la classe di layout del flusso

class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let attributes = super.layoutAttributesForElements(in: rect)?.map { $0.copy() as! UICollectionViewLayoutAttributes }

        var leftMargin = sectionInset.left
        var maxY: CGFloat = -1.0
        attributes?.forEach { layoutAttribute in
            guard layoutAttribute.representedElementCategory == .cell else {
                return
            }

            if Int(layoutAttribute.frame.origin.y) >= Int(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 + minimumInteritemSpacing
            maxY = max(layoutAttribute.frame.maxY , maxY)
        }

        return attributes
    }
}

Mi hai salvato la giornata
David 'mArm' Ansermot il

0

Il problema con UICollectionView è che tenta di adattare automaticamente le celle nell'area disponibile. L'ho fatto definendo prima il numero di righe e colonne e quindi definendo la dimensione della cella per quella riga e colonna

1) Per definire le sezioni (righe) della mia UICollectionView:

(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView

2) Per definire il numero di elementi in una sezione. È possibile definire un numero diverso di elementi per ogni sezione. puoi ottenere il numero di sezione usando il parametro "sezione".

(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section

3) Per definire la dimensione della cella per ciascuna sezione e riga separatamente. È possibile ottenere il numero di sezione e il numero di riga utilizzando il parametro "indexPath", ad esempio [indexPath section]per il numero di sezione e [indexPath row]per il numero di riga.

(CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath

4) Quindi puoi visualizzare le tue celle in righe e sezioni usando:

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

NOTA: in UICollectionView

Section == Row
IndexPath.Row == Column

0

La risposta di Mike Sand è buona, ma avevo riscontrato alcuni problemi con questo codice (come una lunga cella ritagliata). E nuovo codice:

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

0

Modificata la risposta di Angel García Olloqui al rispetto minimumInteritemSpacingda parte del delegato collectionView(_:layout:minimumInteritemSpacingForSectionAt:), se la implementa.

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
}

0

Il codice sopra funziona per me. Vorrei condividere il rispettivo codice 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
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.