如何开发一个自定义的UICollectionViewLayout,它具有交错的自定义单元格列?

时间:2015-09-04 12:35:04

标签: ios swift

我正在研究我已在Android上开发的应用的iOS版本。此应用程序具有以下2列自定义(固定宽度但可变高度)单元格:

Staggered 2 column layout on Android

在Android版本中实现这一点非常简单,因为Google为其StaggeredGridLayoutManager提供了RecyclerView。您可以指定列数和滚动方向。

默认UICollectionView布局UICollectionViewFlowLayout不允许我正在寻找的交错布局,因此我必须实现自定义布局。我观看了2个关于这个话题的WWDC视频(What's New in Table and Collection ViewsAdvanced User Interfaces with Collection Views),我或多或少都知道应该如何实现它。

第1步。首先计算布局的近似值。

enter image description here

第2步。然后使用自动布局创建单元格并调整其大小。

enter image description here

第3步。然后控制器会通知单元格大小,以便更新布局。

enter image description here

我在尝试编写这些步骤时遇到了疑问。我找到了一个tutorial来解释创建具有交错列的自定义布局,但它不使用autolayout来获取单元格的大小。这让我有以下问题:

在第2步中,我如何以及何时可以获得单元格大小?

在第3步中,我如何以及何时通知更改的布局?

4 个答案:

答案 0 :(得分:5)

我想指出的是,正如您所提到的,RayWenderlich PinInterest Layout正是帮助您实现此布局的教程。

回答你的问题 - 关于教程:

在第2步中,我如何以及何时可以获得单元格大小?

要获取单元格高度,已实现在自定义prepareLayout的{​​{1}}方法中调用的委托方法。此方法称为一次(或两次,我只是尝试使用UICollectionViewLayout语句运行它,我有两个调用)。 print的要点是初始化单元格的prepareLayout属性,换句话说,提供每个单元格的确切大小。我们知道宽度是常数,只有frame正在发生变化,因此在height的这一行中:

prepareLayout

我们从let cellHeight = delegate.collectionView(collectionView!, heightForItemAtIndexPath: indexPath, withWidth: width) 中实现的委托方法获取单元格的高度。对于我们想要在UICollectionViewController中显示的所有单元格,都会发生这种情况。获取并修改每个单元格的高度后,我们缓存结果,以便稍后检查。

然后,为了collectionView获取屏幕上每个单元格的大小,它需要做的就是查询缓存中的信息。这是在自定义collectionView类的layoutAttributesForElementsInRect方法中完成的。

此方法由UICollectionViewLayout自动调用。当UICollectionViewController需要进入屏幕的单元格的布局信息(例如,作为滚动的结果,或首次加载时),您将从您在{中填充的缓存中返回属性{1}}。

总结您的问题:在第2步中,我如何以及何时可以获得单元格大小?

答案:每个单元格大小都是在自定义UICollectionViewController的{​​{1}}方法中获得的,并且会在prepareLayout的生命周期的早期计算出来。

在第3步中,我如何以及何时通知更改的布局?

请注意,本教程不考虑在运行时添加的新单元格:

  

注意:由于在集合视图的布局无效时调用prepareLayout(),因此在典型实现中可能需要在此处重新计算属性。例如,UICollectionView的边界可能会更改 - 例如方向更改时 - 或者可以在集合中添加或删除项目。这些案例超出了本教程的范围,但在非平凡的实现中了解它们非常重要。

就像他写的那样,这可能是你需要的一个非常重要的实现。但是,如果您的数据集较小(或用于测试目的),则可能会采用一种简单(非常低效)的实现。当您因屏幕旋转或添加/删除单元格而需要使布局无效时,可以在自定义prepareLayout中清除缓存以强制UICollectionViewFlowLayout重新初始化布局属性。

例如,当您必须在collectionView上调用UICollectionView时,也要调用自定义布局类,以删除缓存:

UICollectionViewFlowLayout

答案 1 :(得分:2)

我意识到这不是一个完整的答案,但有关您的步骤2和3的一些指示可以在UICollectionViewLayout的子类注释中找到。

我认为你已经隐藏了UICollectionViewFlowLayout,因为我认为这是一个很好的起点,可以调整布局以获得你想要的交错外观。

对于第2步,layoutAttributesForElementsInRect(_:)应提供自定义单元格的布局属性。

对于第3步,您的布局将使用更改的单元格大小调用shouldInvalidateLayoutForPreferredLayoutAttributes(_:withOriginalAttributes:)

答案 2 :(得分:1)

First example of custom layout

在步骤2中,我如何以及何时可以获得单元格大小? 您需要在 prepareLayout()方法中计算每个单元格的高度。每个单元格的计算结果应分配给 UICollectionViewLayoutAttributes 变量,然后将其放入集合NSDictionary中,其中key为NSIndexPath(每个单元格),值为UICollectionViewLayoutAttributes变量。

示例:

- (void)prepareLayout {
    [_layoutMap removeAllObjects];
    _totalItemsInSection = [self.collectionView numberOfItemsInSection:0];
    _columnsYoffset = [self initialDataForColumnsOffsetY];

    if (_totalItemsInSection > 0 && self.totalColumns > 0) {
        [self calculateItemsSize];

        NSInteger itemIndex = 0;
        CGFloat contentSizeHeight = 0;

        while (itemIndex < _totalItemsInSection) {
            NSIndexPath *targetIndexPath = [NSIndexPath indexPathForItem:itemIndex inSection:0];
            NSInteger columnIndex = [self columnIndexForItemAtIndexPath:targetIndexPath];

            // you need to implement this method and perform your calculations
            CGRect attributeRect = [self calculateItemFrameAtIndexPath:targetIndexPath]; 
            UICollectionViewLayoutAttributes *targetLayoutAttributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:targetIndexPath];
            targetLayoutAttributes.frame = attributeRect;

            contentSizeHeight = MAX(CGRectGetMaxY(attributeRect), contentSizeHeight);
            _columnsYoffset[columnIndex] = @(CGRectGetMaxY(attributeRect) + self.interItemsSpacing);
            _layoutMap[targetIndexPath] = targetLayoutAttributes;

            itemIndex += 1;
        }

        _contentSize = CGSizeMake(self.collectionView.bounds.size.width - self.contentInsets.left - self.contentInsets.right,
                                  contentSizeHeight);
    }
}

