如何在具有动态尺寸的单元格的UICollectionViewFlowLayout子类上正确设置节插图

时间:2019-03-09 14:23:11

标签: ios swift uicollectionview

我的目标是在UICollectionView中具有动态大小的单元格。另外,当contentWidth的可滚动UICollectionView小于Collection的bounds时,我希望项目在集合内居中。

Goal

到目前为止的方法:

我正在尝试以最干净的方式进行此操作。我有自动调整大小的UICollectionViewCells,其中“自动布局”可以根据内容的约束确定单元格的大小。为此,我的单元将覆盖preferredLayoutAttributesFitting函数,并为其内容返回适当的宽度。

我的自定义UICollectionViewFlowLayout子类使用UICollectionViewCell提供的宽度信息来适当地调整单元格的大小。

在我的自定义UICollectionViewFlowLayout上唯一覆盖的方法是:

  • layoutAttributesForItem(at...)
  • layoutAttributesForElementsIn(rect...)
  • shouldInvalidateLayout(forBoundsChange...)

动态调整UICollectionViewCells的大小是完美的。

问题:

使用这种方法,我对每个单元的大小没有先验知识(每个单元可以不同)。我想实现UICollectionView的{​​{1}}方法来提供插图,以使我的内容在收藏夹视图中居中。为此,我认为我应该实现并覆盖

delegate

问题在于inset方法是在布局过程的早期调用的,我不知道在布局过程中将确定的collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets

这里是各种布局方法的堆栈,用于一次完整的布局遍历,按它们运行的​​顺序进行:

contentSize

对于一次布局传递,将多次调用各种布局方法,但是直到最后,才可以从不同单元格获得来自自动布局的尺寸信息,以传递到布局。

我注意到prepare started prepare finished layoutAttributesForElementsInRect started layoutAttributesForElementsInRect finished layoutAttributesForElementsInRect started layoutAttributesForElementsInRect finished layoutAttributesForElementsInRect started layoutAttributesForElementsInRect finished prepare started insetForSectionAt 0 called <--- The datasource has knowledge of the presence of cells but the cells themselves are not yet dequeued prepare finished layoutAttributesForElementsInRect started layoutAttributesForItemAt [0, 0] started layoutAttributesForItemAt [0, 0] finished layoutAttributesForItemAt [0, 1] started layoutAttributesForItemAt [0, 1] finished layoutAttributesForItemAt [0, 2] started layoutAttributesForItemAt [0, 2] finished layoutAttributesForElementsInRect finished layoutAttributesForElementsInRect started layoutAttributesForItemAt [0, 0] started layoutAttributesForItemAt [0, 0] finished layoutAttributesForItemAt [0, 1] started layoutAttributesForItemAt [0, 1] finished layoutAttributesForItemAt [0, 2] started layoutAttributesForItemAt [0, 2] finished layoutAttributesForElementsInRect finished prepare started prepare finished layoutAttributesForElementsInRect started <--- At this point Cells DO return valid sizes through Autolayout layoutAttributesForItemAt [0, 0] started layoutAttributesForItemAt [0, 0] finished layoutAttributesForItemAt [0, 1] started layoutAttributesForItemAt [0, 1] finished layoutAttributesForItemAt [0, 2] started layoutAttributesForItemAt [0, 2] finished layoutAttributesForElementsInRect finished 委托函数在单元正确地出队并允许其自动布局约束运行之前就被调用。因此,不可能在调用插图时将正确的值告知插图。

使用这种方法,是否有可能在布局通过后强制调用重新计算sectionInset还是我应该完全改变技术?

我尝试过的事情:

我尝试从sectionInsetAt委托函数中查询collectionViewLayout的{​​{1}}。不幸的是,由于布局正在进行中,因此无法在布局过程中使用。它会导致崩溃并显示错误:

collectionViewContentSize

我尝试使用insetForSectionAt的{​​{1}}方法在执行[CollectionView] An attempt to update layout information was detected while already in the process of computing the layout (i.e. reentrant call). This will result in unexpected behaviour or a crash. This may happen if a layout pass is triggered while calling out to a delegate. UICollectionViewFlowLayout instance is (<App.ViewportToolbarFlowLayout: 0x7f8a5261add0>) 委托方法的过程中获得大小,但是在运行时它无法提供正确的数据在布局过程中。

1 个答案:

答案 0 :(得分:0)

在此处发布答案。我发现Alex Koshy的this answer对我有很大帮助。

