UICollectionView使边界更改上的布局无效

时间:2015-03-13 01:40:44

标签: ios objective-c

我目前有以下用于计算UICollectionViewCells尺寸的代码段:

- (CGSize)collectionView:(UICollectionView *)mainCollectionView
                  layout:(UICollectionViewLayout *)collectionViewLayout
  sizeForItemAtIndexPath:(NSIndexPath *)atIndexPath
{
    CGSize bounds = mainCollectionView.bounds.size;
    bounds.height /= 4;
    bounds.width /= 4;
    return bounds;
}

这很有效。但是,我现在在viewDidLoad中添加一个键盘观察器(它在UICollectionView出现之前触发了委托和数据源方法,并从故事板中调整自身大小)。因此,界限是错误的。我也想支持轮换。如果UICollectionView改变大小,处理这两个边缘情况并重新计算尺寸的好方法是什么?

5 个答案:

答案 0 :(得分:45)

当集合视图的边界发生更改时,使布局无效的解决方案是覆盖shouldInvalidateLayoutForBoundsChange:并返回YES。 它也在文档中说明:https://developer.apple.com/documentation/uikit/uicollectionviewlayout/1617781-shouldinvalidatelayoutforboundsc

- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds 
{
     return YES;
}

这也应该包括旋转支持。如果没有,请实施viewWillTransitionToSize:withTransitionCoordinator:

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
    [super viewWillTransitionToSize:size
          withTransitionCoordinator:coordinator];

    [coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context)
     {
         [self.collectionView.collectionViewLayout invalidateLayout];
     }
                                 completion:^(id<UIViewControllerTransitionCoordinatorContext> context)
     {
     }];
}

答案 1 :(得分:10)

  1. 您应该在更改集合视图大小时处理这种情况。如果您更改方向或约束,将触发viewWillLayoutSubviews方法。

  2. 您应该使当前集合视图布局无效。使用invalidateLayout方法使布局无效后,将触发UICollectionViewDelegateFlowLayout方法。

  3. 以下是示例代码:

    - (void)viewWillLayoutSubviews {
        [super viewWillLayoutSubviews];
        [mainCollectionView.collectionViewLayout invalidateLayout];
    }
    

答案 2 :(得分:1)

此方法允许您执行此操作而无需将布局子类化,而是将其添加到您可能已经存在的UICollectionViewController子类中,并且避免了递归调用viewWillLayoutSubviews的可能性,这是已接受的变体解决方案,略有简化,因为它不使用transitionCoordinator。在Swift中:

override func viewWillTransition(
    to size: CGSize,
    with coordinator: UIViewControllerTransitionCoordinator
) {
    super.viewWillTransition(to: size, with: coordinator)
    collectionViewLayout.invalidateLayout()
}

答案 3 :(得分:0)

尽管@tubtub的答案是有效的,但有些人可能会遇到以下错误:The behaviour of the UICollectionViewFlowLayout is not defined

首先,请记住在shouldInvalidateLayout类中覆盖CustomLayout。 (下面的示例)

然后,您应该根据新布局考虑视图中所有元素的大小是否已更改(请参见示例代码中的可选步骤)。

以下是以下代码,可以帮助您入门。根据创建UI的方式,您可能必须尝试找到合适的视图来调用recalculate方法,但这应该可以指导您迈出第一步。

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {

    super.viewWillTransition(to: size, with: coordinator)

    /// (Optional) Additional step 1. Depending on your layout, you may have to manually indicate that the content size of a visible cells has changed
    /// Use that step if you experience the `the behavior of the UICollectionViewFlowLayout is not defined` errors.

    collectionView.visibleCells.forEach { cell in
        guard let cell = cell as? CustomCell else {
            print("`viewWillTransition` failed. Wrong cell type")
            return
        }

        cell.recalculateFrame(newSize: size)

    }

    /// (Optional) Additional step 2. Recalculate layout if you've explicitly set the estimatedCellSize and you'll notice that layout changes aren't automatically visible after the #3

    (collectionView.collectionViewLayout as? CustomLayout)?.recalculateLayout(size: size)


    /// Step 3 (or 1 if none of the above is applicable)

    coordinator.animate(alongsideTransition: { context in
        self.collectionView.collectionViewLayout.invalidateLayout()
    }) { _ in
        // code to execute when the transition's finished.
    }

}

