你如何确定UICollectionView flowLayout中的单元格之间的间距

我有一个stream布局的UICollectionView,每个单元格是一个正方形。 我如何确定每行的每个单元格之间的间距? 我似乎无法find适当的设置。 我看到在集合视图的nib文件上有一个min间距属性,但是我把它设置为0,单元格甚至没有粘住。

在这里输入图像说明

任何其他的想法?

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


以@亚光为首,我修改了他的代码,以确保项目总是左alignment。 我发现如果一个项目本身结束了,它将被stream布局居中。 我做了以下更改来解决这个问题。

这种情况只会发生,如果你有单元格的宽度变化,这可能会导致像下面的布局。 由于UICollectionViewFlowLayout的行为,最后一行总是保持alignment,问题在于它们本身在任何行中,而不是最后一行。

用@亚特的代码,我看到了。

在这里输入图像说明

在那个例子中,我们看到如果单元格自己最后到达,单元格就会居中。 下面的代码确保您的collections视图看起来像这样。

在这里输入图像说明

 #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 

为了获得最大的项目间隔,子类UICollectionViewFlowLayout并重写layoutAttributesForElementsInRect:layoutAttributesForItemAtIndexPath:

例如,一个常见的问题是:集合视图的行是左alignment的,除了最后一行是左alignment的。 比方说,我们希望所有的行都是左alignment的,所以他们之间的空间是,比方说,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已经为我们完成的布局工作。 已经决定每一行有多less项目, 如果你明白我的意思,我们只是一边阅读这些文章,一边推着这些文章。

有几件事情需要考虑:

  1. 尝试更改IB中的最小间距,但将光标留在该字段中。 请注意,Xcode不会立即将文档标记为已更改。 但是,当您点击不同的字段时,Xcode会注意到文档已更改,并在文件导航器中将其标记。 所以,请务必在进行更改之后选项卡或点击另一个字段。

  2. 进行更改后保存storyboard / xib文件,并确保重新生成应用程序。 不要错过这个步骤,然后你就不知道为什么你的改变似乎没有任何效果。

  3. UICollectionViewFlowLayout具有minimumInteritemSpacing属性,这是您在IB中设置的内容。 但是集合的委托也可以有一个方法来确定项目间的间距 。 该方法王牌的布局的属性,所以如果你在委托中实现它,你的布局的属性将不会被使用。

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

一个简单的左alignment的方法是在你的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; } 

有一点math可以更容易的做到这一点。 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; } 

克里斯解决scheme的迅捷版本。

 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 } 

对于感兴趣的人来说,这是一个更清晰的快速版本,基于Chris Wagner的回答:

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

这里是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 } } 

UICollectionViewFlowLayout的“问题”在于它对单元格应用了一个alignment的alignment方式:一行中的第一个单元格是左alignment的,一行中的最后一个单元格是右alignment的,并且其间的所有其他单元格都是均匀分布的间距大于minimumInteritemSpacing

已经有许多很好的答案来解决这个问题,通过UICollectionViewFlowLayout 。 因此,您会得到一个alignment左侧单元格的布局。 另一个有效的方法是以一个恒定的间距分布单元格,以便将单元格alignment。

AlignedCollectionViewFlowLayout

我也创build了一个UICollectionViewFlowLayout子类,它遵循UICollectionViewFlowLayoutChris Wagner所build议的类似思路,可以将单元格

⬅︎

左对齐布局

或者➡︎ 右键

右对齐的布局

你可以简单地从这里下载它,将布局文件添加到你的项目中,并将AlignedCollectionViewFlowLayout设置为你的集合视图的布局类:
https://github.com/mischa-hildebrand/AlignedCollectionViewFlowLayout

它是如何工作的(对于左alignment的单元格):

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

这里的概念是检查索引为i的当前单元格和索引为i-1的前一个单元格是否占据同一行。

  • 如果它们不是索引为i的单元格是行中最左边的单元格。
    →将单元格移到集合视图的左边缘(不更改其垂直位置)。
  • 如果他们这样做,具有索引i的单元格不是行中最左边的单元格。
    →获取前一个单元格的框架(索引为i-1 )并移动其旁边的当前单元格。

对于右alignment的单元格…

反之亦然,即你检查下一个索引i + 1的单元格。

清洁的Swift解决scheme,从进化的历史:

  1. 有无光泽的答案
  2. 有克里斯瓦格纳孤独项目修复
  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 ,其中我实际上添加了支持两个滚动方向(水平和垂直)的function。

一个基于mokagio的快速版本: https : //github.com/fanpyi/UICollectionViewLeftAlignedLayout-Swift

 class UICollectionViewLeftAlignedLayout: UICollectionViewFlowLayout { override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? { let attributesToReturn = super.layoutAttributesForElementsInRect(rect) if let attributesToReturn = attributesToReturn { for attributes in attributesToReturn { if attributes.representedElementKind == nil { let indexpath = attributes.indexPath if let attr = layoutAttributesForItemAtIndexPath(indexpath) { attributes.frame = attr.frame } } } } return attributesToReturn } override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? { if let currentItemAttributes = super.layoutAttributesForItemAtIndexPath(indexPath){ let sectionInset = self.evaluatedSectionInsetForItemAtIndex(indexPath.section) let isFirstItemInSection = indexPath.item == 0; let layoutWidth = CGRectGetWidth(self.collectionView!.frame) - sectionInset.left - sectionInset.right; if (isFirstItemInSection) { currentItemAttributes.leftAlignFrameWithSectionInset(sectionInset) return currentItemAttributes } let previousIndexPath = NSIndexPath(forItem: indexPath.item - 1, inSection: indexPath.section) let previousFrame = layoutAttributesForItemAtIndexPath(previousIndexPath)?.frame ?? CGRectZero let previousFrameRightPoint = previousFrame.origin.x + previousFrame.width let currentFrame = currentItemAttributes.frame; let strecthedCurrentFrame = CGRectMake(sectionInset.left, currentFrame.origin.y, layoutWidth, currentFrame.size.height) // if the current frame, once left aligned to the left and stretched to the full collection view // widht intersects the previous frame then they are on the same line let isFirstItemInRow = !CGRectIntersectsRect(previousFrame, strecthedCurrentFrame) if (isFirstItemInRow) { // make sure the first item on a line is left aligned currentItemAttributes.leftAlignFrameWithSectionInset(sectionInset) return currentItemAttributes } var frame = currentItemAttributes.frame; frame.origin.x = previousFrameRightPoint + evaluatedMinimumInteritemSpacingForSectionAtIndex(indexPath.section) currentItemAttributes.frame = frame; return currentItemAttributes; } return nil } func evaluatedMinimumInteritemSpacingForSectionAtIndex(sectionIndex:Int) -> CGFloat { if let delegate = self.collectionView?.delegate as? UICollectionViewDelegateFlowLayout { if delegate.respondsToSelector("collectionView:layout:minimumInteritemSpacingForSectionAtIndex:") { return delegate.collectionView!(self.collectionView!, layout: self, minimumInteritemSpacingForSectionAtIndex: sectionIndex) } } return self.minimumInteritemSpacing } func evaluatedSectionInsetForItemAtIndex(index: Int) ->UIEdgeInsets { if let delegate = self.collectionView?.delegate as? UICollectionViewDelegateFlowLayout { if delegate.respondsToSelector("collectionView:layout:insetForSectionAtIndex:") { return delegate.collectionView!(self.collectionView!, layout: self, insetForSectionAtIndex: index) } } return self.sectionInset } }