左alignmentUICollectionView中的单元格

我在我的项目中使用UICollectionView,在一行中有多个不同宽度的单元格。 根据: https : //developer.apple.com/library/content/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/UsingtheFlowLayout/UsingtheFlowLayout.html

它通过相同的填充在整个行上传播出单元格。 这发生的预期,除了我想左alignment他们,并硬编码填充宽度。

我想我需要inheritanceUICollectionViewFlowLayout,但是在阅读了一些教程等在线我似乎并没有得到如何工作。

当这条线由1个条目组成或者过于复杂时,这里的其他解决scheme不能正常工作。

根据Ryan给出的例子,我通过检查新元素的Y位置来更改代码以检测新行。 性能非常简单快捷。

迅速:

class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout { override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { let attributes = super.layoutAttributesForElements(in: rect) var leftMargin = sectionInset.left var maxY: CGFloat = -1.0 attributes?.forEach { layoutAttribute in if layoutAttribute.frame.origin.y >= maxY { leftMargin = sectionInset.left } layoutAttribute.frame.origin.x = leftMargin leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing maxY = max(layoutAttribute.frame.maxY , maxY) } return attributes } } 

Objective-C的:

 - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { NSArray *attributes = [super layoutAttributesForElementsInRect:rect]; CGFloat leftMargin = self.sectionInset.left; //initalized to silence compiler, and actaully safer, but not planning to use. CGFloat maxY = -1.0f; //this loop assumes attributes are in IndexPath order for (UICollectionViewLayoutAttributes *attribute in attributes) { if (attribute.frame.origin.y >= maxY) { leftMargin = self.sectionInset.left; } attribute.frame = CGRectMake(leftMargin, attribute.frame.origin.y, attribute.frame.size.width, attribute.frame.size.height); leftMargin += attribute.frame.size.width + self.minimumInteritemSpacing; maxY = MAX(CGRectGetMaxY(attribute.frame), maxY); } return attributes; } 

这个问题已经有一段时间了,但没有答案,这是一个很好的问题。 答案是重写UICollectionViewFlowLayout子类中的一个方法:

 @implementation MYFlowLayoutSubclass //Note, the layout's minimumInteritemSpacing (default 10.0) should not be less than this. #define ITEM_SPACING 10.0f - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { NSArray *attributesForElementsInRect = [super layoutAttributesForElementsInRect:rect]; NSMutableArray *newAttributesForElementsInRect = [[NSMutableArray alloc] initWithCapacity:attributesForElementsInRect.count]; CGFloat leftMargin = self.sectionInset.left; //initalized to silence compiler, and actaully safer, but not planning to use. //this loop assumes attributes are in IndexPath order for (UICollectionViewLayoutAttributes *attributes in attributesForElementsInRect) { if (attributes.frame.origin.x == self.sectionInset.left) { leftMargin = self.sectionInset.left; //will add outside loop } else { CGRect newLeftAlignedFrame = attributes.frame; newLeftAlignedFrame.origin.x = leftMargin; attributes.frame = newLeftAlignedFrame; } leftMargin += attributes.frame.size.width + ITEM_SPACING; [newAttributesForElementsInRect addObject:attributes]; } return newAttributesForElementsInRect; } @end 

按照Apple的build议,您可以从super获得布局属性,并遍历它们。 如果它是该行中的第一个(由其origin.x定义在左边距上定义),则将其保留,并将x重置为零。 然后对于第一个单元格和每个单元格,添加该单元格的宽度以及一些边距。 这将传递给循环中的下一个项目。 如果它不是第一项,则将其origin.x设置为正在运行的计算边距,并将新元素添加到数组中。

我有同样的问题,尝试给Cocoapod UICollectionViewLeftAlignedLayout 。 只需将其包含在您的项目中,并像这样初始化它:

 UICollectionViewLeftAlignedLayout *layout = [[UICollectionViewLeftAlignedLayout alloc] init]; UICollectionView *leftAlignedCollectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:layout]; 

基于Michael Sand的回答 ,我创build了一个子类UICollectionViewFlowLayout库来完成左,右或全部(基本上是默认的)水平alignment – 它还允许您设置每个单元之间的绝对距离。 我也计划增加水平中心理由和垂直理由。

https://github.com/eroth/ERJustifiedFlowLayout