不要忘记实施以下方法:

- (NSArray <UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
    NSMutableArray<UICollectionViewLayoutAttributes *> *layoutAttributesArray = [NSMutableArray new];

    for (UICollectionViewLayoutAttributes *layoutAttributes in _layoutMap.allValues) {
        if (CGRectIntersectsRect(layoutAttributes.frame, rect)) {
            [layoutAttributesArray addObject:layoutAttributes];
        }
    }

    return layoutAttributesArray;
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
    return _layoutMap[indexPath];
}

一旦调用reloadData()mehtod或invalidateLayout(),就会触发这些方法。

在第3步中,我如何以及何时通知更改的布局? 只需调用self.collectionView.collectionViewLayout.invalidateLayout()并再次调用prepareLayout()方法,即可重新计算所需的所有参数。

您可以在此处找到有关自定义UICollectionViewLayout的完整教程https://octodev.net/custom-collectionviewlayout/

教程包含两种语言的实现:Swift和Objective-C。 非常乐意回答你的所有问题。

答案 3 :(得分:0)

“单元格大小”由布局子类中的UICollectionViewLayoutAttribute定义,这意味着您可以在每次有机会触摸它时对其进行修改。您可以将每个属性的大小设置为您想要的大小。

例如,您可以在layoutAttributesOfElementsInRect(:)中执行此操作,计算正确的大小并配置所有属性,然后再将它们传递给collectionView。您也可以在layoutAttributeOfItemAtIndexPath(:)中执行此操作,在创建每个属性时进行计算。

此外,考虑通过数据源提供所需的大小,以便每个属性都可以轻松地获得其索引的大小。

如果您想要单元格大小来布局单元格中的子视图,请在collectionView委托方法中执行:collectionView:ItemAtIndexPath:

希望得到这个帮助。