Пользовательские UICollectionViewLayout с ячейками автоматического калибровки разрываются с более высокими оцененными высотами элементов
Я создаю пользовательский UICollectionViewLayout
который поддерживает автоматическую калибровку ячеек, и я столкнулся с проблемой, когда расчетная высота элемента больше конечной высоты. Когда предпочтительные атрибуты макета запускают частичную недействительность, некоторые ячейки ниже становятся видимыми, не все из них получают правильные фреймы.
На изображении ниже левый скриншот показывает начальный рендеринг с большой оцененной высотой, а на правом изображении показано, где расчетная высота меньше конечной высоты.
Эта проблема возникает в IOS 10 и 11.
С меньшей оцененной высотой размер содержимого увеличивается во время компоновки, а предпочтительные атрибуты раскладки не приводят к тому, что больше элементов перемещается в видимый прямоугольник. Представление коллекции прекрасно справляется с этой ситуацией.
Логика вычисления недействительности и кадра представляется действительной, поэтому я не уверен, почему представление коллекции не обрабатывает случай, когда частичная недействительность приводит к появлению новых элементов.
При более глубоком анализе кажется, что окончательные мнения, которые должны быть отобраны в поле зрения, становятся недействительными и просят рассчитать их размер, но их конечные атрибуты не применяются.
Здесь код макета очень урезанной версии пользовательского макета для демонстрационных целей, который показывает этот сбой:
/// Simple demo layout, only 1 section is supported
/// This is not optimised, it is purely a simplified version
/// of a more complex custom layout that demonstrates
/// the glitch.
public class Layout: UICollectionViewLayout {
public var estimatedItemHeight: CGFloat = 50
public var spacing: CGFloat = 10
var contentWidth: CGFloat = 0
var numberOfItems = 0
var heightCache = [Int: CGFloat]()
override public func prepare() {
super.prepare()
self.contentWidth = self.collectionView?.bounds.width ?? 0
self.numberOfItems = self.collectionView?.numberOfItems(inSection: 0) ?? 0
}
override public var collectionViewContentSize: CGSize {
// Get frame for last item an duse maxY
let lastItemIndex = self.numberOfItems - 1
let contentHeight = self.frame(for: IndexPath(item: lastItemIndex, section: 0)).maxY
return CGSize(width: self.contentWidth, height: contentHeight)
}
override public func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// Not optimal but works, get all frames for all items and calculate intersection
let attributes: [UICollectionViewLayoutAttributes] = (0 ..< self.numberOfItems)
.map { IndexPath(item: $0, section: 0) }
.compactMap { indexPath in
let frame = self.frame(for: indexPath)
guard frame.intersects(rect) else {
return nil
}
let attributesForItem = self.layoutAttributesForItem(at: indexPath)
return attributesForItem
}
return attributes
}
override public func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = self.frame(for: indexPath)
return attributes
}
public func frame(for indexPath: IndexPath) -> CGRect {
let heightsTillNow: CGFloat = (0 ..< indexPath.item).reduce(0) {
return $0 + self.spacing + (self.heightCache[$1] ?? self.estimatedItemHeight)
}
let height = self.heightCache[indexPath.item] ?? self.estimatedItemHeight
let frame = CGRect(
x: 0,
y: heightsTillNow,
width: self.contentWidth,
height: height
)
return frame
}
override public func shouldInvalidateLayout(forPreferredLayoutAttributes preferredAttributes: UICollectionViewLayoutAttributes, withOriginalAttributes originalAttributes: UICollectionViewLayoutAttributes) -> Bool {
let index = originalAttributes.indexPath.item
let shouldInvalidateLayout = self.heightCache[index] != preferredAttributes.size.height
return shouldInvalidateLayout
}
override public func invalidationContext(forPreferredLayoutAttributes preferredAttributes: UICollectionViewLayoutAttributes, withOriginalAttributes originalAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutInvalidationContext {
let context = super.invalidationContext(forPreferredLayoutAttributes: preferredAttributes, withOriginalAttributes: originalAttributes)
let index = originalAttributes.indexPath.item
let oldContentSize = self.collectionViewContentSize
self.heightCache[index] = preferredAttributes.size.height
let newContentSize = self.collectionViewContentSize
let contentSizeDelta = newContentSize.height - oldContentSize.height
context.contentSizeAdjustment = CGSize(width: 0, height: contentSizeDelta)
// Everything underneath has to be invalidated
let indexPaths: [IndexPath] = (index ..< self.numberOfItems).map {
return IndexPath(item: $0, section: 0)
}
context.invalidateItems(at: indexPaths)
return context
}
}
Здесь ячейка предпочитает вычисление атрибутов макета (обратите внимание, что мы разрешаем макету решать и фиксируем ширину, и мы просим автозапуск рассчитать высоту ячейки, заданной шириной).
public class Cell: UICollectionViewCell {
// ...
public override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
let finalWidth = layoutAttributes.bounds.width
// With the fixed width given by layout, calculate the height using autolayout
let finalHeight = systemLayoutSizeFitting(
CGSize(width: finalWidth, height: 0),
withHorizontalFittingPriority: .required,
verticalFittingPriority: .fittingSizeLevel
).height
let finalSize = CGSize(width: finalWidth, height: finalHeight)
layoutAttributes.size = finalSize
return layoutAttributes
}
}
Есть ли что-то очевидное, что вызывает это в логике компоновки?
Ответы
Ответ 1
Я продублировал проблему с помощью набора
estimatedItemHeight = 500
в демо-коде. У меня вопрос о вашей логике для вычисления фрейма для каждой ячейки: вся высота в self.heightCache равна нулю, поэтому утверждение
return $0 + self.spacing + (self.heightCache[$1] ?? self.estimatedItemHeight)
в рамке функции такая же, как и
return $0 + self.spacing + self.estimatedItemHeight
Я думаю, может быть, вы должны проверить этот код
self.heightCache[index] = preferredAttributes.size.height
в функции
invalidationContext(forPreferredLayoutAttributes preferredAttributes: UICollectionViewLayoutAttributes, withOriginalAttributes originalAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutInvalidationContext
как preferredAttributes.size.height всегда равен нулю
и
finalHeight
также равно нулю в классе
клетка
Ответ 2
Я также пытаюсь создать пользовательский подкласс UICollectionViewLayout
для макета UITableView
-style, и я сталкиваюсь с немного другой проблемой. Но я обнаружил, что в shouldInvalidateLayoutForPreferredLayoutAttributes
, если вы вернетесь в зависимости от того, соответствует ли предпочтительная высота исходной высоте (а не соответствует ли высота, соответствующая высоте вашего кеша), она будет правильно применять атрибуты макета, и все ваши ячейки будут имеют правильную высоту.
Но тогда вы получаете недостающие ячейки, потому что вы не всегда получаете layoutAttributesForElementsInRect
на layoutAttributesForElementsInRect
после того, как произошла автоматическая калибровка, и изменили высоты вашей ячейки (и, следовательно, позиции y).
См. Пример проекта здесь, на GitHub.
Изменение: на мой вопрос ответили, и пример GitHub теперь работает.