it-roy-ru.com

Как вы определяете расстояние между ячейками в UICollectionView flowLayout

У меня есть UICollectionView с макетом потока, и каждая ячейка представляет собой квадрат. Как определить расстояние между ячейками в каждом ряду? Я не могу найти соответствующие настройки для этого. Я вижу, что в nib-файле есть атрибуты min spacing для представления коллекции, но я установил это значение в 0, и ячейки даже не слипаются.

enter image description here

Любая другая идея?

46
adit

Обновление: Swift версия этого ответа: https://github.com/fanpyi/UICollectionViewLeftAlignedLayout-Swift


Взяв @ matt's lead я изменил его код, чтобы убедиться, что элементы ВСЕГДА выровнены по левому краю. Я обнаружил, что если бы элемент оказался в отдельной строке, он был бы центрирован по схеме потока. Я сделал следующие изменения для решения этой проблемы.

Такая ситуация может возникнуть, только если у вас есть ячейки, которые различаются по ширине, что может привести к макету, подобному следующему. Последняя строка всегда выравнивается по левому краю из-за поведения UICollectionViewFlowLayout, проблема заключается в элементах, которые сами по себе находятся в любой строке, кроме последней.

С кодом @ Мэтта я видел.

enter image description here

В этом примере мы видим, что ячейки центрируются, если они сами оказываются на линии. Код ниже гарантирует, что ваш вид коллекции будет выглядеть следующим образом.

enter image description here

#import "CWDLeftAlignedCollectionViewFlowLayout.h"

const NSInteger kMaxCellSpacing = 9;

@implementation CWDLeftAlignedCollectionViewFlowLayout

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

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

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

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

        return currentItemAttributes;
    }

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

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

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

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

@end
68
Chris Wagner

Чтобы получить максимальный интервал между элементами, создайте подкласс UICollectionViewFlowLayout и переопределите layoutAttributesForElementsInRect: и layoutAttributesForItemAtIndexPath:.

Например, общая проблема заключается в следующем: строки представления коллекции выровнены по правому и левому краю, за исключением последней строки, выровненной по левому краю. Допустим, мы хотим, чтобы все линии были выровнены по левому краю, чтобы расстояние между ними составляло, скажем, 10 точек. Вот простой способ (в вашем подклассе UICollectionViewFlowLayout):

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

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

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

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

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

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

Причина, по которой это так просто, в том, что мы не выполняем тяжелую работу по макету; мы используем работу по макету, которую UICollectionViewFlowLayout уже сделал для нас. Он уже решил, сколько предметов идет в каждой строке; мы просто читаем эти строки и собираем все вместе, если вы понимаете, о чем я.

32
matt

Есть несколько вещей, которые следует учитывать:

  1. Попробуйте изменить минимальный интервал в IB, но оставьте курсор в этом поле. Обратите внимание, что Xcode не сразу помечает документ как измененный. Однако, когда вы щелкаете в другом поле, XCode замечает, что документ был изменен, и помечает его в файловом навигаторе. Поэтому обязательно внесите вкладку или нажмите на другое поле после внесения изменений.

  2. Сохраните файл раскадровки/xib после внесения изменений и обязательно перестройте приложение. Нетрудно пропустить этот шаг, и тогда вы остаетесь чесать голову, задаваясь вопросом, почему ваши изменения, похоже, не дали никакого эффекта.

  3. UICollectionViewFlowLayout имеет свойство minimumInteritemSpacing, которое вы устанавливаете в IB. Но делегат коллекции также может иметь метод для определения расстояния между элементами . Этот метод имеет свойство макета, поэтому если вы реализуете его в своем делегате, свойство макета не будет использоваться.

  4. Помните, что интервал есть минимум интервал. Макет будет использовать это число (независимо от того, получено ли оно из свойства или из метода делегата) в качестве наименьшего допустимого пространства, но он может использовать большее пространство, если у него осталось место на строке. Так, если, например, вы установили минимальный интервал равным 0, вы все равно можете увидеть несколько пикселей между элементами. Если вам нужен больший контроль над точным расположением элементов, вам, вероятно, следует использовать другой макет (возможно, созданный вами).

