如何确定UICollectionView流式布局中单元格之间的间距

51

我有一个使用流式布局的UICollectionView,每个单元格都是正方形。如何确定每行中每个单元格之间的间距?我找不到相应的设置。我在集合视图的nib文件中看到了最小间距属性,但我将其设置为0,单元格甚至不能够粘贴在一起。

图片描述

还有其他的想法吗?


请左对齐UICollectionView中的单元格。参见https://dev59.com/C2Eh5IYBdhLWcg3wVCLK - Cœur
12个回答

73
更新:这个答案的 Swift 版本:https://github.com/fanpyi/UICollectionViewLeftAlignedLayout-Swift

参照 @matt 的方法,我修改了他的代码以确保项目始终左对齐。我发现,如果一个项目单独出现在一行上,流式布局会将其居中。我进行了以下更改来解决此问题。

只有当您的单元格宽度不同时,才会出现此情况,可能会导致以下布局。由于 UICollectionViewFlowLayout 的行为,最后一行总是左对齐,但问题在于任何行中单独存在的项目。

使用 @matt 的代码时,我看到了以下结果。

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];

    CGRect currentFrame = currentItemAttributes.frame;

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

        return currentItemAttributes;
    }

    NSIndexPath* previousIndexPath = [NSIndexPath indexPathForItem:indexPath.item-1 inSection:indexPath.section];
    CGRect previousFrame = [self layoutAttributesForItemAtIndexPath:previousIndexPath].frame;
    CGFloat previousFrameRightPoint = CGRectGetMaxX(previousFrame) + kMaxCellSpacing;

    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
        currentFrame.origin.x = sectionInset.left; // first item on the line should always be left aligned
        currentItemAttributes.frame = currentFrame;
        return currentItemAttributes;
    }

    currentFrame.origin.x = previousFrameRightPoint;
    currentItemAttributes.frame = currentFrame;
    return currentItemAttributes;
}

@end

1
离题了,我该如何实现像你们一样的标签UI元素?有什么开源项目可以建议吗?谢谢。 - X.Y.
1
您在我的截图中看到的是UICollectionView中的UICollectionViewCell。触摸事件由委托方法处理。然后,它们使用可伸缩背景进行样式设置,除了蓝色背景,它只是单元格内的一张图片。据我所知,没有开源的东西,但是使用UICollectionView相当简单,难点是让它们以这种方式对齐,而这里已经为您完成了 ;) - Chris Wagner
@ChrisWagner你能提供一些有关设置行距的细节吗?我已将kMaxCellSpacing更改为3,并实现了- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath,它返回标签按钮的计算大小(是的,我正在制作与您屏幕截图相同的东西)。但是,我仍然在处理行间距方面遇到麻烦。当单元格之间的距离为3时,行之间的距离仍然约为10。您能告诉我如何影响它吗? - kokoko
@ChrisWagner,实际上我不是在处理单元格(标签)之间的间距方面有问题,而是不知道如何更改行之间的间距,当单元格超出边界并且逻辑将它们放在新的“行”上时。抱歉,如果你误解了我,我弄混了行和线 :) - kokoko
1
请点击此处查看可能对代码的改进:http://stackoverflow.com/review/suggested-edits/4003993 - Robert Harvey
显示剩余9条评论

32
为了获得最大的单元间间距,需要子类化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已经为我们完成的布局工作。它已经决定了每行放多少项;我们只需读取这些行并将项目推在一起,如果你明白我的意思的话。

只是一个问题,因为我现在没有测试您的代码的可能性。这个答案能帮助我解决我在这里遇到的问题吗?http://stackoverflow.com/questions/13411609/pack-collectionviewcells-tightly-together-on-ios - Joakim Engstrom
1
太棒了,这里加入了代码的垂直版本:https://dev59.com/xWYq5IYBdhLWcg3wcgLq#14542269感谢提供如此棒的代码。 - Rob R.
2
感谢您提供的解决方案。有一个小问题:如果集合视图每行有大量水平项目(collectionView.contentSize.width比单元格宽度大得多),则此代码中的递归可能会很耗费资源。 - ricosrealm

16

