Как разработать пользовательский UICollectionViewLayout, который имеет пошаговые столбцы с ячейками для самостоятельной калибровки?
Я работаю над версией iOS приложения, которое я уже разработал на Android. Это приложение имеет следующую 2-х столбчатую сетку с ячейками с фиксированной шириной, но с переменной высотой:
![Staggered 2 column layout on Android]()
Достижение этого в версии Android было легко, потому что Google предоставляет StaggeredGridLayoutManager
для своего RecyclerView
. Вы указываете количество столбцов и направление прокрутки, и вы закончили.
Макет UICollectionView
UICollectionViewFlowLayout
по умолчанию UICollectionViewFlowLayout
не позволяет разбивать макет, который я ищу, поэтому я должен реализовать собственный макет. Я смотрел 2 видео WWDC, которые рассказывают об этой теме (Что нового в представлении таблицы и коллекции и Расширенные пользовательские интерфейсы с коллекционными видами), и я более или менее имею представление о том, как это должно быть реализовано.
Шаг 1. Сначала вычисляется аппроксимация макета.
![введите описание изображения здесь]()
Шаг 2. Затем ячейки создаются и сортируются с автоопределением.
![введите описание изображения здесь]()
Шаг 3. Затем контроллер уведомляет размеры ячеек, чтобы макет обновлялся.
![введите описание изображения здесь]()
Мое сомнение возникает при попытке скопировать эти шаги. Я нашел tutorial, который объясняет создание настраиваемого макета с шагами в шахматном порядке, но он не использует автозапуск для получения размера клетки. Что оставляет меня со следующими вопросами:
На шаге 2, как и когда я могу получить размер ячейки?
На шаге 3 как и когда я могу уведомить макет изменений?
Ответы
Ответ 1
Хочу отметить, что, как вы уже упоминали, RayWenderlich PinInterest Layout - это именно то, что поможет вам достичь этого макета.
Чтобы ответить на ваши вопросы - в отношении учебника:
На шаге 2, как и когда я могу получить размер ячейки?
Чтобы получить высоту ячейки, был реализован метод делегата, который был вызван в методе prepareLayout
пользовательского UICollectionViewLayout
. Этот метод называется один раз (или дважды, я просто попытался запустить его с помощью оператора print
, и я получил два вызова). Точка prepareLayout
- это инициализировать свойство ячейки frame
, другими словами, предоставить точный размер каждой ячейки. Мы знаем, что ширина постоянна, и меняется только height
, поэтому в этой строке prepareLayout
:
let cellHeight = delegate.collectionView(collectionView!,
heightForItemAtIndexPath: indexPath, withWidth: width)
Мы получаем высоту ячейки из метода делегата, которая была реализована в UICollectionViewController
. Это происходит для всех ячеек, которые мы хотим показать в collectionView
. После получения и изменения высоты для каждой ячейки мы кэшируем результат, чтобы потом проверить его.
Затем для collectionView
для получения размера каждой ячейки на экране все, что нужно сделать, это запросить кеш для информации. Это делается в layoutAttributesForElementsInRect
методе вашего пользовательского класса UICollectionViewLayout
.
Этот метод вызывается автоматически UICollectionViewController
. Когда UICollectionViewController
требуется информация о расположении для ячеек, которые появляются на экране (в результате прокрутки, например, или при первой загрузке), вы возвращаете атрибуты из кэша, который вы заполнили в prepareLayout
.
В заключение на ваш вопрос: На шаге 2, как и когда я могу получить размер ячейки?
Ответ.. Каждый размер ячейки получается в методе prepareLayout
вашего пользовательского UICollectionViewFlowLayout
и вычисляется на раннем этапе жизненного цикла вашего UICollectionView
.
На шаге 3 как и когда я могу уведомить макет изменений?
Обратите внимание, что в учебнике не учитываются новые ячейки, которые необходимо добавить во время выполнения:
Примечание. Поскольку метод prepareLayout() вызывается всякий раз, когда маска представления коллекций является недействительной, в типичной реализации есть много ситуаций, где вам может потребоваться пересчитать атрибуты. Например, границы UICollectionView могут измениться - например, когда изменения ориентации - или элементы могут быть добавлены или удалены из коллекции. Эти случаи не подходят для этого урока, но важно знать о них в нетривиальной реализации.
Как он писал, это нетривиальная реализация, которая вам может понадобиться. Однако существует тривиальная (очень неэффективная) реализация, которую вы можете принять, если ваш набор данных невелик (или для целей тестирования). Когда вам нужно сделать недействительным макет из-за поворота экрана или добавления/удаления ячеек, вы можете очистить кеш в пользовательском UICollectionViewFlowLayout
, чтобы заставить prepareLayout
повторно инициализировать атрибуты макета.
Например, когда вы вызываете reloadData
в коллекции, также вызываете свой собственный класс макета, чтобы удалить кеш:
cache.removeAll()
Ответ 2
Я понимаю, что это не полный ответ, но некоторые указатели на ваши шаги 2 и 3 можно найти в примечаниях к подклассам для UICollectionViewLayout
.
Я предполагаю, что у вас есть подклассы UICollectionViewFlowLayout
, так как с моей точки зрения я считаю, что это хорошая отправная точка для внесения корректировок в макет, чтобы получить желаемый вид в шахматном порядке.
Для шага 2 layoutAttributesForElementsInRect(_:)
должны быть указаны атрибуты макета для ячеек собственного размера.
Для шага 3 ваш макет будет иметь shouldInvalidateLayoutForPreferredLayoutAttributes(_:withOriginalAttributes:)
с измененными размерами ячеек.
Ответ 3
![Первый пример пользовательского макета]()
На шаге 2, как и когда я могу получить размер ячейки?
Вам нужно рассчитать высоту каждой ячейки в методе prepareLayout(). Результат вычисления для каждой ячейки должен быть назначен переменной UICollectionViewLayoutAttributes, а затем помещен в сборник NSDictionary, где ключ будет 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.
Было бы более чем приятно ответить на все ваши вопросы.
Ответ 4
"Размер ячейки" определяется UICollectionViewLayoutAttribute в подклассе макета, что означает, что вы можете изменять его каждый раз, когда у вас есть возможность коснуться их. Вы можете установить размер каждого атрибута по своему желанию.
Например, вы можете сделать это в layoutAttributesOfElementsInRect (:), вычислить правильный размер и настроить все атрибуты, прежде чем передавать их в collectionView. Вы также можете сделать это в layoutAttributeOfItemAtIndexPath (:), сделать расчет при создании каждого атрибута.
Кроме того, подумайте о том, чтобы обеспечить требуемый размер с помощью источника данных, чтобы каждый атрибут мог легко получить свой размер с их индексом.
Для, если вы хотите, чтобы размер ячейки для макета отображал subviews в ячейке, сделайте это в методе делегата collectionView: collectionView: ItemAtIndexPath:
Надеюсь на эту помощь.