我在我的项目中使用UICollectionView,其中一行中有多个不同宽度的单元格。根据: https://developer.apple.com/library/content/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/UsingtheFlowLayout/UsingtheFlowLayout.html
它使用相同的填充将细胞扩散到整个线上。这种情况按预期发生,除了我想左对齐它们,并硬编码填充宽度。
我想我需要继承UICollectionViewFlowLayout,但是在线阅读了一些教程之后,我似乎无法了解其工作原理。
答案 0 :(得分:114)
当线条仅由1个项目组成或者过于复杂时,此处的其他解决方案无法正常工作。
根据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
}
}
如果您希望补充视图保持其大小,请在forEach
调用中的闭包顶部添加以下内容:
guard layoutAttribute.representedElementCategory == .cell else {
return
}
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;
}
答案 1 :(得分:34)
这个问题的答案中包含许多好主意。但是,大多数都有一些缺点:
大多数解决方案仅覆盖layoutAttributesForElements(in rect: CGRect)
功能。 它们不会覆盖layoutAttributesForItem(at indexPath: IndexPath)
。这是一个问题,因为集合视图会定期调用后一个函数来检索特定索引路径的布局属性。如果您没有从该功能返回正确的属性,则可能会遇到所有类型的视觉错误,例如:在插入和删除单元格动画期间或使用自定义单元格时,通过设置集合视图布局estimatedItemSize
。
Apple docs州:
每个自定义布局对象都应实现
layoutAttributesForItemAtIndexPath:
方法。
许多解决方案还会对传递给rect
函数的layoutAttributesForElements(in rect: CGRect)
参数进行假设。例如,许多假设rect
总是从新行的开头开始,而不一定是这种情况。
换句话说:
为了解决这些问题,我创建了一个UICollectionViewFlowLayout
子类,它遵循matt和Chris Wagner在类似问题的答案中提出的类似想法。它可以对齐细胞
⬅︎ 左:
或➡︎ 正确:
并另外提供垂直对齐各个行中的单元格的选项(如果它们的高度不同)。
您可以在此处下载:
用法很简单,并在README文件中进行了解释。您基本上创建了AlignedCollectionViewFlowLayout
的实例,指定了所需的对齐方式并将其分配给您的集合视图collectionViewLayout
属性:
let alignedFlowLayout = AlignedCollectionViewFlowLayout(horizontalAlignment: .left,
verticalAlignment: .top)
yourCollectionView.collectionViewLayout = alignedFlowLayout
(它也可在Cocoapods上找到。)
这里的概念是仅依靠 layoutAttributesForItem(at indexPath: IndexPath)
函数。在layoutAttributesForElements(in rect: CGRect)
中,我们只需获取rect
中所有单元格的索引路径,然后为每个索引路径调用第一个函数以检索正确的帧:
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()
函数只是创建数组中所有布局属性的深层副本。您可以查看source code的实现。)
所以现在我们唯一要做的就是正确实现layoutAttributesForItem(at indexPath: IndexPath)
功能。超类UICollectionViewFlowLayout
已经在每一行中放置了正确数量的单元格,因此我们只需将它们在各自的行内移位。困难在于计算将每个单元格向左移动所需的空间量。
由于我们希望在单元格之间有一个固定的间距,因此核心思想是假设前一个单元格(当前布局的单元格左侧的单元格)已经正确定位。然后,我们只需要将单元格间距添加到前一个单元格框架的maxX
值,并且该值是当前单元格框架的origin.x
值。 / p>
现在我们只需要知道我们何时到达一行的开头,这样我们就不会在前一行的单元格旁边对齐一个单元格。 (这不仅会导致布局不正确,而且还会非常滞后。)因此我们需要一个递归锚点。我用于查找递归锚点的方法如下:
+---------+----------------------------------------------------------------+---------+
| | | |
| | +------------+ | |
| | | | | |
| section |- - -|- - - - - - |- - - - +---------------------+ - - - - - - -| section |
| inset | |intersection| | | line rect | inset |
| |- - -|- - - - - - |- - - - +---------------------+ - - - - - - -| |
| (left) | | | current item | (right) |
| | +------------+ | |
| | previous item | |
+---------+----------------------------------------------------------------+---------+
...我"画"当前单元格周围的矩形,并将其拉伸到整个集合视图的宽度上。当UICollectionViewFlowLayout
垂直居中所有单元格时,同一行中的每个单元格必须与此矩形相交。
因此,我只是检查索引 i-1 的单元格是否与从索引为 i 的单元格创建的此行矩形相交。
如果它相交,则索引 i 的单元格不是该行中最左边的单元格。
→获取上一个单元格的框架(索引 i-1 )并移动旁边的当前单元格。
如果它不相交,则索引为 i 的单元格是该行中最左边的单元格。
→将单元格移动到集合视图的左边缘(不更改其垂直位置)。
我不会在这里发布layoutAttributesForItem(at indexPath: IndexPath)
函数的实际实现,因为我认为最重要的部分是理解的想法,你总是可以检查我的实现source code。 (它比这里解释的要复杂一点,因为我也允许.right
对齐和各种垂直对齐选项。但是,它遵循相同的想法。)
答案 2 :(得分:20)
问题已经持续了一段时间,但没有答案,这是一个很好的问题。答案是覆盖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的建议,您可以从super获取布局属性并迭代它们。如果它是行中的第一个(由其origin.x定义在左边距上),则不管它并将x重置为零。然后对于第一个单元格和每个单元格,添加该单元格的宽度加上一些边距。这将传递给循环中的下一个项目。如果它不是第一项,则将其原始x设置为正在运行的计算边距,并将新元素添加到数组中。
答案 3 :(得分:17)
使用Swift 4.1和iOS 11,根据您的需要,您可以选择 2以下完整实施之一来解决您的问题。
UICollectionViewCell
s 以下实施说明如何使用UICollectionViewLayout
的{{3}},UICollectionViewFlowLayout
的{{3}}和UILabel
的{{3}}按顺序排列左对齐UICollectionView
中的自动调整单元格:
CollectionViewController.swift
import UIKit
class CollectionViewController: UICollectionViewController {
let array = ["1", "1 2", "1 2 3 4 5 6 7 8", "1 2 3 4 5 6 7 8 9 10 11", "1 2 3", "1 2 3 4", "1 2 3 4 5 6", "1 2 3 4 5 6 7 8 9 10", "1 2 3 4", "1 2 3 4 5 6 7", "1 2 3 4 5 6 7 8 9", "1", "1 2 3 4 5", "1", "1 2 3 4 5 6"]
let columnLayout = FlowLayout(
minimumInteritemSpacing: 10,
minimumLineSpacing: 10,
sectionInset: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
)
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.collectionViewLayout = columnLayout
collectionView?.contentInsetAdjustmentBehavior = .always
collectionView?.register(CollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return array.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell
cell.label.text = array[indexPath.row]
return cell
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
collectionView?.collectionViewLayout.invalidateLayout()
super.viewWillTransition(to: size, with: coordinator)
}
}
FlowLayout.swift
import UIKit
class FlowLayout: UICollectionViewFlowLayout {
required init(minimumInteritemSpacing: CGFloat = 0, minimumLineSpacing: CGFloat = 0, sectionInset: UIEdgeInsets = .zero) {
super.init()
estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
self.minimumInteritemSpacing = minimumInteritemSpacing
self.minimumLineSpacing = minimumLineSpacing
self.sectionInset = sectionInset
sectionInsetReference = .fromSafeArea
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let layoutAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
guard scrollDirection == .vertical else { return layoutAttributes }
// Filter attributes to compute only cell attributes
let cellAttributes = layoutAttributes.filter({ $0.representedElementCategory == .cell })
// Group cell attributes by row (cells with same vertical center) and loop on those groups
for (_, attributes) in Dictionary(grouping: cellAttributes, by: { ($0.center.y / 10).rounded(.up) * 10 }) {
// Set the initial left inset
var leftInset = sectionInset.left
// Loop on cells to adjust each cell's origin and prepare leftInset for the next cell
for attribute in attributes {
attribute.frame.origin.x = leftInset
leftInset = attribute.frame.maxX + minimumInteritemSpacing
}
}
return layoutAttributes
}
}
CollectionViewCell.swift
import UIKit
class CollectionViewCell: UICollectionViewCell {
let label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = .orange
label.preferredMaxLayoutWidth = 120
label.numberOfLines = 0
contentView.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
contentView.layoutMarginsGuide.topAnchor.constraint(equalTo: label.topAnchor).isActive = true
contentView.layoutMarginsGuide.leadingAnchor.constraint(equalTo: label.leadingAnchor).isActive = true
contentView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: label.trailingAnchor).isActive = true
contentView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: label.bottomAnchor).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
预期结果:
layoutAttributesForElements(in:)
UICollectionViewCell
s,大小固定下面的实现显示了如何使用UICollectionViewLayout
的{{3}}和UICollectionViewFlowLayout
的{{3}},以便在UICollectionView
中保留预定义大小的单元格}:
CollectionViewController.swift
import UIKit
class CollectionViewController: UICollectionViewController {
let columnLayout = FlowLayout(
itemSize: CGSize(width: 140, height: 140),
minimumInteritemSpacing: 10,
minimumLineSpacing: 10,
sectionInset: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
)
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.collectionViewLayout = columnLayout
collectionView?.contentInsetAdjustmentBehavior = .always
collectionView?.register(CollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 7
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell
return cell
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
collectionView?.collectionViewLayout.invalidateLayout()
super.viewWillTransition(to: size, with: coordinator)
}
}
FlowLayout.swift
import UIKit
class FlowLayout: UICollectionViewFlowLayout {
required init(itemSize: CGSize, minimumInteritemSpacing: CGFloat = 0, minimumLineSpacing: CGFloat = 0, sectionInset: UIEdgeInsets = .zero) {
super.init()
self.itemSize = itemSize
self.minimumInteritemSpacing = minimumInteritemSpacing
self.minimumLineSpacing = minimumLineSpacing
self.sectionInset = sectionInset
sectionInsetReference = .fromSafeArea
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let layoutAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
guard scrollDirection == .vertical else { return layoutAttributes }
// Filter attributes to compute only cell attributes
let cellAttributes = layoutAttributes.filter({ $0.representedElementCategory == .cell })
// Group cell attributes by row (cells with same vertical center) and loop on those groups
for (_, attributes) in Dictionary(grouping: cellAttributes, by: { ($0.center.y / 10).rounded(.up) * 10 }) {
// Set the initial left inset
var leftInset = sectionInset.left
// Loop on cells to adjust each cell's origin and prepare leftInset for the next cell
for attribute in attributes {
attribute.frame.origin.x = leftInset
leftInset = attribute.frame.maxX + minimumInteritemSpacing
}
}
return layoutAttributes
}
}
CollectionViewCell.swift
import UIKit
class CollectionViewCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = .cyan
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
预期结果:
答案 4 :(得分:10)
我有同样的问题, 试试Cocoapod UICollectionViewLeftAlignedLayout。只需将它包含在您的项目中并按如下所示进行初始化:
UICollectionViewLeftAlignedLayout *layout = [[UICollectionViewLeftAlignedLayout alloc] init];
UICollectionView *leftAlignedCollectionView = [[UICollectionView alloc] initWithFrame:frame collectionViewLayout:layout];
答案 5 :(得分:5)
在Michael Sand's answer的基础上,我创建了一个子类化UICollectionViewFlowLayout
库来做左,右或完全(基本上是默认的)水平对齐 - 它还允许您设置每个单元格之间的绝对距离。我也打算为它添加水平中心对齐和垂直对齐。
答案 6 :(得分:5)
迅速。根据迈克尔斯的回答
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
}
答案 7 :(得分:5)
以下是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
}
}
遗憾的是有一个大的例外。如果您使用UICollectionViewFlowLayout
的{{1}}。内部estimatedItemSize
正在改变一些事情。我没有完全跟踪它,但它在UICollectionViewFlowLayout
自我调整单元格之后明确调用其他方法。根据我的反复试验,我发现在自动调整过程中,每个单元格似乎都会更频繁地调用layoutAttributesForElementsInRect
。此更新layoutAttributesForItemAtIndexPath
适用于LeftAlignedFlowLayout
。它也适用于静态大小的单元格,但是额外的布局调用会让我在不需要自动调整单元格的情况下使用原始答案。
estimatedItemSize
答案 8 :(得分:5)
这是令人沮丧的问题之一,多年来这些事情已经发生了很大的变化。现在很容易。
// as you move across one row ...
a.frame.origin.x = x
x += a.frame.width + minimumInteritemSpacing
// obviously start fresh again each row
override func layoutAttributesForElements(
in rect: CGRect)->[UICollectionViewLayoutAttributes]? {
guard let att = super.layoutAttributesForElements(in: rect) else { return [] }
var x: CGFloat = sectionInset.left
var y: CGFloat = -1.0
for a in att {
if a.representedElementCategory != .cell { continue }
if a.frame.origin.y >= y { x = sectionInset.left }
a.frame.origin.x = x
x += a.frame.width + minimumInteritemSpacing
y = a.frame.maxY
}
return att
}
只需将其复制并粘贴到UICollectionViewFlowLayout
中就可以了。
这就是全部:
class TagsLayout: UICollectionViewFlowLayout {
required override init() {super.init(); common()}
required init?(coder aDecoder: NSCoder) {super.init(coder: aDecoder); common()}
private func common() {
estimatedItemSize = UICollectionViewFlowLayout.automaticSize
minimumLineSpacing = 10
minimumInteritemSpacing = 10
}
override func layoutAttributesForElements(
in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let att = super.layoutAttributesForElements(in:rect) else {return []}
var x: CGFloat = sectionInset.left
var y: CGFloat = -1.0
for a in att {
if a.representedElementCategory != .cell { continue }
if a.frame.origin.y >= y { x = sectionInset.left }
a.frame.origin.x = x
x += a.frame.width + minimumInteritemSpacing
y = a.frame.maxY
}
return att
}
}
感谢上面的@AlexShubin首先澄清了这一点!
答案 9 :(得分:5)
此页面上的大多数解决方案都太复杂了。左对齐它们的最简单方法,即使只有 1 个单元格,也是返回以下边插入:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
if collectionView.numberOfItems(inSection: section) == 1 {
let flowLayout = collectionViewLayout as! UICollectionViewFlowLayout
return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: collectionView.frame.width - flowLayout.itemSize.width)
} else {
return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
}
}
答案 10 :(得分:3)
基于所有答案,我改变了一点,它对我有用
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
}
答案 11 :(得分:1)
如果您的最低部署目标是iOS 13,我强烈建议您利用组成布局(文档here,WWDC演示文稿here)。
最初,我确实尝试了一些最佳答案。不幸的是,我们遇到了一个问题,其中一些细胞倾向于间歇性消失。对我们来说,这是在调用UICollectionView的reloadData
函数之后发生的。同样重要的是要注意,我们的单元格具有可变的宽度,也就是自动调整大小。
让我给你看一个例子。假设我们需要显示一个包含关键字气泡列表的页面。
这是您使用合成布局来完成此任务所需的条件。
override func viewDidLoad() {
super.viewDidLoad()
...
collectionView.collectionViewLayout = createLeftAlignedLayout()
}
private func createLeftAlignedLayout() -> UICollectionViewLayout {
let item = NSCollectionLayoutItem( // this is your cell
layoutSize: NSCollectionLayoutSize(
widthDimension: .estimated(40), // variable width
heightDimension: .absolute(48) // fixed height
)
)
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: .init(
widthDimension: .fractionalWidth(1.0), // 100% width as inset by its Section
heightDimension: .estimated(50) // variable height; allows for multiple rows of items
),
subitems: [item]
)
group.contentInsets = .init(top: 0, leading: 16, bottom: 0, trailing: 16)
group.interItemSpacing = .fixed(10) // horizontal spacing between cells
return UICollectionViewCompositionalLayout(section: .init(group: group))
}
如您所见,这非常简单。
答案 12 :(得分:1)
如果您遇到任何问题-集合视图右侧的某些单元格超出了集合视图的范围。 然后请使用它-
class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
var leftMargin : CGFloat = sectionInset.left
var maxY: CGFloat = -1.0
attributes?.forEach { layoutAttribute in
if Int(layoutAttribute.frame.origin.y) >= Int(maxY) {
leftMargin = sectionInset.left
}
layoutAttribute.frame.origin.x = leftMargin
leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
maxY = max(layoutAttribute.frame.maxY , maxY)
}
return attributes
}
}
使用 INT 代替比较 CGFloat 值。
答案 13 :(得分:1)
感谢Michael Sand's answer。我将它修改为多行的解决方案(每行的相同对齐顶部y),即左对齐,甚至是每个项目的间距。
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;
}
答案 14 :(得分:1)
这是我寻找适用于Swift 5的最佳代码的旅程。我加入了该线程和其他一些线程的答案,以解决我遇到的警告和问题。在集合视图中滚动时,我有一个警告和一些异常行为。控制台将打印以下内容:
这很可能是因为流布局“ xyz”正在修改UICollectionViewFlowLayout返回的属性而不复制它们。
我遇到的另一个问题是,一些冗长的单元格在屏幕的右侧被裁剪了。另外,我在委托函数中设置了部分插图和minimumInteritemSpacing,这导致值未反映在自定义类中。解决该问题的方法是,将这些属性设置为布局的实例,然后再将其应用于我的集合视图。
这是我在集合视图中使用布局的方式:
let layout = LeftAlignedCollectionViewFlowLayout()
layout.minimumInteritemSpacing = 5
layout.minimumLineSpacing = 7.5
layout.sectionInset = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
super.init(frame: frame, collectionViewLayout: layout)
这是流程布局类
class LeftAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)?.map { $0.copy() as! UICollectionViewLayoutAttributes }
var leftMargin = sectionInset.left
var maxY: CGFloat = -1.0
attributes?.forEach { layoutAttribute in
guard layoutAttribute.representedElementCategory == .cell else {
return
}
if Int(layoutAttribute.frame.origin.y) >= Int(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 + minimumInteritemSpacing
maxY = max(layoutAttribute.frame.maxY , maxY)
}
return attributes
}
}
答案 15 :(得分:0)
Angel 的回答仍然有意义。您只需要创建一个自定义流布局(并设置您的 collectionview 以使用该自定义流布局),但您唯一需要添加到该自定义类的是此方法(目标 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;
}
这是最终的 collectionview(具有自定义单元格宽度)最终的样子:collectionview
答案 16 :(得分:0)
基于所有答案。 适用于leftToRight和rightToLeft
class AlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
{
let attributes = super.layoutAttributesForElements(in: rect)
let ltr = UIApplication.shared.userInterfaceLayoutDirection == .leftToRight
var leftMargin = ltr ? sectionInset.left : (rect.maxX - sectionInset.right)
var maxY: CGFloat = -1.0
attributes?.forEach { layoutAttribute in
if layoutAttribute.frame.origin.y >= maxY
{
leftMargin = ltr ? sectionInset.left : (rect.maxX - sectionInset.right)
}
layoutAttribute.frame.origin.x = leftMargin - (ltr ? 0 : layoutAttribute.frame.width)
if (ltr)
{
leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
}
else
{
leftMargin -= layoutAttribute.frame.width + minimumInteritemSpacing
}
maxY = max(layoutAttribute.frame.maxY , maxY)
}
return attributes
}
}
答案 17 :(得分:0)
UICollectionView的问题在于它尝试自动调整可用区域中的单元格。 我这样做是首先定义行数和列数,然后定义该行和列的单元格大小
1)定义我的UICollectionView的部分(行):
(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
2)定义部分中的项目数。您可以为每个部分定义不同数量的项目。您可以使用'部分'获取部分编号。参数。
(NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
3)分别为每个部分和行定义单元格大小。您可以使用' indexPath'来获取部分编号和行号。参数,即 [indexPath section]
表示节号, [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
答案 18 :(得分:0)
根据此处的答案,但在收集视图也有页眉或页脚时修复了崩溃和对齐问题。左对齐单元格对齐:
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
}
}
答案 19 :(得分:0)
以上代码适合我。我想分享相应的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
}
}
答案 20 :(得分:0)
编辑AngelGarcíaOlloqui的回答是尊重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
}
答案 21 :(得分:0)
Mike Sand的回答很好,但我遇到过这段代码的一些问题(比如冗长的单元格被剪掉)。新代码:
#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;
}