有几点需要考虑:

  1. 尝试在IB中更改最小间距,但将光标留在该字段中。请注意,Xcode不会立即将文档标记为已更改。 但是,当您单击其他字段时,Xcode会注意到文档已更改,并在文件导航器中标记它。因此,在进行更改后,请务必切换到其他字段。

  2. 在更改后保存storyboard / xib文件,并确保重新构建应用程序。很容易忽略这一步,然后你会想知道为什么更改似乎没有任何效果。

  3. UICollectionViewFlowLayout有一个minimumInteritemSpacing属性,在IB中进行设置。但是,集合的代理也可以有一种方法来确定项目间距。该方法优先于布局的属性,因此如果在代理中实现它,则不会使用布局的属性。

  4. 请记住,那里的间距是最小间距。布局将使用该数字(无论它是来自属性还是委托方法)作为最小允许空间,但如果行上有剩余空间,则可能会使用更大的空间。因此,例如,如果将最小间距设置为0,则可能仍会看到项目之间的几个像素。如果您想要更精确地控制项目的间距,您可能应该使用不同的布局(可能是您自己创建的布局)。


2
谢谢您提供这个棒极了的答案。我认为我更需要的是maximumInteritemSpacing,这个值有吗? - adit
2
不,没有最大值。如果你考虑流式布局的工作原理,你就会明白为什么了。流式布局将尽可能多的项目放在一行上,同时仍然遵守最小项目间距。此时,将会剩下 n 个像素,其中 n 大于或等于 0,小于下一个项目的大小加上最小间距。然后,布局将取这些 n 个像素并均匀地分配它们以使行上的项目间距相等。如果您设置了最大间距,它将无法执行此操作。但是,您可以编写自己的布局以任何您喜欢的方式排列项目。 - Caleb
+1,你也可以根据你想要实现的效果设置minimumInteritemSpacingminimumLineSpacing - iDev
请问能否确定布局委托中确切的方法,用于确定水平项目间距。 - Drux
1
@Drux 抱歉,措辞不当。我会修正的。有一个 UICollectionViewDelegateFlowLayout 协议,集合视图的代理可以采用以支持流式布局。请参见方法 – collectionView:layout:minimumInteritemSpacingForSectionAtIndex: - Caleb

5
稍微进行一点数学计算就可以更容易地解决问题。Chris Wagner编写的代码很糟糕,因为它调用了每个先前项的布局属性。所以你滚动得越多,它就变得越慢......

只需像这样使用取模运算(我将我的minimumInteritemSpacing值同时用作最大值):

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

2
在我的情况下,前面单元格的大小很重要,这就是为什么需要调用前一个单元格的布局属性。我没有在大型集合视图上进行性能测试。我的用例最多有11个单元格,因此从未出现问题。 - Chris Wagner

3

调整UICollectionViewFlowLayout子类中的layoutAttributesForElementsInRect:方法是实现左对齐的简单方法:

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

干净利落的Swift解决方案,源自于演变历程:

  1. matt的答案
  2. Chris Wagner对单独项进行修复
  3. mokagio对sectionInset和minimumInteritemSpacing进行了改进
  4. fanpyi提供的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,在那里我实际上添加了一个支持水平和垂直两个滚动方向的功能。


2
克里斯解决方案的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
您可以用两种方式来完成它。
首先,在`layoutAttributesForItem`中进行一些修改,通过之前的`layoutAttributesForItem(at: IndexPath(item: indexPath.item - 1, section: indexPath.section))?.frame`获取当前属性的布局。
其次,通过`UICollectionViewLayoutAttributes(forCellWith: indexPath)`新建一些属性,将它们布局在任何您想要的地方。
而且这个数学计算有点大,因为它执行了布局的重活。
在`layoutAttributesForElements(in:)`方法中,您需要返回给定矩形内所有项目的布局属性。您将属性作为一个`UICollectionViewLayoutAttributes`数组返回给集合视图。
你可以查看我的代码库
  • 你可以将项目左对齐

您可以将项目右对齐。

您可以将项目右对齐并反转。


2
UICollectionViewFlowLayout存在一个问题,即它将单元格应用了一种合理的对齐方式:一行中的第一个单元格左对齐,一行中的最后一个单元格右对齐,而在它们之间的所有其他单元格都平均分布,且间距大于minimumInteritemSpacing
已经有许多很好的答案解决了这个问题,通过子类化UICollectionViewFlowLayout。结果是您可以得到一个将单元格左对齐的布局。另一种有效的解决方案是使用一个常量间距来分配单元格并将其右对齐

AlignedCollectionViewFlowLayout

我也创建了一个UICollectionViewFlowLayout子类,遵循与mattChris 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下一个单元格。


1

以下是基于Chris Wagner答案的更简洁的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
    }
}

CGRectIntersectsRect很直观且易于借鉴。@matt的CGFloat rightPrev = fPrev.origin.x + fPrev.size.width + 10;更好。结果相同,而且做得更少。比较Y位置更加简洁。 - dengST30

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接