UICollectionView中的单元格左对齐


95

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

它以相等的填充将单元格分布到整个行中。发生这种情况是预期的,除了我想左对齐它们,并硬编码填充宽度。

我认为我需要继承UICollectionViewFlowLayout的子类,但是在在线阅读了一些教程等之后,我似乎并不清楚它是如何工作的。


Answers:


169

当该行仅由一项组成或过于复杂时,该线程中的其他解决方案将无法正常工作。

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

目标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;
}

4
谢谢!我收到警告,此错误通过在数组中复制属性来解决 let attributes = super.layoutAttributesForElementsInRect(rect)?.map { $0.copy() as! UICollectionViewLayoutAttributes }
tkuichooseyou

2
对于可能像我一样偶然发现此问题的人,我在这里发布了Angel答案的修改版本:stackoverflow.com/a/38254368/2845237
Alex Koshy

我尝试了此解决方案,但集合视图右侧的某些单元格超出了集合视图的范围。还有其他人遇到吗?
Happiehappie

@Happiehappie是的,我也有这种行为。任何解决办法?
约翰·鲍姆

尝试了此解决方案,但无法将布局与“界面构建器实用程序”面板中的collectionView关联。最终通过代码完成。有什么线索吗?
acrespo

61

这个问题的答案中包含许多好主意。但是,它们大多数都有一些缺点:

  • 不检查单元格y值的解决方案仅适用于单行布局。它们对于多行的集合视图布局失败。
  • 确实检查y值的解决方案(例如AngelGarcíaOlloqui的答案) 仅在所有单元格都具有相同高度时才起作用。它们对于高度可变的单元失败。
  • 大多数解决方案仅覆盖该layoutAttributesForElements(in rect: CGRect)功能。它们不会覆盖layoutAttributesForItem(at indexPath: IndexPath)这是一个问题,因为收集视图会定期调用后一个函数来检索特定索引路径的布局属性。如果没有从该函数返回适当的属性,则很可能会遇到各种视觉错误,例如,在单元格的插入和删除动画期间,或者通过设置集合视图布局的来使用自动调整大小的单元格时estimatedItemSize。在苹果文档的状态:

    每个自定义布局对象均应实现该layoutAttributesForItemAtIndexPath:方法。

  • 许多解决方案还对rect传递给layoutAttributesForElements(in rect: CGRect)函数的参数进行了假设。例如,许多基于这样的假设:rect始终总是在新行的开头开始,而事实并非一定如此。

换句话说:

此页面上建议的大多数解决方案都适用于某些特定的应用程序,但它们并非在每种情况下都可以正常工作。


AlignedCollectionViewFlowLayout

为了解决这些问题,我创建了一个UICollectionViewFlowLayout子类,该子类遵循与mattChris Wagner在回答类似问题时所提出的类似思想。它可以对齐细胞

⬅︎

左对齐布局

➡︎ 正确

右对齐的布局

并且还提供了一些选项,以使单元格在各自的行中垂直对齐(以防它们的高度发生变化)。

您只需在这里下载:

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

用法很简单,并在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()函数仅创建数组中所有布局属性的深层副本。您可以查看其实现的源代码。)

因此,现在我们唯一要做的就是layoutAttributesForItem(at indexPath: IndexPath)正确实现该功能。超类UICollectionViewFlowLayout已经在每行中放置了正确数量的单元格,因此我们只需要在它们各自的行中向左移动它们即可。困难在于计算我们需要将每个单元向左移动的空间量。

由于我们希望在单元之间具有固定的间距,因此核心思想是假设先前的单元(当前布置的单元左侧的单元)已经正确放置。然后,我们只需将像元间距添加到maxX前一个像元帧的origin.x值,即当前像元帧的值。

现在我们只需要知道何时到达行的开头,这样就不会在上一行中的单元格旁边对齐单元格。(这不仅会导致布局不正确,而且还会造成很大的延迟。)因此,我们需要一个递归锚点。我用于查找递归锚点的方法如下:

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

 +---------+----------------------------------------------------------------+---------+
 |         |                                                                |         |
 |         |     +------------+                                             |         |
 |         |     |            |                                             |         |
 | 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)此处发布该函数的实际实现,因为我认为最重要的部分是理解该思想,并且您随时可以在源代码中检查我的实现。(这比这里解释的要复杂一些,因为我也允许.right对齐和各种垂直对齐选项。但是,它遵循相同的思想。)


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


我在这种情况下适用stackoverflow.com/questions/56349052/…, 但是它不起作用。您能告诉我如何实现吗
Nazmul Hasan

即使有这个(这是一个非常棒的项目),我在我的应用程序垂直滚动集合视图中仍会遇到闪烁。我有20个单元格,在第一个滚动条上,第一个单元格为空白,所有单元格都向右移动一个位置。向上和向下滚动一整步后,一切正常对齐。
Parth Tamane

很棒的项目!很高兴看到它为Carthage启用。
pjuzeliunas

30