基本上,该工作必须在layoutAttributesForElementsIn(rect...)中进行,因为它是唯一可获得完整版式图片的地方。我已经调整了Alex的代码以处理水平UICollectionViews和垂直scrollDirection(可能不是每个人都想要的,但正是我所需要的)。我使用布局的scrollDirection属性来确定元素是水平居中(当.horizontalscrollDirection时还是垂直居中(当.verticaloverride func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { guard let collectionView = collectionView else { return nil } guard let superLayoutAttributes = super.layoutAttributesForElements(in: rect) else { return nil } let computedAttributes = superLayoutAttributes.compactMap { layoutAttribute -> UICollectionViewLayoutAttributes? in return layoutAttribute.representedElementCategory == .cell ? layoutAttributesForItem(at: layoutAttribute.indexPath) : layoutAttribute } guard let attributes = NSArray(array: computedAttributes, copyItems: true) as? [UICollectionViewLayoutAttributes] else { return nil } let interItemSpacing = minimumInteritemSpacing switch scrollDirection { case .horizontal: let leftPadding: CGFloat = self.sectionInset.left var leftMargin: CGFloat = leftPadding // Modified to determine origin.x for each item var maxY: CGFloat = -1.0 // Modified to determine origin.y for each item var rowSizes: [[CGFloat]] = [] // Tracks the starting and ending x-values for the first and last item in the row var currentRow: Int = 0 // Tracks the current row attributes.forEach { layoutAttribute in // Each layoutAttribute represents its own item if layoutAttribute.frame.origin.y >= maxY { // This layoutAttribute represents the left-most item in the row leftMargin = leftPadding currentRow += rowSizes.isEmpty ? 0 : 1 // Register its origin.x in rowSizes for use later rowSizes.append([leftMargin, 0]) } layoutAttribute.frame.origin.x = leftMargin leftMargin += layoutAttribute.frame.width + interItemSpacing maxY = max(layoutAttribute.frame.maxY, maxY) // Add right-most x value for last item in the row rowSizes[currentRow][1] = leftMargin - interItemSpacing } // At this point, all cells are left aligned // Reset tracking values and add extra left padding to center align entire row leftMargin = leftPadding maxY = -1.0 currentRow = 0 attributes.forEach { (layoutAttribute) in if layoutAttribute.frame.origin.y >= maxY { // This layoutAttribute represents the left-most item in the row leftMargin = leftPadding // Need to bump it up by an appended margin let rowWidth = rowSizes[currentRow][1] - rowSizes[currentRow][0] // last.x - first.x let appendedMargin = (collectionView.frame.width - leftPadding - rowWidth - leftPadding) / 2 leftMargin += appendedMargin currentRow += 1 } layoutAttribute.frame.origin.x = leftMargin leftMargin += layoutAttribute.frame.width + interItemSpacing maxY = max(layoutAttribute.frame.maxY, maxY) } case .vertical: let topPadding: CGFloat = self.sectionInset.top var topMargin: CGFloat = topPadding var maxX: CGFloat = -1.0 // Modified to determine origine.x for each item var colSizes: [[CGFloat]] = [] // Tracks the starting and ending y-values for the first and last item in the column var currentCol: Int = 0 attributes.forEach { (layoutAttribute) in if layoutAttribute.frame.origin.x >= maxX { topMargin = topPadding currentCol += colSizes.isEmpty ? 0 : 1 colSizes.append([topMargin, 0]) } layoutAttribute.frame.origin.y = topMargin topMargin += layoutAttribute.frame.height + interItemSpacing maxX = max(layoutAttribute.frame.maxX, maxX) colSizes[currentCol][1] = topMargin - interItemSpacing } topMargin = topPadding maxX = -1.0 currentCol = 0 attributes.forEach { (layoutAttribute) in if layoutAttribute.frame.origin.x >= maxX { topMargin = topPadding let colWidth = colSizes[currentCol][1] - colSizes[currentCol][0] let appendedMargin = (collectionView.frame.height - topPadding - colWidth - topPadding) / 2 topMargin += appendedMargin currentCol += 1 } layoutAttribute.frame.origin.y = topMargin topMargin += layoutAttribute.frame.height + interItemSpacing maxX = max(layoutAttribute.frame.maxX, maxX) } } return attributes } 时)

代码如下:

addToFavorites({ state }, payload) {
firebase
.database()
.ref('users/' + state.user.user.uid + '/favorites')
.once('value', snapshot => {
    if (snapshot.exists()) {
        firebase
            .database()
            .ref('users')
            .child(state.user.user.uid + '/favorites')
            .push(payload);
    }
});