How do I implement an NSCollectionView with centered items that have self-destructive margins?

Suppose 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 of those items will be distributed horizontally as a collection with appropriate spacing between them.

If the size of the collection view has changed, the spacing between the items will be resized at the same time to fit the new size of the collection view.

The NSCollectionView

default behavior is to align items to the left, without spacing between multiple items.

Should I use a layoutManager

collection view layer to lay out items?

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

+3


source to share


2 answers


You can subclass NSCollectionViewLayout and implement layoutAttributes methods accordingly.



+1


source


The easiest way is to NSCollectionViewFlowLayout

subclass NSCollectionViewFlowLayout

. This layout is almost what you want - it will always have the same number of lines and elements per line that you are looking for: you just want them to be centered.

The basic idea is to take the frames that are NSCollectionViewFlowLayout

for each element, subtract those widths from the total width, and then update the frames so they are evenly distributed.

As an overview, these steps:

  1. Override prepareLayout

    to calculate the number of columns in the current layout and the spaces needed between each item (and edges). This is done here, so we only need to calculate the values ​​once.

  2. Override layoutAttributesForElementsInRect

    . Here, get NSCollectionViewLayoutAttributes

    for each item in the given rectangle and adjust the NSCollectionViewLayoutAttributes

    X position based on the column the item is in and the grid spacing calculated above. Return new attributes.

  3. Override shouldInvalidateLayoutForBoundsChange

    to always return YES

    as we need to recalculate everything when the bounds change.



I have a working example application that demonstrates this here:

https://github.com/demitri/CenteringCollectionViewFlowLayout

but this is the complete implementation:

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

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

@interface CenteredFlowLayout()
{
    CGFloat itemWidth;   // wdith 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;

    //CGFloat inset = self.sectionInset.left;

    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

      

0


source







All Articles