使用Swift 4.1和iOS 11,您可以根据需要选择以下两种完整实现之一来解决问题。


#1。左对齐自动尺寸UICollectionViewCell小号

下图为执行如何使用UICollectionViewLayoutlayoutAttributesForElements(in:)UICollectionViewFlowLayoutestimatedItemSizeUILabelpreferredMaxLayoutWidth,以左对齐自动尺寸细胞在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")
    }

}

预期结果:

在此处输入图片说明


#2。UICollectionViewCell固定尺寸左对齐

下图为执行如何使用UICollectionViewLayoutlayoutAttributesForElements(in:)UICollectionViewFlowLayoutitemSize,以左对齐细胞与预定义的大小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")
    }

}

预期结果:

在此处输入图片说明


按行对单元格属性进行分组时,是否使用10任意数字?
amariduran

25

2019年的简单解决方案

多年来,这是令人沮丧的问题,事情发生了很大的变化。现在很容易。

基本上,您只是这样做:

    // as you move across one row ...
    a.frame.origin.x = x
    x += a.frame.width + minimumInteritemSpacing
    // and, 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!


嘿。我有点困惑。在开头和结尾处为每个字符串添加空间仍然会在项目单元格中显示我的单词,而没有添加的空间。有什么想法吗?
Mohamed Lee,

如果您的收藏夹视图中有节标题,这似乎会杀死它们。
TylerJames

22

这个问题已经有一段时间了,但是没有答案,这是一个很好的问题。答案是重写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重置为零。然后,对于第一个单元格和每个单元格,添加该单元格的宽度加上一些边距。这将传递到循环中的下一个项目。如果不是第一项,则将其origin.x设置为运行的计算出的边距,然后将新元素添加到数组中。


我认为这比克里斯·瓦格纳(Chris Wagner )在链接问题上的解决方案(stackoverflow.com/a/15554667/482557)更好-您的实现具有零个重复的UICollectionViewLayout调用。
Evan R

1
如果该行只有一个项目,则看起来不可行,因为UICollectionViewFlowLayout居中。您可以检查中心以解决问题。像attribute.center.x == self.collectionView?.bounds.midX
Ryan Poolos 2015年

我实现了它,但是由于某种原因,它在第一部分中中断了:第一个单元格出现在第一行的左侧,第二个单元格(不适合同一行)出现在下一行,但不在左侧对齐。而是,其x位置从第一个单元格结束的位置开始。
Nicolas Miari

@NicolasMiari循环确定它是新行,并leftMarginif (attributes.frame.origin.x == self.sectionInset.left) {支票重置。假设默认布局已将新行的单元格与对齐sectionInset.left。如果不是这种情况,将导致您描述的行为。我将在循环内插入一个断点,并在比较之前检查两边的第二行单元格。祝你好运。
Mike Sand

3
谢谢...我最终使用了UICollectionViewLeftAlignedLayout:github.com/mokagio/UICollectionViewLeftAlignedLayout。它覆盖了其他两种方法。
Nicolas Miari

9

我遇到了同样的问题,尝试一下Cocoapod UICollectionViewLeftAlignedLayout。只需将其包含在您的项目中,然后像这样初始化即可:

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

您在那里的布局初始化时缺少方括号
RyanOfCourse 2015年

我测试了两个答案github.com/eroth/ERJustifiedFlowLayout和UICollectionViewLeftAlignedLayout。当使用estimatedItemSize(自动调整大小)时,只有第二个产生正确的结果。
EckhardN


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

例外:自动调整单元格

可悲的是,有一个大例外。如果您使用UICollectionViewFlowLayoutestimatedItemSize。内部UICollectionViewFlowLayout正在改变事情。我还没有完全找到它,但是在对layoutAttributesForElementsInRect单元格进行自定义大小后,它清楚地调用了其他方法。通过反复试验,我发现它似乎在layoutAttributesForItemAtIndexPath在自动调整大小期间每个单元。此更新的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
    }
}

这适用于第一部分,但是标签不会在第二部分上调整大小,直到表格滚动为止。我的收藏视图在表格单元格中。
EI机长v2.0,19年

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
}

4

根据所有答案,我做了些改动,对我很有用

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
}

4

如果您遇到任何问题- 集合视图右侧的某些单元格会超出集合视图的范围。然后请使用-

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值。


3

根据此处的答案,但可以解决在集合视图还包含页眉或页脚时崩溃和对齐的问题。对齐仅左单元格:

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

2

感谢Michael Sand的回答。我将其修改为左对齐的多行解决方案(每行相同的对齐方式Top 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;
}

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

您保存了我的一天
David'mArm'Ansermot

0

UICollectionView的问题在于,它试图自动适应可用区域中的单元格。我首先定义行和列的数量,然后定义该行和列的单元格大小

1)定义我的UICollectionView的Sections(行):

(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView

2)定义节中的项目数。您可以为每个部分定义不同数量的项目。您可以使用“ section”参数获取部分编号。

(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

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

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
}

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
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.