Вирівнюйте ліворуч клітинки в UICollectionView


96

Я використовую UICollectionView у своєму проекті, де на рядку є кілька комірок різної ширини. Відповідно до: https://developer.apple.com/library/content/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/UsingtheFlowLayout/UsingtheFlowLayout.html

він розподіляє клітинки по всій лінії з однаковим заповненням. Це трапляється, як очікувалося, за винятком того, що я хочу залишити їх виправданням, і жорстким кодом ширини відступу.

Я вважаю, що мені потрібно підклас UICollectionViewFlowLayout, однак після прочитання деяких навчальних посібників тощо в Інтернеті я, здається, не розумію, як це працює.


Відповіді:


169

Інші рішення в цій темі не працюють належним чином, коли рядок складається лише з 1 елемента або є надто складним.

На основі прикладу, наведеного Райаном, я змінив код, щоб виявити новий рядок, перевіривши положення Y нового елемента. Дуже простий і швидкий у виконанні.

Стрімкий:

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

Якщо ви хочете, щоб додаткові подання зберегли свій розмір, додайте у верхній частині закриття forEachдзвінка наступне :

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

Завдання-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
Дякую! Я отримував попередження, яке виправлено шляхом копіювання атрибутів у масиві let attributes = super.layoutAttributesForElementsInRect(rect)?.map { $0.copy() as! UICollectionViewLayoutAttributes }
tkuichooseyou

2
Для тих, хто, можливо, натрапив на це, шукаючи вирівняну по центру версію, як і я, я опублікував змінену версію відповіді Ангела тут: stackoverflow.com/a/38254368/2845237
Алекс Коши

Я спробував це рішення, але частина комірок, розташованих праворуч від подання колекції, перевищує межі подання колекції. Хтось ще стикався з цим?
Happiehappie

@Happiehappie так, у мене така поведінка також. будь-яке виправлення?
Джон Баум,

Спробував це рішення, але не зміг пов’язати макет із collectionView на панелі «Інструменти побудови інтерфейсу». Закінчився робити це за допомогою коду. Будь-яка підказка, чому?
відповідь

63

У відповідях на це питання є багато чудових ідей. Однак у більшості з них є деякі недоліки:

  • Рішення, які не перевіряють значення y комірки, працюють лише для однорядкових макетів . Вони не вдаються до макетів подання колекції з кількома рядками.
  • Рішення, які перевіряють значення y, як відповідь Ангела Гарсії Оллокі, працюють, лише якщо всі клітинки мають однакову висоту . Вони виходять з ладу для комірок зі змінною висотою.
  • Більшість рішень замінює лише layoutAttributesForElements(in rect: CGRect)функцію. Вони не замінюють layoutAttributesForItem(at indexPath: IndexPath). Це проблема, оскільки подання колекції періодично викликає останню функцію для отримання атрибутів макета для певного шляху до індексу. Якщо ви не повернете належні атрибути з цієї функції, ви, ймовірно, натрапите на всі види візуальних помилок, наприклад, під час вставки та видалення анімацій комірок або при використанні саморозмірних комірок, встановивши макет вигляду колекції estimatedItemSize. У документах Apple зазначено:

    Очікується, що кожен об’єкт нестандартного макета реалізує layoutAttributesForItemAtIndexPath:метод.

  • Багато рішень також роблять припущення щодо rectпараметра, який передається layoutAttributesForElements(in rect: CGRect)функції. Наприклад, багато хто базується на припущенні, що rectзавжди починається на початку нового рядка, що не обов'язково так.

Отже іншими словами:

Більшість рішень, запропонованих на цій сторінці, працюють для деяких конкретних програм, але вони працюють не так, як очікувалось у кожній ситуації.


AlignedCollectionViewFlowLayout

Для вирішення цих питань я створив UICollectionViewFlowLayoutпідклас, який слідує подібній ідеї, як пропонували Метт та Кріс Вагнер у своїх відповідях на подібне питання. Він може вирівняти клітини

⬅︎ ліворуч :

Макет, вирівняний за лівим краєм

або ➡︎ праворуч :

Макет, вирівняний по правому краю

і додатково пропонує варіанти вертикального вирівнювання комірок у відповідних рядках (у випадку, якщо вони різняться по висоті).

Ви можете просто завантажити його тут:

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

Використання є простим і пояснюється у файлі README. Ви в основному створюєте екземпляр AlignedCollectionViewFlowLayout, вказуєте бажане вирівнювання та призначаєте його collectionViewLayoutвластивості перегляду колекції :

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

 yourCollectionView.collectionViewLayout = alignedFlowLayout