/// Example implementations of the `recalculateFrame` and `recalculateLayout` methods:

    /// Within the `CustomCell` class:
    func recalculateFrame(newSize: CGSize) {
        self.frame = CGRect(x: self.bounds.origin.x,
                            y: self.bounds.origin.y,
                            width: newSize.width - 14.0,
                            height: self.frame.size.height)
    }

    /// Within the `CustomLayout` class:
    func recalculateLayout(size: CGSize? = nil) {
        estimatedItemSize = CGSize(width: size.width - 14.0, height: 100)
    }

    /// IMPORTANT: Within the `CustomLayout` class.
    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {

        guard let collectionView = collectionView else {
            return super.shouldInvalidateLayout(forBoundsChange: newBounds)
        }

        if collectionView.bounds.width != newBounds.width || collectionView.bounds.height != newBounds.height {
            return true
        } else {
            return false
        }
    }

答案 4 :(得分:0)

在UICollectionView的边界更改或方向更改时使用FlowLayout调整项目大小/布局的步骤。

  1. 子类UICollectionViewFlowLayout
  2. 覆盖shouldInvalidateLayout(forBoundsChange:)以返回true。您可以使用此方法来确定当特定范围发生更改时,此布局是否应自动调用invalidateLayout(with:)。基类UICollectionViewLayout总是返回false,也就是说,在边界改变时不会使任何内容失效。但是,UICollectionViewFlowLayout有时会返回true来完成自己的工作。 要违反现有功能,您一开始会叫super,只有在super返回false时,您才做额外的工作。
  3. 覆盖invalidateLayout(with:),以控制要完全无效的内容。 context参数管理有关此布局的所有详细信息。对于FlowLayout,它提供了其他控件,包括invalidateFlowLayoutDelegateMetrics(是否要调整项目的大小,即通过调用collectionView(:layout:sizeForItemAt:)重新计算项目的大小)和invalidateFlowLayoutAttributes(是否要重新分发项目,例如,重新计算项目的位置)。您可以在调用super之前操纵此context

示例代码(宽度更改时调整大小和重新布局项目)。

final class FlowLayout: UICollectionViewFlowLayout {
    
    /// A cache for `newBounds` parameter in `shouldInvalidateLayout(forBoundsChange:)`.
    var bounds: CGRect?
    
    /// Should we invalidate both size and layout information for next layout.
    var invalidatesAll = false
    
    override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) {
        guard let flowContext = context as? UICollectionViewFlowLayoutInvalidationContext else {
            return
        }
        if invalidatesAll {
            // Tell layout to recompute the size of items
            flowContext.invalidateFlowLayoutDelegateMetrics = true
            // Tell layout to recompute the layout of items
            flowContext.invalidateFlowLayoutAttributes = true
            // Reset this flag, because we do this only if there is a width change.
            invalidatesAll = false
        }
        super.invalidateLayout(with: flowContext)
    }
    
    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        if super.shouldInvalidateLayout(forBoundsChange: newBounds) {
            // If super (flowlayout) has already determined an invalidation, we simply hand over.
            return true
        }
        
        // We make sure there is a width change to avoid unnecessory invalidations.
        // Note: for the first time, `bounds` is always nil and flow layout always do first layout automatically, so we want to avoid this too.
        let isWidthChanged = bounds != nil && newBounds.size.width != bounds?.size.width
        if isWidthChanged {
            // A width change happens!, and we want to recompute both size and layout on width change.
            invalidatesAll = true
        }
        bounds = newBounds
        return isWidthChanged
    }
}