How do you determine spacing between cells in UICollectionView flowLayout

后端 未结 12 1171
夕颜
夕颜 2020-11-29 15:32

I have a UICollectionView with a flow layout and each cell is a square. How do I determine the spacing between each cells on each row? I can\'t seem to find the appropriate

相关标签:
12条回答
  • 2020-11-29 16:03

    Clean Swift solution, from an history of evolution:

    1. there was matt answer
    2. there was Chris Wagner lone items fix
    3. there was mokagio sectionInset and minimumInteritemSpacing improvement
    4. there was fanpyi Swift version
    5. now here is a simplified and clean version of mine:
    open class UICollectionViewLeftAlignedLayout: UICollectionViewFlowLayout {
        open override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
            return super.layoutAttributesForElements(in: rect)?.map { $0.representedElementKind == nil ? layoutAttributesForItem(at: $0.indexPath)! : $0 }
        }
    
        open override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
            guard let currentItemAttributes = super.layoutAttributesForItem(at: indexPath)?.copy() as? UICollectionViewLayoutAttributes,
                collectionView != nil else {
                // should never happen
                return nil
            }
    
            // if the current frame, once stretched to the full row intersects the previous frame then they are on the same row
            if indexPath.item != 0,
                let previousFrame = layoutAttributesForItem(at: IndexPath(item: indexPath.item - 1, section: indexPath.section))?.frame,
                currentItemAttributes.frame.intersects(CGRect(x: -.infinity, y: previousFrame.origin.y, width: .infinity, height: previousFrame.size.height)) {
                // the next item on a line
                currentItemAttributes.frame.origin.x = previousFrame.origin.x + previousFrame.size.width + evaluatedMinimumInteritemSpacingForSection(at: indexPath.section)
            } else {
                // the first item on a line
                currentItemAttributes.frame.origin.x = evaluatedSectionInsetForSection(at: indexPath.section).left
            }
            return currentItemAttributes
        }
    
        func evaluatedMinimumInteritemSpacingForSection(at section: NSInteger) -> CGFloat {
            return (collectionView?.delegate as? UICollectionViewDelegateFlowLayout)?.collectionView?(collectionView!, layout: self, minimumInteritemSpacingForSectionAt: section) ?? minimumInteritemSpacing
        }
    
        func evaluatedSectionInsetForSection(at index: NSInteger) -> UIEdgeInsets {
            return (collectionView?.delegate as? UICollectionViewDelegateFlowLayout)?.collectionView?(collectionView!, layout: self, insetForSectionAt: index) ?? sectionInset
        }
    }
    

    Usage: the spacing between items is determined by delegate's collectionView (_:layout:minimumInteritemSpacingForSectionAt:).

    I put it on github, https://github.com/Coeur/UICollectionViewLeftAlignedLayout, where I actually added a feature of supporting both scroll directions (horizontal and vertical).

    0 讨论(0)
  • 2020-11-29 16:04

    Update: Swift version of this answer: https://github.com/fanpyi/UICollectionViewLeftAlignedLayout-Swift


    Taking @matt's lead I modified his code to insure that items are ALWAYS left aligned. I found that if an item ended up on a line by itself, it would be centered by the flow layout. I made the following changes to address this issue.

    This situation would only ever occur if you have cells that vary in width, which could result in a layout like the following. The last line always left aligns due to the behavior of UICollectionViewFlowLayout, the issue lies in items that are by themselves in any line but the last one.

    With @matt's code I was seeing.

    enter image description here

    In that example we see that cells get centered if they end up on the line by themselves. The code below insures your collection view would look like this.

    enter image description here

    #import "CWDLeftAlignedCollectionViewFlowLayout.h"
    
    const NSInteger kMaxCellSpacing = 9;
    
    @implementation CWDLeftAlignedCollectionViewFlowLayout
    
    - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
        NSArray* attributesToReturn = [super layoutAttributesForElementsInRect:rect];
        for (UICollectionViewLayoutAttributes* attributes in attributesToReturn) {
            if (nil == attributes.representedElementKind) {
                NSIndexPath* indexPath = attributes.indexPath;
                attributes.frame = [self layoutAttributesForItemAtIndexPath:indexPath].frame;
            }
        }
        return attributesToReturn;
    }
    
    - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
        UICollectionViewLayoutAttributes* currentItemAttributes =
        [super layoutAttributesForItemAtIndexPath:indexPath];
    
        UIEdgeInsets sectionInset = [(UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout sectionInset];
    
        CGRect currentFrame = currentItemAttributes.frame;
    
        if (indexPath.item == 0) { // first item of section
            currentFrame.origin.x = sectionInset.left; // first item of the section should always be left aligned
            currentItemAttributes.frame = currentFrame;
    
            return currentItemAttributes;
        }
    
        NSIndexPath* previousIndexPath = [NSIndexPath indexPathForItem:indexPath.item-1 inSection:indexPath.section];
        CGRect previousFrame = [self layoutAttributesForItemAtIndexPath:previousIndexPath].frame;
        CGFloat previousFrameRightPoint = CGRectGetMaxX(previousFrame) + kMaxCellSpacing;
    
        CGRect strecthedCurrentFrame = CGRectMake(0,
                                                  currentFrame.origin.y,
                                                  self.collectionView.frame.size.width,
                                                  currentFrame.size.height);
    
        if (!CGRectIntersectsRect(previousFrame, strecthedCurrentFrame)) { // if current item is the first item on the line
            // the approach here is to take the current frame, left align it to the edge of the view
            // then stretch it the width of the collection view, if it intersects with the previous frame then that means it
            // is on the same line, otherwise it is on it's own new line
            currentFrame.origin.x = sectionInset.left; // first item on the line should always be left aligned
            currentItemAttributes.frame = currentFrame;
            return currentItemAttributes;
        }
    
        currentFrame.origin.x = previousFrameRightPoint;
        currentItemAttributes.frame = currentFrame;
        return currentItemAttributes;
    }
    
    @end
    
    0 讨论(0)
  • 2020-11-29 16:05

    To get a maximum interitem spacing, subclass UICollectionViewFlowLayout and override layoutAttributesForElementsInRect: and layoutAttributesForItemAtIndexPath:.

    For example, a common problem is this: the rows of a collection view are right-and-left justified, except for the last line which is left-justified. Let's say we want all the lines to be left-justified, so that the space between them is, let's say, 10 points. Here's an easy way (in your UICollectionViewFlowLayout subclass):

    - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
        NSArray* arr = [super layoutAttributesForElementsInRect:rect];
        for (UICollectionViewLayoutAttributes* atts in arr) {
            if (nil == atts.representedElementKind) {
                NSIndexPath* ip = atts.indexPath;
                atts.frame = [self layoutAttributesForItemAtIndexPath:ip].frame;
            }
        }
        return arr;
    }
    
    - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
        UICollectionViewLayoutAttributes* atts =
        [super layoutAttributesForItemAtIndexPath:indexPath];
    
        if (indexPath.item == 0) // degenerate case 1, first item of section
            return atts;
    
        NSIndexPath* ipPrev =
        [NSIndexPath indexPathForItem:indexPath.item-1 inSection:indexPath.section];
    
        CGRect fPrev = [self layoutAttributesForItemAtIndexPath:ipPrev].frame;
        CGFloat rightPrev = fPrev.origin.x + fPrev.size.width + 10;
        if (atts.frame.origin.x <= rightPrev) // degenerate case 2, first item of line
            return atts;
    
        CGRect f = atts.frame;
        f.origin.x = rightPrev;
        atts.frame = f;
        return atts;
    }
    

    The reason this is so easy is that we aren't really performing the heavy lifting of the layout; we are leveraging the layout work that UICollectionViewFlowLayout has already done for us. It has already decided how many items go in each line; we're just reading those lines and shoving the items together, if you see what I mean.

    0 讨论(0)
  • 2020-11-29 16:08

    The "problem" with UICollectionViewFlowLayout is that it applies a justified align to the cells: The first cell in a row is left-aligned, the last cell in a row is right-aligned and all other cells in between are evenly distributed with an equal spacing that's greater than the minimumInteritemSpacing.

    There are already many great answers to this post that solve this problem by subclassing UICollectionViewFlowLayout. As a result you get a layout that aligns the cells left. Another valid solution to distribute the cells with a constant spacing is to align the cells right.

    AlignedCollectionViewFlowLayout

    I've created a UICollectionViewFlowLayout subclass as well that follows a similar idea as suggested by matt and Chris Wagner that can either align the cells

    ⬅︎ left:

    or ➡︎ right:

    You can simply download it from here, add the layout file to your project and set AlignedCollectionViewFlowLayout as your collection view's layout class:
    https://github.com/mischa-hildebrand/AlignedCollectionViewFlowLayout

    How it works (for left-aligned cells):

     +---------+----------------------------------------------------------------+---------+
     |         |                                                                |         |
     |         |     +------------+                                             |         |
     |         |     |            |                                             |         |
     | section |- - -|- - - - - - |- - - - +---------------------+ - - - - - - -| section |
     |  inset  |     |intersection|        |                     |   line rect  |  inset  |
     |         |- - -|- - - - - - |- - - - +---------------------+ - - - - - - -|         |
     | (left)  |     |            |             current item                    | (right) |
     |         |     +------------+                                             |         |
     |         |     previous item                                              |         |
     +---------+----------------------------------------------------------------+---------+
    

    The concept here is to check if the current cell with index i and the previous cell with the index i−1 occupy the same line.

    • If they don't the cell with index i is the left most cell in the line.
      → Move the cell to the left edge of the collection view (without changing its vertical position).
    • If they do, the cell with index i is not the left most cell in the line.
      → Get the previous cell's frame (with the index i−1) and move the current cell next to it.

    For right-aligned cells...

    ... you do the same vice-versa, i.e. you check the next cell with the index i+1 instead.

    0 讨论(0)
  • 2020-11-29 16:12

    A little bit of maths does the trick more easily. The code wrote by Chris Wagner is horrible because it calls the layout attributes of each previous items. So the more you scroll, the more it's slow...

    Just use modulo like this (I'm using my minimumInteritemSpacing value as a max value too):

    - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
    {
        UICollectionViewLayoutAttributes* currentItemAttributes = [super layoutAttributesForItemAtIndexPath:indexPath];
        NSInteger numberOfItemsPerLine = floor([self collectionViewContentSize].width / [self itemSize].width);
    
        if (indexPath.item % numberOfItemsPerLine != 0)
        {
            NSInteger cellIndexInLine = (indexPath.item % numberOfItemsPerLine);
    
            CGRect itemFrame = [currentItemAttributes frame];
            itemFrame.origin.x = ([self itemSize].width * cellIndexInLine) + ([self minimumInteritemSpacing] * cellIndexInLine);
            currentItemAttributes.frame = itemFrame;
        }
    
        return currentItemAttributes;
    }
    
    0 讨论(0)
  • 2020-11-29 16:15

    Here it is for NSCollectionViewFlowLayout

    class LeftAlignedCollectionViewFlowLayout: NSCollectionViewFlowLayout {
    
        var maximumCellSpacing = CGFloat(2.0)
    
        override func layoutAttributesForElementsInRect(rect: NSRect) -> [NSCollectionViewLayoutAttributes] {
    
            let attributesToReturn = super.layoutAttributesForElementsInRect(rect)
    
            for attributes in attributesToReturn ?? [] {
                if attributes.representedElementKind == nil {
                    attributes.frame = self.layoutAttributesForItemAtIndexPath(attributes.indexPath!)!.frame
                }
            }
            return attributesToReturn
        }
    
        override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> NSCollectionViewLayoutAttributes? {
    
            let curAttributes = super.layoutAttributesForItemAtIndexPath(indexPath)
            let sectionInset = (self.collectionView?.collectionViewLayout as! NSCollectionViewFlowLayout).sectionInset
    
            if indexPath.item == 0 {
                let f = curAttributes!.frame
                curAttributes!.frame = CGRectMake(sectionInset.left, f.origin.y, f.size.width, f.size.height)
                return curAttributes
            }
    
            let prevIndexPath = NSIndexPath(forItem: indexPath.item-1, inSection: indexPath.section)
            let prevFrame = self.layoutAttributesForItemAtIndexPath(prevIndexPath)!.frame
            let prevFrameRightPoint = prevFrame.origin.x + prevFrame.size.width + maximumCellSpacing
    
            let curFrame = curAttributes!.frame
            let stretchedCurFrame = CGRectMake(0, curFrame.origin.y, self.collectionView!.frame.size.width, curFrame.size.height)
    
            if CGRectIntersectsRect(prevFrame, stretchedCurFrame) {
                curAttributes!.frame = CGRectMake(prevFrameRightPoint, curFrame.origin.y, curFrame.size.width, curFrame.size.height)
            } else {
                curAttributes!.frame = CGRectMake(sectionInset.left, curFrame.origin.y, curFrame.size.width, curFrame.size.height)
            }
    
            return curAttributes
        }
    }
    
    0 讨论(0)
提交回复
热议问题