(Це також доступно на Cocoapods .)


Як це працює (для комірок, вирівняних за лівим краєм):

Концепція тут полягає в тому, щоб покладатися виключно на layoutAttributesForItem(at indexPath: IndexPath)функцію . У layoutAttributesForElements(in rect: CGRect)ми просто отримуємо шляхи індексу всіх комірок в межах, rectа потім викликаємо першу функцію для кожного шляху індексу для отримання правильних кадрів:

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
}

( copy()Функція просто створює глибоку копію всіх атрибутів макета в масиві. Ви можете переглянути вихідний код для її реалізації.)

Тож єдине, що нам потрібно зробити, це правильно реалізувати layoutAttributesForItem(at indexPath: IndexPath)функцію. Суперклас UICollectionViewFlowLayoutвже розміщує правильну кількість комірок у кожному рядку, тому нам залишається лише зрушити їх ліворуч у межах відповідного рядка. Складність полягає в обчисленні обсягу простору, який нам потрібен для зсуву кожної клітини вліво.

Оскільки ми хочемо мати фіксований інтервал між клітинками, основна ідея полягає в тому, щоб просто припустити, що попередня комірка (клітина ліворуч від клітини, яка в даний час викладена) вже розташована належним чином. Тоді нам залишається лише додати інтервал комірок до maxXзначення кадру попередньої комірки, і це origin.xзначення для поточного кадру комірки.

Тепер нам потрібно лише знати, коли ми дійшли до початку рядка, щоб ми не вирівняли клітинку поруч із коміркою попереднього рядка. (Це призведе не тільки до неправильної розкладки, але також буде надзвичайно відсталим.) Отже, нам потрібно мати якір рекурсії. Підхід, який я використовую для пошуку того прив’язку рекурсії, такий:

Щоб з’ясувати, чи знаходиться комірка з індексом i в одному рядку з коміркою з індексом i-1 ...

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

... Я "малюю" прямокутник навколо поточної комірки і розтягую його на ширину всього подання колекції. Оскільки UICollectionViewFlowLayoutцентри всіх комірок вертикально, кожна клітинка в одній лінії повинна перетинатися з цим прямокутником.

Таким чином, я просто перевіряю, чи перетинається комірка з індексом i-1 з цим прямокутником лінії, створеним з комірки з індексом i .

  • Якщо воно перетинається, клітинка з індексом i не є самою лівою коміркою в рядку.
    → Отримайте кадр попередньої комірки (з індексом i − 1 ) і перемістіть поточну комірку поруч із нею.

  • Якщо воно не перетинається, комірка з індексом i є самою лівою коміркою в рядку.
    → Перемістіть клітинку до лівого краю подання колекції (не змінюючи її вертикальне положення).

Я не буду публікувати тут фактичну реалізацію layoutAttributesForItem(at indexPath: IndexPath)функції, тому що, на мою думку, найважливішою частиною є розуміння ідеї, і ви завжди можете перевірити мою реалізацію у вихідному коді . (Це трохи складніше, ніж пояснено тут, тому що я також дозволяю .rightвирівнювання та різні варіанти вертикального вирівнювання. Однак, це слідує тій самій ідеї.)


Нічого собі, я думаю, це найдовша відповідь, яку я коли-небудь писав на Stackoverflow. Сподіваюся, це допоможе. 😉


Я застосовую цей сценарій stackoverflow.com/questions/56349052/…, але він не працює.
підкажіть,

Навіть з цим (що є дійсно приголомшливим проектом) я відчуваю блимання у поданні колекції вертикальної прокрутки своїх додатків. У мене 20 комірок, і на першому прокручуванні перша комірка порожня, і всі комірки зміщуються на одну позицію вправо. Після однієї повної прокрутки вгору і вниз все вирівнюється належним чином.
Parth Tamane

Чудовий проект! Було б чудово, якби це було включено для Карфагена.
pjuzeliunas

30

У Swift 4.1 та iOS 11, відповідно до ваших потреб, ви можете вибрати одну з 2 наступних повних реалізацій , щоб вирішити проблему.


№1. Вирівнювання за лівим краєм автоматичного розміру UICollectionViewCells

Реалізація нижче показує, як використовувати UICollectionViewLayout's layoutAttributesForElements(in:), UICollectionViewFlowLayout' estimatedItemSizeі UILabel's preferredMaxLayoutWidthдля того, щоб вирівняти клітини авторозміру вліво в 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")
    }

}