迅速。 根据迈克尔斯的回答

 override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? { guard let oldAttributes = super.layoutAttributesForElementsInRect(rect) else { return super.layoutAttributesForElementsInRect(rect) } let spacing = CGFloat(50) // REPLACE WITH WHAT SPACING YOU NEED var newAttributes = [UICollectionViewLayoutAttributes]() var leftMargin = self.sectionInset.left for attributes in oldAttributes { if (attributes.frame.origin.x == self.sectionInset.left) { leftMargin = self.sectionInset.left } else { var newLeftAlignedFrame = attributes.frame newLeftAlignedFrame.origin.x = leftMargin attributes.frame = newLeftAlignedFrame } leftMargin += attributes.frame.width + spacing newAttributes.append(attributes) } return newAttributes } 

这个问题的答案中有很多很棒的点子。 但是,他们大多数有一些缺点:

  • 不检查单元格y值的解决scheme仅适用于单行布局 。 他们失败了多行收集视图布局。
  • 如果所有单元格具有相同的高度那么检查y值的解决scheme就像AngelGarcíaOlloqui的答案 一样 。 他们失败的高度变化的单元格。
  • 大多数解决scheme只覆盖layoutAttributesForElements(in rect: CGRect)函数。 他们不重写layoutAttributesForItem(at indexPath: IndexPath) 这是一个问题,因为集合视图会定期调用后一个函数来检索特定索引path的布局属性。 如果不从该函数返回适当的属性,则可能会遇到各种可视错误,例如在单元格的插入和删除animation过程中,或者在设置集合视图布局的estimatedItemSize时使用自定义大小的单元格。 苹果文档陈述:

    每个自定义布局对象都需要实现layoutAttributesForItemAtIndexPath:方法。

  • 许多解决scheme还对传递给layoutAttributesForElements(in rect: CGRect)函数的rect参数进行了假设。 例如,很多都是基于这样的假设,即rect总是从一个新的行开始,而不一定是这种情况。

换句话说:

本页面提出的大多数解决scheme都适用于某些特定的应用程序,但在任何情况下都无法按预期工作。


AlignedCollectionViewFlowLayout

为了解决这些问题,我创build了一个UICollectionViewFlowLayout子类,它遵循UICollectionViewFlowLayoutChris Wagner在类似问题的答案中提出的类似思路。 它可以alignment单元格

⬅︎

左对齐布局

或者➡︎ 右键

右对齐的布局

并另外提供选项来垂直alignment它们各自行中的单元(以防它们的高度变化)。

你可以简单地在这里下载它:

https://github.com/mischa-hildebrand/AlignedCollectionViewFlowLayout

这个用法很简单,在README文件中有解释。 你基本上创build一个AlignedCollectionViewFlowLayout的实例,指定所需的alignment,并将其分配给您的集合视图的collectionViewLayout属性:

  let alignedFlowLayout = AlignedCollectionViewFlowLayout(horizontalAlignment: .left, verticalAlignment: .top) yourCollectionView.collectionViewLayout = alignedFlowLayout 

(它也可以在Cocoapods上使用 。)


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

这里的概念是完全依赖layoutAttributesForItem(at indexPath: IndexPath)函数 。 在layoutAttributesForElements(in rect: CGRect)我们只需获取rect中所有单元格的索引path,然后调用每个索引path的第一个函数来检索正确的帧:

 override public func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { // We may not change the original layout attributes // or UICollectionViewFlowLayout might complain. let layoutAttributesObjects = copy(super.layoutAttributesForElements(in: rect)) layoutAttributesObjects?.forEach({ (layoutAttributes) in if layoutAttributes.representedElementCategory == .cell { // Do not modify header views etc. let indexPath = layoutAttributes.indexPath // Retrieve the correct frame from layoutAttributesForItem(at: indexPath): if let newFrame = layoutAttributesForItem(at: indexPath)?.frame { layoutAttributes.frame = newFrame } } }) return layoutAttributesObjects } 

copy()函数只是创build数组中所有布局属性的深层副本,您可以查看源代码的实现。)

所以现在我们唯一要做的就是正确地实现layoutAttributesForItem(at indexPath: IndexPath)函数。 超类UICollectionViewFlowLayout已经在每一行中放入了正确数量的单元格,所以我们只需要将它们在各自的行内左移。 难点在于计算将每个单元向左移动所需的空间量。

因为我们希望在单元之间有一个固定的间距,所以核心思想就是假定前一个单元(当前布局的单元左侧的单元)已经正确定位了。 那么我们只需要把单元格间距加到前一个单元格框架的maxX值上,这就是当前单元格框架的origin.x值。

现在我们只需要知道什么时候我们已经到达一行的开始,这样我们就不会在上一行中的单元格旁边alignment一个单元格。 (这不仅会导致布局错误,而且还会非常滞后。)所以我们需要有一个recursion锚。 我用来findrecursion锚点的方法如下:

要找出索引i中的单元格是否与索引为i-1的单元格在同一行中

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

…我在当前单元格周围“绘制”一个矩形,并将其拉伸到整个集合视图的宽度。 由于UICollectionViewFlowLayout所有单元格垂直居中,同一行中的每个单元格必须与此矩形相交。

因此,我只需检查索引为i-1的单元格是否与从索引为i的单元格创build的此行矩形相交。

  • 如果相交,索引为i的单元格不是该行中最左边的单元格。
    →获取前一个单元格的框架(索引为i-1 )并移动其旁边的当前单元格。

  • 如果不相交,索引为i的单元格是行中最左边的单元格。
    →将单元格移到集合视图的左边缘(不更改其垂直位置)。

我不会在这里发布layoutAttributesForItem(at indexPath: IndexPath)函数的实际实现,因为我认为最重要的部分是理解这个想法,并且可以随时在源代码中检查我的实现。 (这里稍微复杂一些,因为我也允许.rightalignment和各种垂直alignment选项,但是它们遵循相同的想法。)


哇,我猜这是我在Stackoverflow上写的最长的答案。 我希望这有帮助。 😉

这里是Swift的原始答案。 它仍然大多数工作。

 class LeftAlignedFlowLayout: UICollectionViewFlowLayout { private override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? { let attributes = super.layoutAttributesForElementsInRect(rect) var leftMargin = sectionInset.left attributes?.forEach { layoutAttribute in if layoutAttribute.frame.origin.x == sectionInset.left { leftMargin = sectionInset.left } else { layoutAttribute.frame.origin.x = leftMargin } leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing } return attributes } } 

例外:自动调整单元格

可悲的是有一个很大的例外。 如果您使用的是UICollectionViewFlowLayoutestimatedItemSize 。 内部UICollectionViewFlowLayout正在改变一些东西。 我没有完全追踪它,但它明确调用layoutAttributesForElementsInRect后调用其他方法,而自我大小单元格。 从我的试验和错误,我发现它似乎调用layoutAttributesForItemAtIndexPath每个单元格更经常自动resize。 这个更新的LeftAlignedFlowLayout可以很好地处理estimatedItemSize 。 它也适用于静态大小的单元格,但额外的布局调用使我可以在任何时候使用原始的答案,我不需要自动化单元格。

 class LeftAlignedFlowLayout: UICollectionViewFlowLayout { private override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? { let layoutAttribute = super.layoutAttributesForItemAtIndexPath(indexPath)?.copy() as? UICollectionViewLayoutAttributes // First in a row. if layoutAttribute?.frame.origin.x == sectionInset.left { return layoutAttribute } // We need to align it to the previous item. let previousIndexPath = NSIndexPath(forItem: indexPath.item - 1, inSection: indexPath.section) guard let previousLayoutAttribute = self.layoutAttributesForItemAtIndexPath(previousIndexPath) else { return layoutAttribute } layoutAttribute?.frame.origin.x = previousLayoutAttribute.frame.maxX + self.minimumInteritemSpacing return layoutAttribute } } 

感谢Michael Sand的回答 。 我将它修改为左alignment的多行(相同的每行alignment顶部y)的解决scheme,甚至到每个项目的间距。

 static CGFloat const ITEM_SPACING = 10.0f; - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { CGRect contentRect = {CGPointZero, self.collectionViewContentSize}; NSArray *attributesForElementsInRect = [super layoutAttributesForElementsInRect:contentRect]; NSMutableArray *newAttributesForElementsInRect = [[NSMutableArray alloc] initWithCapacity:attributesForElementsInRect.count]; CGFloat leftMargin = self.sectionInset.left; //initalized to silence compiler, and actaully safer, but not planning to use. NSMutableDictionary *leftMarginDictionary = [[NSMutableDictionary alloc] init]; for (UICollectionViewLayoutAttributes *attributes in attributesForElementsInRect) { UICollectionViewLayoutAttributes *attr = attributes.copy; CGFloat lastLeftMargin = [[leftMarginDictionary valueForKey:[[NSNumber numberWithFloat:attributes.frame.origin.y] stringValue]] floatValue]; if (lastLeftMargin == 0) lastLeftMargin = leftMargin; CGRect newLeftAlignedFrame = attr.frame; newLeftAlignedFrame.origin.x = lastLeftMargin; attr.frame = newLeftAlignedFrame; lastLeftMargin += attr.frame.size.width + ITEM_SPACING; [leftMarginDictionary setObject:@(lastLeftMargin) forKey:[[NSNumber numberWithFloat:attributes.frame.origin.y] stringValue]]; [newAttributesForElementsInRect addObject:attr]; } return newAttributesForElementsInRect; } 

基于所有的答案,我改变了一下,它对我有好处

 override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { let attributes = super.layoutAttributesForElements(in: rect) var leftMargin = sectionInset.left var maxY: CGFloat = -1.0 attributes?.forEach { layoutAttribute in if layoutAttribute.frame.origin.y >= maxY || layoutAttribute.frame.origin.x == sectionInset.left { leftMargin = sectionInset.left } if layoutAttribute.frame.origin.x == sectionInset.left { leftMargin = sectionInset.left } else { layoutAttribute.frame.origin.x = leftMargin } leftMargin += layoutAttribute.frame.width maxY = max(layoutAttribute.frame.maxY, maxY) } return attributes } 

UICollectionView的问题是它试图自动适应可用区域中的单元格。 我通过首先定义行数和列数然后定义该行和列的单元格大小来完成此操作

1)定义我的UICollectionView的部分(行):

 (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView 

2)定义一个部分的项目数量。 您可以为每个部分定义不同数量的项目。 你可以使用'section'参数来获得节号。

 (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section 

3)分别定义每个部分和行的单元格大小。 您可以使用'indexPath'参数获得部分编号和行号,即部分编号为[indexPath section] [indexPath row] ,行编号为[indexPath row]

 (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath 

4)然后你可以显示你的单元格的行和部分使用:

 (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath 

注意:在UICollectionView中

 Section == Row IndexPath.Row == Column 

迈克·桑德的回答很好,但是我遇到了这个代码的一些问题(像冗长的单元格一样)。 和新的代码:

 #define ITEM_SPACE 7.0f @implementation LeftAlignedCollectionViewFlowLayout - (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 + ITEM_SPACE; 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; } 

编辑天使加西亚Olloqui的答案尊重minimumInteritemSpacing从委托的collectionView(_:layout:minimumInteritemSpacingForSectionAt:) ,如果它实现它。

 override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { let attributes = super.layoutAttributesForElements(in: rect) var leftMargin = sectionInset.left var maxY: CGFloat = -1.0 attributes?.forEach { layoutAttribute in if layoutAttribute.frame.origin.y >= maxY { leftMargin = sectionInset.left } layoutAttribute.frame.origin.x = leftMargin let delegate = collectionView?.delegate as? UICollectionViewDelegateFlowLayout let spacing = delegate?.collectionView?(collectionView!, layout: self, minimumInteritemSpacingForSectionAt: 0) ?? minimumInteritemSpacing leftMargin += layoutAttribute.frame.width + spacing maxY = max(layoutAttribute.frame.maxY , maxY) } return attributes } 

基于这里的答案,但是当你的集合视图也有页眉或页脚时,修复崩溃和alignment问题。 alignment只留下单元格:

 class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout { override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { let attributes = super.layoutAttributesForElements(in: rect) var leftMargin = sectionInset.left var prevMaxY: CGFloat = -1.0 attributes?.forEach { layoutAttribute in guard layoutAttribute.representedElementCategory == .cell else { return } if layoutAttribute.frame.origin.y >= prevMaxY { leftMargin = sectionInset.left } layoutAttribute.frame.origin.x = leftMargin leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing prevMaxY = layoutAttribute.frame.maxY } return attributes } } 

上面的代码适用于我。 我想分享各自的Swift 3.0代码。

 class SFFlowLayout: UICollectionViewFlowLayout { let itemSpacing: CGFloat = 3.0 override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { let attriuteElementsInRect = super.layoutAttributesForElements(in: rect) var newAttributeForElement: Array<UICollectionViewLayoutAttributes> = [] var leftMargin = self.sectionInset.left for tempAttribute in attriuteElementsInRect! { let attribute = tempAttribute if attribute.frame.origin.x == self.sectionInset.left { leftMargin = self.sectionInset.left } else { var newLeftAlignedFrame = attribute.frame newLeftAlignedFrame.origin.x = leftMargin attribute.frame = newLeftAlignedFrame } leftMargin += attribute.frame.size.width + itemSpacing newAttributeForElement.append(attribute) } return newAttributeForElement } }