3

Suppose that I have one item in the collection view, the item will be centered in the collection view on the first row.

And with multiple items, all these items will be distributed horizontally in the collection view with appropriate spacing between them.

If the size of the collection view changed, the spacing between items will be changed simultaneously to fit the new size of the collection view.

The default behavior of NSCollectionView aligns the items at the left, without spacing between multiple items.

Should I use the layoutManager of collection view's layer to layout the items?

Since I am using data binding to provide items, it seems not easy to insert the constraints.

Demitri
  • 13,134
  • 4
  • 40
  • 41
mrahmiao
  • 1,291
  • 1
  • 10
  • 22

2 Answers2

3

The most straightforward way is to subclass NSCollectionViewFlowLayout. That layout is almost what you want - it will always have the same number of rows and items per row that you are looking for: you just want them to be centered.

The main idea is to take the frames that NSCollectionViewFlowLayout comes up with for each item, subtract those widths from the total width, and then update the frames so that they are distributed evenly.

As an overview, these are the steps:

  1. Override prepareLayout to calculate the number of columns in the current layout and the whitespace needed between each element (and the edges). This is done here so that we only need to calculate the values once.

  2. Override layoutAttributesForElementsInRect. Here, get the NSCollectionViewLayoutAttributes for each item in the given rect and adjust the x position of the origin based on the column the item is in the and grid spacing calculated above. Return the new attributes.

  3. Override shouldInvalidateLayoutForBoundsChange to always return YES as we need to recalculate everything when the bounds change.

I have a working sample application that demonstrates this here:

https://github.com/demitri/CenteringCollectionViewFlowLayout

but this is the full implementation:

//
//  CenteredFlowLayout.m
//
//  Created by Demitri Muna on 4/10/19.
//

#import "CenteredFlowLayout.h"
#import <math.h>

@interface CenteredFlowLayout()
{
    CGFloat itemWidth;   // width of item; assuming all items have the same width
    NSUInteger nColumns; // number of possible columns based on item width and section insets
    CGFloat gridSpacing; // after even distribution, space between each item and edges (if row full)
    NSUInteger itemCount;
}
- (NSUInteger)columnForIndexPath:(NSIndexPath*)indexPath;
@end

#pragma mark -

@implementation CenteredFlowLayout

- (void)prepareLayout
{
    [super prepareLayout];

    id<NSCollectionViewDelegateFlowLayout,NSCollectionViewDataSource> delegate = (id<NSCollectionViewDelegateFlowLayout,NSCollectionViewDataSource>)self.collectionView.delegate;
    NSCollectionView *cv = self.collectionView;

    if ([delegate collectionView:cv numberOfItemsInSection:0] == 0)
        return;

    itemCount = [delegate collectionView:cv numberOfItemsInSection:0];

    // Determine the maximum number of items per row (i.e. number of columns)
    //
    // Get width of first item (assuming all are the same)
    // Get the attributes returned by NSCollectionViewFlowLayout, not our method override.
    NSUInteger indices[] = {0,0};
    NSCollectionViewLayoutAttributes *attr = [super layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathWithIndexes:indices length:2]];
    itemWidth = attr.size.width;

    NSEdgeInsets insets;
    if ([delegate respondsToSelector:@selector(collectionView:layout:insetForSectionAtIndex:)])
        insets = [delegate collectionView:cv layout:self insetForSectionAtIndex:0];
    else
        insets = self.sectionInset;

    // calculate the number of columns that can fit excluding minimumInteritemSpacing:
    nColumns = floor((cv.frame.size.width - insets.left - insets.right) / itemWidth);
    // is there enough space for minimumInteritemSpacing?
    while ((cv.frame.size.width
            - insets.left - insets.right
            - (nColumns*itemWidth)
            - (nColumns-1)*self.minimumInteritemSpacing) < 0) {
        if (nColumns == 1)
            break;
        else
            nColumns--;
    }

    if (nColumns > itemCount)
        nColumns = itemCount; // account for a very wide window and few items

    // Calculate grid spacing
    // For a centered layout, all spacing (left inset, right inset, space between items) is equal
    // unless a row has fewer items than columns (but they are still aligned with that grid).
    //
    CGFloat totalWhitespace = cv.bounds.size.width - (nColumns * itemWidth);
    gridSpacing = floor(totalWhitespace/(nColumns+1));  // e.g.:   |  [x]  [x]  |
}

- (NSUInteger)columnForIndexPath:(NSIndexPath*)indexPath
{
    // given an index path in a collection view, return which column in the grid the item appears
    NSUInteger index = [indexPath indexAtPosition:1];
    NSUInteger row = (NSUInteger)floor(index/nColumns);
    return (index - (nColumns * row));
}

- (NSArray<__kindof NSCollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(NSRect)rect
{
    // We do not need to modify the number of rows/columns that NSCollectionViewFlowLayout
    // determines, we just need to adjust the x position to keep them evenly distributed horizontally.

    if (nColumns == 0) // prepareLayout not yet called
        return [super layoutAttributesForElementsInRect:rect];

    NSArray *attributes = [super layoutAttributesForElementsInRect:rect];
    if (attributes.count == 0)
        return attributes;

    for (NSCollectionViewLayoutAttributes *attr in attributes) {
        NSUInteger col = [self columnForIndexPath:attr.indexPath]; // column number
        NSRect newFrame = NSMakeRect(floor((col * itemWidth) + gridSpacing * (1 + col)),
                                     attr.frame.origin.y,
                                     attr.frame.size.width,
                                     attr.frame.size.height);
        attr.frame = newFrame;
    }

    return attributes;
}

- (BOOL)shouldInvalidateLayoutForBoundsChange:(NSRect)newBounds
{
    return YES;
}

@end
Demitri
  • 13,134
  • 4
  • 40
  • 41
1

You can create a subclass of NSCollectionViewLayout and implements the layoutAttributes methods accordingly.

Harry Ng
  • 1,070
  • 8
  • 20