【问题标题】:UICollectionView horizontal paging - can I use Flow Layout?UICollectionView 水平分页 - 我可以使用流布局吗?
【发布时间】:2013-05-16 17:17:05
【问题描述】:

这与To use Flow Layout, or to Customize? 相关但不同。

这是我正在尝试做的一个示例:

我想知道我是否可以使用 UICollectionViewFlowLayout 及其子类来做到这一点,或者我是否需要创建一个完全自定义的布局?根据 UICollectionView 上的 WWDC 2012 视频,如果您使用带有垂直滚动的 Flow Layout,您的布局线是水平的,如果您水平滚动,您的布局线是垂直的。我想要水平滚动的集合视图中的水平布局线。

我的模型中也没有任何固有部分 - 这只是一组项目。我可以将它们分组为部分,但集合视图是可调整大小的,因此页面上可以容纳的项目数量有时会发生变化,似乎每个项目所在页面的选择最好留给布局而不是如果我没有任何有意义的部分,请使用模型。

那么,我可以使用 Flow Layout 来做到这一点,还是需要创建自定义布局?

【问题讨论】:

    标签: ios uikit uicollectionview uicollectionviewlayout


    【解决方案1】:

    这里分享一下我的简单实现!

    .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;
    

    希望足够有用。

    【讨论】:

    • 为什么使用_layoutForAttributesForCellAtIndexPath: 方法?下划线前缀的方法是苹果保留的,而且你所做的只是从layoutAttributesForItemAtIndexPath:转发。
    • 我通常在私有方法中使用我的自定义前缀(2 个字母)后跟一个下划线:XX_myPrivateMethod。复制粘贴时,我删除了类名和私有方法的前缀。我以为没有人会抱怨,我看不是;)
    • 但是为什么要使用私有方法呢?为什么不在-layoutAttributesForItemAtIndexPath: 中完成所有这些工作?
    • 是的,我们可以这样做。
    • 我如何让这个支持多个部分?
    【解决方案2】:

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

    }

    【讨论】:

    • let frameSize = CGSize(width: cellHeight, height: cellWidth)哎呀
    • 感谢您的快速转换;)
    【解决方案3】:

    您是对的 - 这不是股票水平滚动集合视图布局单元格的方式。恐怕您将不得不实现自己的自定义UICollectionViewLayout 子类。要么,要么将您的模型分成几个部分。

    【讨论】:

    【解决方案4】:

    之前的上述实现不完整,有问题,并且单元格大小固定。这是代码的更直译:

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

    【讨论】:

    • 这对你有用吗?我试过这个,它渲染第一部分没问题。但是其他部分没有被渲染
    • 是的,你可以在这个例子中看到它的工作原理:github.com/sprint84/TabbedCollectionView
    • @GuilhermeSprint 展示如何将此布局添加到集合视图并避免常见错误(例如返回 0 个部分并确保设置 itemSize)可能会有所帮助。
    【解决方案5】:

    斯威夫特 4

    代码:

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

    这是 @GuilhermeSprint 答案的 Swift 3 版本

    代码:

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

    【讨论】:

    【解决方案6】:

    可以简单地将 UICollectionView.xib 中的滚动方向更改为水平。并与数组中元素的正确顺序一起使用。

    【讨论】:

    • 有趣的想法 - 重新排列数组中的元素,使它们布局正确?我宁愿保持数据源(数组)的纯净,并保存应用程序的视图/表示层的布局顺序,所以我更有可能选择像this one by vilanovi 这样的答案。
    【解决方案7】:

    当然,您最后的手段是在外部水平滚动的集合视图中的每个部分内使用多个垂直集合视图。 除了增加代码复杂性和执行单元格间动画的难度之外,我想不出这种方法的主要问题。

    【讨论】:

      【解决方案8】:
      @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
      

      【讨论】:

      • 这个解决方案差不多好了。但是在第三页之后有一个错误,当集合视图布局停止传递第一个项目时,只传递那些对可见页面很重要的项目
      猜你喜欢
      • 1970-01-01
      • 2016-10-04
      • 2016-11-17
      • 1970-01-01
      • 2013-10-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多