16
Caleb

Немного математики делает трюк легче. Код, написанный Крисом Вагнером, ужасен, потому что он вызывает атрибуты макета каждого из предыдущих элементов. Таким образом, чем больше вы прокручиваете, тем медленнее ...

Просто используйте по модулю вот так (я использую в качестве максимального значения также значение минимального значения InInteritemSpacing):

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

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

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

    return currentItemAttributes;
}
4
ldesroziers

Простой способ выравнивания влево - изменить layoutAttributesForElementsInRect: в вашем подклассе UICollectionViewFlowLayout:

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

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

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

Swift-версия решения Криса.

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

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

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

                return currentItemAttributes;
            }

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

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

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

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

Чтобы использовать это, сделайте следующее:

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

"Проблема" с UICollectionViewFlowLayoutзаключается в том, что он применяет выравнивание по выравниванию в ячейках: первые ячейки в распределении по левому краю, а последние minimumInteritemSpacingname__.

Есть много хороших ответов, которые решают проблему создания подкласса UICollectionViewFlowLayoutname__. В результате вы получите макет, который выравнивает ячейки по левому краю . Правильное решение для распределения ячеек с постоянным интервалом - выравнивание ячеек вправо .

AlignedCollectionViewFlowLayout

Я также создал подкласс UICollectionViewFlowLayoutname__, который следует аналогичной идее, предложенной matt и Chris Wagner , что можно выровнять клетки

⬅︎ левый :

Left-aligned layout

или же➡︎ правый :

Right-aligned layout

Добавить файл макета в свой проект и установить AlignedCollectionViewFlowLayoutв качестве класса макета представления коллекции:
https://github.com/mischa-hildebrand/AlignedCollectionViewFlowLayout

Как это работает (для выровненных по левому краю ячеек):

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

Идея состоит в том, чтобы проверить, занимаются ли текущие ячейки с индексом i и предыдущая ячейка с индексом i-1, занимают одну и ту же самую копию.

  • Если они не имеют ячейки с индексом i самая левая ячейка в строке.
    .
  • Если это так, то ячейка с индексом i не является самой левой ячейкой в ​​строке.
    → Получить кадр предыдущей ячейки (с индексом i - 1) и переместить текущую ячейку рядом с ней.

Для выровненных по правому краю ячеек ...

... вы делаете то же самое, наоборот, вы проверяете ячейку далее с индексом i + 1.

2
Mischa

Решение Clean Swift, из истории эволюции:

  1. был матовый ответ
  2. там было Крис Вагнер исправил одинокие вещи
  3. было mokagio раздел Inset и минимальный InteritemSpacing улучшение
  4. была версия фанпия Swift
  5. теперь вот упрощенная и чистая моя версия:
open class UICollectionViewLeftAlignedLayout: UICollectionViewFlowLayout {
    open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        return super.layoutAttributesForElements(in: rect)?.map { $0.representedElementKind == nil ? layoutAttributesForItem(at: $0.indexPath)! : $0 }
    }

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

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

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

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

Использование: расстояние между элементами определяется делегатом collectionView (_:layout:minimumInteritemSpacingForSectionAt:).

Я поставил его на github, https://github.com/Coeur/UICollectionViewLeftAlignedLayout , где я фактически добавил функцию поддержки обоих направлений прокрутки (горизонтального и вертикального).

1
Cœur

Более чистая версия Swift для заинтересованных людей, основанная на ответе Криса Вагнера:

class AlignLeftFlowLayout: UICollectionViewFlowLayout {

    var maximumCellSpacing = CGFloat(9.0)

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

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

        return attributesToReturn
    }

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

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

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

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

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

        return curAttributes
    }
}
1
Kevin R

Вот это для NSCollectionViewFlowLayout

class LeftAlignedCollectionViewFlowLayout: NSCollectionViewFlowLayout {

    var maximumCellSpacing = CGFloat(2.0)

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

        let attributesToReturn = super.layoutAttributesForElementsInRect(rect)

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

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

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

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

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

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

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

        return curAttributes
    }
}
0
Chris