Ответ 1
Вы правы - это не то, как изображение коллекции с горизонтальной прокруткой запаса кладет ячейки. Я боюсь, что вам придется реализовать свой собственный подкласс UICollectionViewLayout
. Либо это, либо отделить ваши модели от секций.
Это связано с но отличается от , чтобы использовать компоновку потока или настроить?.
Вот иллюстрация того, что я пытаюсь сделать:
Мне интересно, могу ли я сделать это с помощью UICollectionViewFlowLayout
, его подкласса или мне нужно создать полностью настраиваемый макет? Основываясь на видео WWDC 2012 на UICollectionView, кажется, что если вы используете Flow Layout с вертикальной прокруткой, ваши линии компоновки горизонтальны, и если вы прокручиваете горизонтально, ваши линии макета вертикальны. Я хочу, чтобы горизонтальные макеты отображались в виде горизонтальной прокрутки.
У меня также нет каких-либо неотъемлемых разделов в моей модели - это всего лишь один набор элементов. Я мог бы сгруппировать их по разделам, но просмотр коллекции изменен, поэтому количество элементов, которые могут поместиться на странице, иногда менялось бы, и кажется, что выбор каждой страницы для каждого элемента лучше оставить на макете, чем на модель, если у меня нет каких-либо значимых разделов.
Итак, могу ли я сделать это с помощью Flow Layout, или мне нужно создать собственный макет?
Вы правы - это не то, как изображение коллекции с горизонтальной прокруткой запаса кладет ячейки. Я боюсь, что вам придется реализовать свой собственный подкласс UICollectionViewLayout
. Либо это, либо отделить ваши модели от секций.
Здесь я разделяю мою простую реализацию!
Файл .h:
/**
* CollectionViewLayout for an horizontal flow type:
*
* | 0 1 | 6 7 |
* | 2 3 | 8 9 | ----> etc...
* | 4 5 | 10 11 |
*
* Only supports 1 section and no headers, footers or decorator views.
*/
@interface HorizontalCollectionViewLayout : UICollectionViewLayout
@property (nonatomic, assign) CGSize itemSize;
@end
Файл .m:
@implementation HorizontalCollectionViewLayout
{
NSInteger _cellCount;
CGSize _boundsSize;
}
- (void)prepareLayout
{
// Get the number of cells and the bounds size
_cellCount = [self.collectionView numberOfItemsInSection:0];
_boundsSize = self.collectionView.bounds.size;
}
- (CGSize)collectionViewContentSize
{
// We should return the content size. Lets do some math:
NSInteger verticalItemsCount = (NSInteger)floorf(_boundsSize.height / _itemSize.height);
NSInteger horizontalItemsCount = (NSInteger)floorf(_boundsSize.width / _itemSize.width);
NSInteger itemsPerPage = verticalItemsCount * horizontalItemsCount;
NSInteger numberOfItems = _cellCount;
NSInteger numberOfPages = (NSInteger)ceilf((CGFloat)numberOfItems / (CGFloat)itemsPerPage);
CGSize size = _boundsSize;
size.width = numberOfPages * _boundsSize.width;
return size;
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
// This method requires to return the attributes of those cells that intsersect with the given rect.
// In this implementation we just return all the attributes.
// In a better implementation we could compute only those attributes that intersect with the given rect.
NSMutableArray *allAttributes = [NSMutableArray arrayWithCapacity:_cellCount];
for (NSUInteger i=0; i<_cellCount; ++i)
{
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
UICollectionViewLayoutAttributes *attr = [self _layoutForAttributesForCellAtIndexPath:indexPath];
[allAttributes addObject:attr];
}
return allAttributes;
}
- (UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
return [self _layoutForAttributesForCellAtIndexPath:indexPath];
}
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
// We should do some math here, but we are lazy.
return YES;
}
- (UICollectionViewLayoutAttributes*)_layoutForAttributesForCellAtIndexPath:(NSIndexPath*)indexPath
{
// Here we have the magic of the layout.
NSInteger row = indexPath.row;
CGRect bounds = self.collectionView.bounds;
CGSize itemSize = self.itemSize;
// Get some info:
NSInteger verticalItemsCount = (NSInteger)floorf(bounds.size.height / itemSize.height);
NSInteger horizontalItemsCount = (NSInteger)floorf(bounds.size.width / itemSize.width);
NSInteger itemsPerPage = verticalItemsCount * horizontalItemsCount;
// Compute the column & row position, as well as the page of the cell.
NSInteger columnPosition = row%horizontalItemsCount;
NSInteger rowPosition = (row/horizontalItemsCount)%verticalItemsCount;
NSInteger itemPage = floorf(row/itemsPerPage);
// Creating an empty attribute
UICollectionViewLayoutAttributes *attr = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
CGRect frame = CGRectZero;
// And finally, we assign the positions of the cells
frame.origin.x = itemPage * bounds.size.width + columnPosition * itemSize.width;
frame.origin.y = rowPosition * itemSize.height;
frame.size = _itemSize;
attr.frame = frame;
return attr;
}
#pragma mark Properties
- (void)setItemSize:(CGSize)itemSize
{
_itemSize = itemSize;
[self invalidateLayout];
}
@end
И, наконец, если вы хотите постраничное поведение, вам просто нужно настроить свой UICollectionView:
_collectionView.pagingEnabled = YES;
В надежде быть достаточно полезным.
Конвертированный код vilanovi в Swift на случай, если он кому-то понадобится, в будущем.
public class HorizontalCollectionViewLayout : UICollectionViewLayout {
private var cellWidth = 90 // Don't kow how to get cell size dynamically
private var cellHeight = 90
public override func prepareLayout() {
}
public override func collectionViewContentSize() -> CGSize {
let numberOfPages = Int(ceilf(Float(cellCount) / Float(cellsPerPage)))
let width = numberOfPages * Int(boundsWidth)
return CGSize(width: CGFloat(width), height: boundsHeight)
}
public override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
var allAttributes = [UICollectionViewLayoutAttributes]()
for (var i = 0; i < cellCount; ++i) {
let indexPath = NSIndexPath(forRow: i, inSection: 0)
let attr = createLayoutAttributesForCellAtIndexPath(indexPath)
allAttributes.append(attr)
}
return allAttributes
}
public override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! {
return createLayoutAttributesForCellAtIndexPath(indexPath)
}
public override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
return true
}
private func createLayoutAttributesForCellAtIndexPath(indexPath:NSIndexPath)
-> UICollectionViewLayoutAttributes {
let layoutAttributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
layoutAttributes.frame = createCellAttributeFrame(indexPath.row)
return layoutAttributes
}
private var boundsWidth:CGFloat {
return self.collectionView!.bounds.size.width
}
private var boundsHeight:CGFloat {
return self.collectionView!.bounds.size.height
}
private var cellCount:Int {
return self.collectionView!.numberOfItemsInSection(0)
}
private var verticalCellCount:Int {
return Int(floorf(Float(boundsHeight) / Float(cellHeight)))
}
private var horizontalCellCount:Int {
return Int(floorf(Float(boundsWidth) / Float(cellWidth)))
}
private var cellsPerPage:Int {
return verticalCellCount * horizontalCellCount
}
private func createCellAttributeFrame(row:Int) -> CGRect {
let frameSize = CGSize(width:cellWidth, height: cellHeight )
let frameX = calculateCellFrameHorizontalPosition(row)
let frameY = calculateCellFrameVerticalPosition(row)
return CGRectMake(frameX, frameY, frameSize.width, frameSize.height)
}
private func calculateCellFrameHorizontalPosition(row:Int) -> CGFloat {
let columnPosition = row % horizontalCellCount
let cellPage = Int(floorf(Float(row) / Float(cellsPerPage)))
return CGFloat(cellPage * Int(boundsWidth) + columnPosition * Int(cellWidth))
}
private func calculateCellFrameVerticalPosition(row:Int) -> CGFloat {
let rowPosition = (row / horizontalCellCount) % verticalCellCount
return CGFloat(rowPosition * Int(cellHeight))
}
}
Предыдущая реализация не была полной, ошибкой и фиксированным размером ячейки. Здесь более буквальный перевод кода:
import UIKit
class HorizontalFlowLayout: UICollectionViewLayout {
var itemSize = CGSizeZero {
didSet {
invalidateLayout()
}
}
private var cellCount = 0
private var boundsSize = CGSizeZero
override func prepareLayout() {
cellCount = self.collectionView!.numberOfItemsInSection(0)
boundsSize = self.collectionView!.bounds.size
}
override func collectionViewContentSize() -> CGSize {
let verticalItemsCount = Int(floor(boundsSize.height / itemSize.height))
let horizontalItemsCount = Int(floor(boundsSize.width / itemSize.width))
let itemsPerPage = verticalItemsCount * horizontalItemsCount
let numberOfItems = cellCount
let numberOfPages = Int(ceil(Double(numberOfItems) / Double(itemsPerPage)))
var size = boundsSize
size.width = CGFloat(numberOfPages) * boundsSize.width
return size
}
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var allAttributes = [UICollectionViewLayoutAttributes]()
for var i = 0; i < cellCount; i++ {
let indexPath = NSIndexPath(forRow: i, inSection: 0)
let attr = self.computeLayoutAttributesForCellAtIndexPath(indexPath)
allAttributes.append(attr)
}
return allAttributes
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
return self.computeLayoutAttributesForCellAtIndexPath(indexPath)
}
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
return true
}
func computeLayoutAttributesForCellAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes {
let row = indexPath.row
let bounds = self.collectionView!.bounds
let verticalItemsCount = Int(floor(boundsSize.height / itemSize.height))
let horizontalItemsCount = Int(floor(boundsSize.width / itemSize.width))
let itemsPerPage = verticalItemsCount * horizontalItemsCount
let columnPosition = row % horizontalItemsCount
let rowPosition = (row/horizontalItemsCount)%verticalItemsCount
let itemPage = Int(floor(Double(row)/Double(itemsPerPage)))
let attr = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
var frame = CGRectZero
frame.origin.x = CGFloat(itemPage) * bounds.size.width + CGFloat(columnPosition) * itemSize.width
frame.origin.y = CGFloat(rowPosition) * itemSize.height
frame.size = itemSize
attr.frame = frame
return attr
}
}
Можно просто изменить направление прокрутки в UICollectionView.xib на горизонтальное. И используйте с правильным порядком элементов в массиве.
Это Swift 3 версия @GuilhermeSprint ответ
public class HorizontalCollectionViewLayout : UICollectionViewLayout {
var itemSize = CGSize(width: 0, height: 0) {
didSet {
invalidateLayout()
}
}
private var cellCount = 0
private var boundsSize = CGSize(width: 0, height: 0)
public override func prepare() {
cellCount = self.collectionView!.numberOfItems(inSection: 0)
boundsSize = self.collectionView!.bounds.size
}
public override var collectionViewContentSize: CGSize {
let verticalItemsCount = Int(floor(boundsSize.height / itemSize.height))
let horizontalItemsCount = Int(floor(boundsSize.width / itemSize.width))
let itemsPerPage = verticalItemsCount * horizontalItemsCount
let numberOfItems = cellCount
let numberOfPages = Int(ceil(Double(numberOfItems) / Double(itemsPerPage)))
var size = boundsSize
size.width = CGFloat(numberOfPages) * boundsSize.width
return size
}
public override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var allAttributes = [UICollectionViewLayoutAttributes]()
for i in 0...(cellCount-1) {
let indexPath = IndexPath(row: i, section: 0)
let attr = self.computeLayoutAttributesForCellAt(indexPath: indexPath)
allAttributes.append(attr)
}
return allAttributes
}
public override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return computeLayoutAttributesForCellAt(indexPath: indexPath)
}
public override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
private func computeLayoutAttributesForCellAt(indexPath:IndexPath)
-> UICollectionViewLayoutAttributes {
let row = indexPath.row
let bounds = self.collectionView!.bounds
let verticalItemsCount = Int(floor(boundsSize.height / itemSize.height))
let horizontalItemsCount = Int(floor(boundsSize.width / itemSize.width))
let itemsPerPage = verticalItemsCount * horizontalItemsCount
let columnPosition = row % horizontalItemsCount
let rowPosition = (row/horizontalItemsCount)%verticalItemsCount
let itemPage = Int(floor(Double(row)/Double(itemsPerPage)))
let attr = UICollectionViewLayoutAttributes(forCellWith: indexPath)
var frame = CGRectMake(0, 0, 0, 0)
frame.origin.x = CGFloat(itemPage) * bounds.size.width + CGFloat(columnPosition) * itemSize.width
frame.origin.y = CGFloat(rowPosition) * itemSize.height
frame.size = itemSize
attr.frame = frame
return attr
}
}
// I want to have 4 items in the page / see screenshot below
let itemWidth = collectionView.frame.width / 2.0
let itemHeight = collectionView.frame.height / 2.0
let horizontalCV = HorizontalCollectionViewLayout();
horizontalCV.itemSize = CGSize(width: itemWidth, height: itemHeight)
collectionView.collectionViewLayout = horizontalCV
extension MyViewController : UICollectionViewDelegateFlowLayout, UICollectionViewDataSource{
// Delegate
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("Clicked")
}
// DataSource
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: BottomMenuCCell.xib, for: indexPath) as? BottomMenuCCell {
cell.ibi = bottomMenuButtons[indexPath.row]
cell.layer.borderWidth = 0
return cell
}
return BaseCollectionCell()
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize.init(width: (collectionView.width / 2.0), height: collectionView.height / 2.0)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return bottomMenuButtons.count
}
// removing spacing between items
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 0.0
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 0.0
}
}
В последнем случае, конечно, было бы использовать несколько вертикальных коллекционных представлений внутри каждого раздела во внешнем представлении коллекции с горизонтальной прокруткой. Помимо увеличения сложности кода и сложности при выполнении анимации межсетевых ячеек, я не могу думать о серьезных проблемах с этим подходом прямо с моей головы.
@interface HorizontalCollectionViewLayout : UICollectionViewFlowLayout
@end
@implementation HorizontalCollectionViewLayout
- (instancetype)init
{
self = [super init];
if (self) {
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.minimumLineSpacing = 0;
self.minimumInteritemSpacing = 0;
}
return self;
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray *attributesArray = [super layoutAttributesForElementsInRect:rect];
NSInteger verticalItemsCount = (NSInteger)floorf(self.collectionView.bounds.size.height / self.itemSize.height);
NSInteger horizontalItemsCount = (NSInteger)floorf(self.collectionView.bounds.size.width / self.itemSize.width);
NSInteger itemsPerPage = verticalItemsCount * horizontalItemsCount;
for (NSInteger i = 0; i < attributesArray.count; i++) {
UICollectionViewLayoutAttributes *attributes = attributesArray[i];
NSInteger currentPage = (NSInteger)floor((double)i / (double)itemsPerPage);
NSInteger currentRow = (NSInteger)floor((double)(i - currentPage * itemsPerPage) / (double)horizontalItemsCount);
NSInteger currentColumn = i % horizontalItemsCount;
CGRect frame = attributes.frame;
frame.origin.x = self.itemSize.width * currentColumn + currentPage * self.collectionView.bounds.size.width;
frame.origin.y = self.itemSize.height * currentRow;
attributes.frame = frame;
}
return attributesArray;
}
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
return YES;
}
@end