Configure UICollectionViewFlowLayout to layout row

2019-02-18 11:28发布

问题:

By default (i.e., with a vertical scrolling direction), the UICollectionViewFlowLayout lays out cells by starting at the top-left, going from left to right, until the row is filled, and then proceeds to the next line down. Instead, I would like it to start at the bottom-left, go from left to right, until the row is filled, and then proceed to the next line up.

Is there a straightforward way to do this by subclassing UIScrollViewFlowLayout, or do I basically need to re-implement that class from scratch?

Apple's documentation on subclassing flow layout suggests that I only need to override and re-implement my own version of layoutAttributesForElementsInRect:, layoutAttributesForItemAtIndexPath:, and collectionViewContentSize. But this does not seem straightforward. Since UICollectionViewFlowLayout does not expose any of the grid layout calculations it makes internally in prepareLayout, I need to deduce all the layout values needed for the bottom-to-top layout from the values it generates for a top-to-bottom layout.

I am not sure this is possible. While I can re-use its calculations about which groups of items get put on the same rows, I will need to calculate new y offsets. And to make my calculations I will need information about all the items, but those superclass methods do not report that.

回答1:

You could basically implement it with a simple logic, however this seems to be some how odd. If the collectionview contentsize is same as that of the collectionview bounds or if all the cells are visible then you could implement this with simple flowLayout as this,

@implementation SimpleFlowLayout


- (UICollectionViewLayoutAttributes*)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath{
  UICollectionViewLayoutAttributes *attribute = [super layoutAttributesForItemAtIndexPath:indexPath];
  [self modifyLayoutAttribute:attribute];
  return attribute;
}

- (NSArray*)layoutAttributesForElementsInRect:(CGRect)rect{
  NSArray *attributes = [super layoutAttributesForElementsInRect:rect];
  for(UICollectionViewLayoutAttributes *attribute in attributes){
    [self modifyLayoutAttribute:attribute];
  }
  return attributes;
}


- (void)modifyLayoutAttribute:(UICollectionViewLayoutAttributes*)attribute{
  CGSize contentSize = self.collectionViewContentSize;
  CGRect frame = attribute.frame;
  frame.origin.x = contentSize.width - attribute.frame.origin.x - attribute.frame.size.width;
  frame.origin.y = contentSize.height - attribute.frame.origin.y - attribute.frame.size.height;
  attribute.frame = frame;

}

@end

And so the figure looks like this,

But, if you use more rows, more than the that can be seen on the screen at the same time, then there seems to be some problem with reusing. Since the UICollectionView datasource method, collectionView:cellForItemAtIndexPath: works linearly and asks for the indexPath as the user scrolls, the cell are asked in the usual increasing indexPath pattern such as 1 --- 100 though we would want it to reverse this pattern. While scrolling we would need the collectionView to ask us for the items in decreasing order since our 100 item resides at top and 1 item at bottom. So, I dont have any particular idea about how this could be accomplished.



回答2:

The very helpful answer by @insane-36 showed a way to do it when collectionView.bounds == collectionView.collectionViewContentSize.

But if you wish to support the case where collectionView.bounds < collectionViewcontentSize, then I believe you need to re-map the rects exactly to support scrolling properly. If you wish to support the case where collectionView.bounds > collectionViewContentSize, then you need to override collectionViewContentSize to ensure the content rect is positioned at the bottom of the collectionView (since otherwise it will be positioned at the top, due to the top-to-bottom default behavior of UIScrollView).

So the full solution is a bit more involved, and I ended up developing it here: https://github.com/algal/ALGReversedFlowLayout.



回答3:

UICollectionView with a reversed flow layout

import Foundation
import UIKit

class InvertedFlowLayout: UICollectionViewFlowLayout {
    let cellHeight: CGFloat = 120.0 // Your cell height here...

    override func prepare() {
        super.prepare()
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard super.layoutAttributesForElements(in: rect) != nil else { return nil }
        var attributesArrayNew = [UICollectionViewLayoutAttributes]()

        if let collectionView = self.collectionView {
            for section in 0 ..< collectionView.numberOfSections {
                for item in 0 ..< collectionView.numberOfItems(inSection: section) {
                    let indexPath = IndexPath(item: item, section: section)
                    if let attributeCell = layoutAttributesForItem(at: indexPath) {
                        if attributeCell.frame.intersects(rect) {
                            attributesArrayNew.append(attributeCell)
                        }
                    }
                }
            }

            for section in 0 ..< collectionView.numberOfSections {
                let indexPath = IndexPath(item: 0, section: section)
                if let attributeKind = layoutAttributesForSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, at: indexPath) {
                    attributesArrayNew.append(attributeKind)
                }
            }
        }

        return attributesArrayNew
    }

    override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        let attributeKind = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: elementKind, with: indexPath)

        var itemsCount = 0

        for i in 0 ..< indexPath.section + 1 {
            itemsCount += self.collectionView!.numberOfItems(inSection: i)
        }

        attributeKind.frame = CGRect(x: 0, y: collectionViewContentSize.height - CGFloat(itemsCount) * (cellHeight + minimumLineSpacing) - CGFloat(indexPath.section + 1) * headerHeight(indexPath.section) - sectionInset.bottom + minimumLineSpacing, width: collectionViewContentSize.width, height: headerHeight(indexPath.section))
        return attributeKind
    }

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        let attributeCell = UICollectionViewLayoutAttributes(forCellWith: indexPath)

        var itemsCount = 0

        for i in 0 ..< indexPath.section {
            itemsCount += self.collectionView!.numberOfItems(inSection: i)
        }

        attributeCell.frame = CGRect(x: 0, y: collectionViewContentSize.height - CGFloat(indexPath.item + 1 + itemsCount) * cellHeight - CGFloat(indexPath.item + itemsCount) * minimumLineSpacing - CGFloat(indexPath.section) * headerHeight(indexPath.section) - sectionInset.bottom, width: collectionViewContentSize.width, height: cellHeight)

        return attributeCell
    }

    override var collectionViewContentSize: CGSize {
        get {
            var height: CGFloat = 0.0
            var bounds = CGRect.zero

            if let collectionView = self.collectionView {
                for section in 0 ..< collectionView.numberOfSections {
                    let numItems = collectionView.numberOfItems(inSection: section)
                    height += CGFloat(numItems) * (cellHeight + minimumLineSpacing)
                }

                height += sectionInset.bottom + CGFloat(collectionView.numberOfSections) * headerHeight(0)
                bounds = collectionView.bounds
            }

            return CGSize(width: bounds.width, height: max(height, bounds.height))
        }
    }

    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        if let oldBounds = self.collectionView?.bounds,
            oldBounds.width != newBounds.width || oldBounds.height != newBounds.height {
            return true
        }

        return false
    }

    func headerHeight(_ section: Int) -> CGFloat {
        if let collectionView = self.collectionView, let delegateFlowLayout = collectionView.delegate as? UICollectionViewDelegateFlowLayout {
            let size = delegateFlowLayout.collectionView!(collectionView, layout: self, referenceSizeForHeaderInSection: section)
            return size.height
        }

        return 0
    }
}