Очікуваний результат:

введіть тут опис зображення


№2. Вирівняйте ліворуч UICollectionViewCells за фіксованим розміром

Реалізація нижче показує, як використовувати UICollectionViewLayout' layoutAttributesForElements(in:)і UICollectionViewFlowLayout' itemSize, щоб вирівняти клітинки за заздалегідь визначеним розміром у 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")
    }

}

Очікуваний результат:

введіть тут опис зображення


Чи використовується 10довільне число при групуванні атрибутів комірок за рядками ?
amariduran

25

Просте рішення у 2019 році

Це одне з тих гнітючих питань, де за ці роки все сильно змінилося. Зараз це легко.

В основному ви просто робите це:

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

Зараз вам потрібен лише шаблонний код:

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
}

Просто скопіюйте та вставте це в UICollectionViewFlowLayout- все готово.

Повне робоче рішення для копіювання та вставлення:

Це все:

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

введіть тут опис зображення

І, нарешті...

Подякуйте @AlexShubin, який першим пояснив це!


Привіт Я трохи розгублений. Додавання пробілу для кожного рядка на початку та закінченні все одно відображає моє слово в клітинці елемента без доданого місця. Будь-яка ідея, як це зробити?
Мохамед Лі

Якщо у вашій колекції є заголовки розділів, це, здається, їх вбиває.
TylerJames

22

Питання задавалось деякий час, але відповіді немає, і це хороше питання. Відповідь полягає в тому, щоб замінити один метод у підкласі 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

Як рекомендує Apple, ви отримуєте атрибути макета від super і переглядаєте їх. Якщо це перший у рядку (визначається тим, що його origin.x знаходиться на лівому полі), ви залишаєте це в спокої і скидаєте x до нуля. Потім для першої комірки та кожної комірки ви додаєте ширину цієї комірки плюс деяке поле. Це передається наступному елементу циклу. Якщо це не перший елемент, ви встановлюєте для origin.x поточне обчислене поле та додаєте нові елементи до масиву.


Я думаю, це набагато краще, ніж рішення Кріса Вагнера ( stackoverflow.com/a/15554667/482557 ) щодо пов'язаного питання - ваша реалізація має нульові повторювані виклики UICollectionViewLayout.
Еван Р

1
Не схоже, що це працює, якщо в рядку є один елемент, оскільки UICollectionViewFlowLayout центрує його. Ви можете перевірити центр, щоб вирішити проблему. Щось на зразокattribute.center.x == self.collectionView?.bounds.midX
Райан Пуолос

Я реалізував це, але чомусь воно переривається для першого розділу: перша клітинка з’являється ліворуч від першого рядка, а друга комірка (яка не вміщується в цьому ж рядку) переходить до наступного рядка, але не зліва вирівняні. Натомість його положення x починається там, де закінчується перша комірка.
Ніколас Міарі

