简体   繁体   English

UICollectionView水平分页 - 我可以使用Flow Layout吗?

[英]UICollectionView horizontal paging - can I use Flow Layout?

This is related to but distinct from To use Flow Layout, or to Customize? 这与“使用流程布局”或“自定义”有何不同? .

Here is an illustration of what I'm trying to do: 以下是我正在尝试做的事情的说明: 我想要做的插图

I'm wondering if I can do this with a UICollectionViewFlowLayout , a subclass thereof, or if I need to create a completely custom layout? 我想知道我是否可以使用UICollectionViewFlowLayout ,它的子类,或者如果我需要创建一个完全自定义的布局? Based on the WWDC 2012 videos on UICollectionView, it appears that if you use Flow Layout with vertical scrolling, your layout lines are horizontal, and if you scroll horizontally, your layout lines are vertical. 根据UICollectionView上的WWDC 2012视频,如果您使用垂直滚动的Flow Layout,您的布局线条是水平的,如果您水平滚动,您的布局线条是垂直的。 I want horizontal layout lines in a horizontally-scrolling collection view. 我想在水平滚动的集合视图中使用水平布局线。

I also don't have any inherent sections in my model - this is just a single set of items. 我的模型中也没有任何固有的部分 - 这只是一组项目。 I could group them into sections, but the collection view is resizable, so the number of items that can fit on a page would change sometimes, and it seems like the choice of which page each item goes on is better left to the layout than to the model if I don't have any meaningful sections. 我可以将它们分成几个部分,但是集合视图是可调整大小的,因此可以适应页面的项目数量有时会发生变化,看起来每个项目所在的页面的选择最好留给布局而不是如果我没有任何有意义的部分,该模型。

So, can I do this with Flow Layout, or do I need to create a custom layout? 那么,我可以使用Flow Layout执行此操作,还是需要创建自定义布局?

Here I share my simple implementation! 在这里,我分享我的简单实现!

The .h file: .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

The .m file: .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

And finally, if you want a paginated behaviour, you just need to configure your UICollectionView: 最后,如果您想要一个分页行为,您只需要配置您的UICollectionView:

_collectionView.pagingEnabled = YES;

Hoping to be useful enough. 希望足够有用。

Converted vilanovi code to Swift in case someone, needs it in the future. 将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))
}

} }

You're right – that's not how a stock horizontally-scrolling collection view lays out cells. 你是对的 - 这不是股票水平滚动集合视图如何布局单元格。 I'm afraid that you're going to have to implement your own custom UICollectionViewLayout subclass. 我担心你将不得不实现自己的自定义UICollectionViewLayout子类。 Either that, or separate your models into sections. 要么是这样,要么将模型分成几个部分。

The previous above implementation was not complete, buggy, and with fixed cell size. 上面的实现不完整,有缺陷,并且具有固定的单元大小。 Here's a more literal translation for the code: 这是代码的更直译:

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
    }
}

Swift 4 斯威夫特4

Code: 码:

public class HorizontalFlowLayout: 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 = CGRect(x: 0, y: 0, width: 0, height: 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
    }
}

This is the Swift 3 version of @GuilhermeSprint answer 这是@GuilhermeSprint答案的Swift 3版本

Code: 码:

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
    }
}

Usage: 用法:

    // 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

Result 结果

截图

My Delegates extension if you wanna check it also 如果你想检查我的代表扩展

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
    }
}

Can simply change Scroll Direction in UICollectionView.xib to Horizontal. 可以简单地将UICollectionView.xib中的Scroll Direction更改为Horizo​​ntal。 And use with the correct order of elements in the array. 并使用数组中元素的正确顺序。

Your last resort, of course, would be to use multiple vertically collection views inside each section in an outer horizontally scrolling collection view. 当然,您最后的选择是在外部水平滚动集合视图中的每个部分内使用多个垂直集合视图。 Apart from increasing code complexity and difficulty in performing inter-section cell animations, I can't think of major issues with this approach right off my head. 除了增加代码复杂性和执行节间单元动画的困难之外,我无法想到这种方法的主要问题。

@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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM