如何实现具有自调整边距的居中项目的NSCollectionView?

时间:2015-06-03 04:29:33

标签: macos cocoa nscollectionview nscollectionviewitem

假设我在集合视图中有一个项目,该项目将在第一行的集合视图中居中。

对于多个项目,所有这些项目将在集合视图中水平分布,并在它们之间留出适当的间距。

如果集合视图的大小发生更改,则项目之间的间距将同时更改以适合集合视图的新大小。

NSCollectionView的默认行为会对齐左侧的项目,而不会在多个项目之间留出间距。

我应该使用layoutManager集合视图的图层来布置项目吗?

由于我使用数据绑定来提供项目,因此插入约束似乎并不容易。

2 个答案:

答案 0 :(得分:1)

您可以创建NSCollectionViewLayout的子类并相应地实现layoutAttributes方法。

答案 1 :(得分:0)

最直接的方法是继承NSCollectionViewFlowLayout。该布局几乎是您想要的-它始终具有与您要查找的行和项目相同的行数:您只希望它们居中。

主要思想是获取NSCollectionViewFlowLayout对应的每个项目的框架,从总宽度中减去这些宽度,然后更新框架以使其均匀分布。

作为概述,以下是这些步骤:

  1. 覆盖prepareLayout,以计算当前布局中的列数以及每个元素(和边缘)之间所需的空白。这样做是为了使我们只需要计算一次值。

  2. 覆盖layoutAttributesForElementsInRect。在此,获取给定rect中每个项目的NSCollectionViewLayoutAttributes并根据项目所在的列和上述计算的网格间距调整原点的x位置。返回新属性。

  3. 覆盖shouldInvalidateLayoutForBoundsChange以始终返回YES是因为我们需要在范围更改时重新计算所有内容。

我有一个工作示例应用程序,在这里演示了这一点:

https://github.com/demitri/CenteringCollectionViewFlowLayout

但这是完整的实现:

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