@NicolasMiari Цикл вирішує, що це новий рядок, і скидає за leftMarginдопомогою if (attributes.frame.origin.x == self.sectionInset.left) {чека. Це передбачає, що за замовчуванням макет вирівняв комірку нового рядка так, щоб вона була на рівні з sectionInset.left. Якщо це не так, поведінка, яку ви описуєте, матиме наслідком. Я б вставив точку зупинки всередину циклу і перевірив обидві сторони на клітинку другого рядка безпосередньо перед порівнянням. Удачі.
Mike Sand

3
Дякую ... У підсумку я використав UICollectionViewLeftAlignedLayout: github.com/mokagio/UICollectionViewLeftAlignedLayout . Це замінює ще кілька методів.
Ніколас Міарі

9

У мене була та ж проблема, спробуйте Cocoapod UICollectionViewLeftAlignedLayout . Просто включіть його у свій проект та ініціалізуйте так:

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

Не вистачає початкової квадратної дужки під час ініціалізації макета там
RyanOfCourse

Я протестував дві відповіді github.com/eroth/ERJustifiedFlowLayout та UICollectionViewLeftAlignedLayout. Лише другий дав правильний результат при використанні оціненогоItemSize (саморозмір).
ЕкхардН

5

Спираючись на відповідь Майкла Сенда , я створив підкласовану UICollectionViewFlowLayoutбібліотеку для горизонтального вирівнювання ліворуч, праворуч або повністю (в основному за замовчуванням) - це також дозволяє встановити абсолютну відстань між кожною клітинкою. Я планую додати до нього також вирівнювання по горизонталі та вертикальне вирівнювання.

https://github.com/eroth/ERJustifiedFlowLayout


5

Ось оригінальна відповідь у Swift. Це все ще чудово працює в основному.

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

Виняток: автоматичний розмір комірок

На жаль, є один великий виняток. Якщо ви використовуєте UICollectionViewFlowLayout«S estimatedItemSize. Внутрішній процес UICollectionViewFlowLayoutтрохи змінює ситуацію. Я не повністю його відстежив, але зрозуміло, як викликати інші методи після того, layoutAttributesForElementsInRectяк самостійно проклеює клітини. З моїх спроб і помилок я виявив, що, здається, layoutAttributesForItemAtIndexPathчастіше вимагається для кожної комірки під час автоматичного розміру. Це оновлення LeftAlignedFlowLayoutчудово працює з estimatedItemSize. Це працює і з комірками статичного розміру, однак додаткові виклики макету змушують мене використовувати оригінальну відповідь у будь-який час, коли мені не потрібні автоматичні розміри комірок.

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

Це працює для першого розділу, однак мітка не змінює розмір другого розділу, поки таблиця не прокрутиться. Перегляд моєї колекції знаходиться в комірці таблиці.
EI Captain v2.0

5

Швидко. Відповідно до відповіді Майклза

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

Виходячи з усіх відповідей, я трохи змінююся, і це добре для мене працює

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

Якщо хтось із вас стикається з проблемою - деякі комірки, розташовані праворуч від подання колекції, перевищують межі подання колекції . Тоді, будь ласка, використовуйте це -

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

Використовуйте INT замість порівняння значень CGFloat .


3

На основі відповідей тут, але виправлені збої та вирівнювання проблем, коли у вашій колекції також є верхній або нижній колонтитули. Вирівнювання лише лівих комірок:

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

Дякую за відповідь Майкла Сенда . Я змінив його на рішення з декількох рядків (однакове вирівнювання вгорі у кожного рядка), яке є лівим вирівнюванням, навіть з інтервалом до кожного елемента.

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

Ось моя подорож, щоб знайти найкращий код, який працює з Swift 5. Я об’єднав кілька відповідей із цієї теми та деяких інших тем, щоб вирішити попередження та проблеми, з якими я стикався. У мене було попередження та деяка ненормальна поведінка під час прокрутки подання колекції. Консоль друкує таке:

Це, ймовірно, відбувається, оскільки макет потоку "xyz" змінює атрибути, повернені UICollectionViewFlowLayout, не копіюючи їх.

Ще одне питання, з яким я зіткнувся, полягає в тому, що деякі довгі клітинки обрізаються праворуч на екрані. Крім того, я встановлював вставки розділів та мінімальний InterteremSpacing у функціях делегата, що призвело до значень, не відображалися у власному класі. Виправленням цього було встановлення цих атрибутів на екземпляр макета перед застосуванням його до мого подання колекції.

Ось як я використовував макет для подання своєї колекції:

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)

Ось клас макета потоку

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

Ти врятував мій день
Девід 'mArm' Ансермот,

0

Проблема UICollectionView полягає в тому, що він намагається автоматично помістити комірки у доступну область. Я зробив це, спершу визначивши кількість рядків і стовпців, а потім визначивши розмір комірки для цього рядка та стовпця

1) Щоб визначити розділи (рядки) мого UICollectionView:

(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView

2) Визначити кількість елементів у розділі. Ви можете визначити різну кількість елементів для кожного розділу. Ви можете отримати номер розділу за допомогою параметра 'section'.

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

3) Визначити розмір комірки для кожного розділу та рядка окремо. Ви можете отримати номер розділу та номер рядка за допомогою параметра 'indexPath', тобто [indexPath section]для номера розділу та [indexPath row]для номера рядка.

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

4) Потім ви можете відображати свої комірки в рядках та розділах, використовуючи:

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

ПРИМІТКА: У UICollectionView

Section == Row
IndexPath.Row == Column

0

Відповідь Майка Сенда хороша, але у мене виникли деякі проблеми з цим кодом (наприклад, довга клітинка, яку висікають). І новий код:

#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

Відредаговано відповідь Ангела Гарсії Оллокі на повагу minimumInteritemSpacingз боку делегата collectionView(_:layout:minimumInteritemSpacingForSectionAt:), якщо він її реалізує.

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

Наведений вище код працює для мене. Я хотів би поділитися відповідним кодом 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
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.