How can I center rows in UICollectionView?

前端 未结 9 1590
囚心锁ツ
囚心锁ツ 2021-02-02 06:24

I have a UICollectionView with random cells. Is there any method that allows me to center rows?

This is how it looks by default:

[ x x x x         


        
相关标签:
9条回答
  • 2021-02-02 07:03

    Swift 3.0 version of the class:

    class UICollectionViewFlowCenterLayout: UICollectionViewFlowLayout {
        override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
            guard let suggestedAttributes = super.layoutAttributesForElements(in: rect) else { return nil }
    
            guard scrollDirection == .vertical else { return suggestedAttributes }
    
            var newAttributes: [UICollectionViewLayoutAttributes] = []
    
            var currentRowAttributes: [UICollectionViewLayoutAttributes] = []
            var yOffset:CGFloat = sectionInset.top
            for attributes in suggestedAttributes {
                if attributes.frame.origin.y != yOffset {
                    centerSingleRowWithItemsAttributes(attributes: &currentRowAttributes, rect: rect)
                    newAttributes += currentRowAttributes
                    currentRowAttributes = []
                    yOffset = attributes.frame.origin.y
                }
                currentRowAttributes += [attributes]
            }
            centerSingleRowWithItemsAttributes(attributes: &currentRowAttributes, rect: rect)
            newAttributes += currentRowAttributes
    
            return newAttributes
        }
    
    
        private func centerSingleRowWithItemsAttributes( attributes: inout [UICollectionViewLayoutAttributes], rect: CGRect) {
            guard let item = attributes.last else { return }
    
            let itemsCount = CGFloat(attributes.count)
            let sideInsets = rect.width - (item.frame.width * itemsCount) - (minimumInteritemSpacing * (itemsCount - 1))
            var leftOffset = sideInsets / 2
    
            for attribute in attributes {
                attribute.frame.origin.x = leftOffset
                leftOffset += attribute.frame.width + minimumInteritemSpacing
            }
        }
    }
    
    0 讨论(0)
  • 2021-02-02 07:05

    This can be achieved with a (relatively) simple custom layout, subclassed from UICollectionViewFlowLayout. Here's an example in Swift:

    /**
     * A simple `UICollectionViewFlowLayout` subclass that would make sure the items are center-aligned in the collection view, when scrolling vertically.
     */
    class UICollectionViewFlowCenterLayout: UICollectionViewFlowLayout {
    
        override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
            guard let suggestedAttributes = super.layoutAttributesForElementsInRect(rect) else { return nil }
    
            guard scrollDirection == .Vertical else { return suggestedAttributes }
    
            var newAttributes: [UICollectionViewLayoutAttributes] = []
    
            /// We will collect items for each row in this array
            var currentRowAttributes: [UICollectionViewLayoutAttributes] = []
            /// We will use this variable to detect new rows when iterating over items
            var yOffset:CGFloat = sectionInset.top
            for attributes in suggestedAttributes {
                /// If we happen to run into a new row...
                if attributes.frame.origin.y != yOffset {
                    /*
                     * Update layout of all items in the previous row and add them to the resulting array
                     */
                    centerSingleRowWithItemsAttributes(&currentRowAttributes, rect: rect)
                    newAttributes += currentRowAttributes
                    /*
                     * Reset the accumulated values for the new row
                     */
                    currentRowAttributes = []
                    yOffset = attributes.frame.origin.y
                }
                currentRowAttributes += [attributes]
            }
            /*
             * Update the layout of the last row.
             */
            centerSingleRowWithItemsAttributes(&currentRowAttributes, rect: rect)
            newAttributes += currentRowAttributes
    
            return newAttributes
        }
    
        /**
         Updates the attributes for items, so that they are center-aligned in the given rect.
    
         - parameter attributes: Attributes of the items
         - parameter rect:       Bounding rect
         */
        private func centerSingleRowWithItemsAttributes(inout attributes: [UICollectionViewLayoutAttributes], rect: CGRect) {
            guard let item = attributes.last else { return }
    
            let itemsCount = CGFloat(attributes.count)
            let sideInsets = rect.width - (item.frame.width * itemsCount) - (minimumInteritemSpacing * (itemsCount - 1))
            var leftOffset = sideInsets / 2
    
            for attribute in attributes {
                attribute.frame.origin.x = leftOffset
                leftOffset += attribute.frame.width + minimumInteritemSpacing
            }
        }
    }
    
    0 讨论(0)
  • 2021-02-02 07:06

    With Swift 4.1 and iOS 11, according to your needs, you may choose one of the 2 following complete implementations in order to fix your problem.


    #1. Center UICollectionViewCells with fixed size

    The implementation below shows how to use UICollectionViewLayout's layoutAttributesForElements(in:) and UICollectionViewFlowLayout's itemSize in order to center the cells of a UICollectionView:

    CollectionViewController.swift

    import UIKit
    
    class CollectionViewController: UICollectionViewController {
    
        let columnLayout = FlowLayout(
            itemSize: CGSize(width: 140, height: 140),
            minimumInteritemSpacing: 10,
            minimumLineSpacing: 10,
            sectionInset: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
        )
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            title = "Center cells"
    
            collectionView?.collectionViewLayout = columnLayout
            collectionView?.contentInsetAdjustmentBehavior = .always
            collectionView?.register(CollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
        }
    
        override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            return 7
        }
    
        override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell
            return cell
        }
    
        override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
            collectionView?.collectionViewLayout.invalidateLayout()
            super.viewWillTransition(to: size, with: coordinator)
        }
    
    }
    

    FlowLayout.swift

    import UIKit
    
    class FlowLayout: UICollectionViewFlowLayout {
    
        required init(itemSize: CGSize, minimumInteritemSpacing: CGFloat = 0, minimumLineSpacing: CGFloat = 0, sectionInset: UIEdgeInsets = .zero) {
            super.init()
    
            self.itemSize = itemSize
            self.minimumInteritemSpacing = minimumInteritemSpacing
            self.minimumLineSpacing = minimumLineSpacing
            self.sectionInset = sectionInset
            sectionInsetReference = .fromSafeArea
        }
    
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
        override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
            let layoutAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
            guard scrollDirection == .vertical else { return layoutAttributes }
    
            // Filter attributes to compute only cell attributes
            let cellAttributes = layoutAttributes.filter({ $0.representedElementCategory == .cell })
    
            // Group cell attributes by row (cells with same vertical center) and loop on those groups
            for (_, attributes) in Dictionary(grouping: cellAttributes, by: { ($0.center.y / 10).rounded(.up) * 10 }) {
                // Get the total width of the cells on the same row
                let cellsTotalWidth = attributes.reduce(CGFloat(0)) { (partialWidth, attribute) -> CGFloat in
                    partialWidth + attribute.size.width
                }
    
                // Calculate the initial left inset
                let totalInset = collectionView!.safeAreaLayoutGuide.layoutFrame.width - cellsTotalWidth - sectionInset.left - sectionInset.right - minimumInteritemSpacing * CGFloat(attributes.count - 1)
                var leftInset = (totalInset / 2 * 10).rounded(.down) / 10 + sectionInset.left
    
                // Loop on cells to adjust each cell's origin and prepare leftInset for the next cell
                for attribute in attributes {
                    attribute.frame.origin.x = leftInset
                    leftInset = attribute.frame.maxX + minimumInteritemSpacing
                }
            }
    
            return layoutAttributes
        }
    
    }
    

    CollectionViewCell.swift

    import UIKit
    
    class CollectionViewCell: UICollectionViewCell {
    
        override init(frame: CGRect) {
            super.init(frame: frame)
    
            contentView.backgroundColor = .cyan
        }
    
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
    }
    

    Expected result:


    #2. Center autoresizing UICollectionViewCells

    The implementation below shows how to use UICollectionViewLayout's layoutAttributesForElements(in:), UICollectionViewFlowLayout's estimatedItemSize and UILabel's preferredMaxLayoutWidth in order to center the cells of a UICollectionView:

    CollectionViewController.swift

    import UIKit
    
    class CollectionViewController: UICollectionViewController {
    
        let array = ["1", "1 2", "1 2 3 4 5 6 7 8", "1 2 3 4 5 6 7 8 9 10 11", "1 2 3", "1 2 3 4", "1 2 3 4 5 6", "1 2 3 4 5 6 7 8 9 10", "1 2 3 4", "1 2 3 4 5 6 7", "1 2 3 4 5 6 7 8 9", "1", "1 2 3 4 5", "1", "1 2 3 4 5 6"]
    
        let columnLayout = FlowLayout(
            minimumInteritemSpacing: 10,
            minimumLineSpacing: 10,
            sectionInset: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
        )
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            title = "Center cells"
    
            collectionView?.collectionViewLayout = columnLayout
            collectionView?.contentInsetAdjustmentBehavior = .always
            collectionView?.register(CollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
        }
    
        override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            return array.count
        }
    
        override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell
            cell.label.text = array[indexPath.row]
            return cell
        }
    
        override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
            collectionView?.collectionViewLayout.invalidateLayout()
            super.viewWillTransition(to: size, with: coordinator)
        }
    
    }
    

    FlowLayout.swift

    import UIKit
    
    class FlowLayout: UICollectionViewFlowLayout {
    
        required init(minimumInteritemSpacing: CGFloat = 0, minimumLineSpacing: CGFloat = 0, sectionInset: UIEdgeInsets = .zero) {
            super.init()
    
            estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize
            self.minimumInteritemSpacing = minimumInteritemSpacing
            self.minimumLineSpacing = minimumLineSpacing
            self.sectionInset = sectionInset
            sectionInsetReference = .fromSafeArea
        }
    
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
        override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
            let layoutAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }
            guard scrollDirection == .vertical else { return layoutAttributes }
    
            // Filter attributes to compute only cell attributes
            let cellAttributes = layoutAttributes.filter({ $0.representedElementCategory == .cell })
    
            // Group cell attributes by row (cells with same vertical center) and loop on those groups
            for (_, attributes) in Dictionary(grouping: cellAttributes, by: { ($0.center.y / 10).rounded(.up) * 10 }) {
                // Get the total width of the cells on the same row
                let cellsTotalWidth = attributes.reduce(CGFloat(0)) { (partialWidth, attribute) -> CGFloat in
                    partialWidth + attribute.size.width
                }
    
                // Calculate the initial left inset
                let totalInset = collectionView!.safeAreaLayoutGuide.layoutFrame.width - cellsTotalWidth - sectionInset.left - sectionInset.right - minimumInteritemSpacing * CGFloat(attributes.count - 1)
                var leftInset = (totalInset / 2 * 10).rounded(.down) / 10 + sectionInset.left
    
                // Loop on cells to adjust each cell's origin and prepare leftInset for the next cell
                for attribute in attributes {
                    attribute.frame.origin.x = leftInset
                    leftInset = attribute.frame.maxX + minimumInteritemSpacing
                }
            }
    
            return layoutAttributes
        }
    
    }
    

    CollectionViewCell.swift

    import UIKit
    
    class CollectionViewCell: UICollectionViewCell {
    
        let label = UILabel()
    
        override init(frame: CGRect) {
            super.init(frame: frame)
    
            contentView.backgroundColor = .orange
            label.preferredMaxLayoutWidth = 120
            label.numberOfLines = 0
    
            contentView.addSubview(label)
            label.translatesAutoresizingMaskIntoConstraints = false
            contentView.layoutMarginsGuide.topAnchor.constraint(equalTo: label.topAnchor).isActive = true
            contentView.layoutMarginsGuide.leadingAnchor.constraint(equalTo: label.leadingAnchor).isActive = true
            contentView.layoutMarginsGuide.trailingAnchor.constraint(equalTo: label.trailingAnchor).isActive = true
            contentView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: label.bottomAnchor).isActive = true
        }
    
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
    }
    

    Expected result:


    0 讨论(0)
  • 2021-02-02 07:08

    I've subclassed UICollectionViewFlowLayout - altered the code i've found here for left aligned collection view.

    1. Left align the collection view
    2. Group the attributes for line arrays
    3. For each line:
      • calculate the space on the right side
      • add half of the the space for each attribute of the line

    It's looks like this:

    - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
    
        NSArray *attributesForElementsInRect = [super layoutAttributesForElementsInRect:rect];
        NSMutableArray *newAttributesForElementsInRect = [[NSMutableArray alloc] initWithCapacity:attributesForElementsInRect.count];
    
        CGFloat leftMargin = self.sectionInset.left;
        NSMutableArray *lines = [NSMutableArray array];
        NSMutableArray *currLine = [NSMutableArray array];
    
        for (UICollectionViewLayoutAttributes *attributes in attributesForElementsInRect) {
            // Handle new line
            BOOL newLine = attributes.frame.origin.x <= leftMargin;
            if (newLine) {
                leftMargin = self.sectionInset.left; //will add outside loop
                currLine = [NSMutableArray arrayWithObject:attributes];
            } else {
                [currLine addObject:attributes];
            }
    
            if ([lines indexOfObject:currLine] == NSNotFound) {
                [lines addObject:currLine];
            }
    
            // Align to the left
            CGRect newLeftAlignedFrame = attributes.frame;
            newLeftAlignedFrame.origin.x = leftMargin;
            attributes.frame = newLeftAlignedFrame;
    
            leftMargin += attributes.frame.size.width + self.minimumInteritemSpacing;
            [newAttributesForElementsInRect addObject:attributes];
        }
    
        // Center left aligned lines
        for (NSArray *line in lines) {
            UICollectionViewLayoutAttributes *lastAttributes = line.lastObject;
            CGFloat space = CGRectGetWidth(self.collectionView.frame) - CGRectGetMaxX(lastAttributes.frame);
    
            for (UICollectionViewLayoutAttributes *attributes in line) {
                CGRect newFrame = attributes.frame;
                newFrame.origin.x = newFrame.origin.x + space / 2;
                attributes.frame = newFrame;
    
            }
        }
    
        return newAttributesForElementsInRect;
    }
    

    Hope it helps someone :)

    0 讨论(0)
  • 2021-02-02 07:13

    I made Swift 4 version of Kyle Truscott answer:

    import UIKit
    
    class CenterFlowLayout: UICollectionViewFlowLayout {
    
        private var attrCache = [IndexPath: UICollectionViewLayoutAttributes]()
    
        override func prepare() {
            attrCache = [:]
        }
    
        override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
            var updatedAttributes = [UICollectionViewLayoutAttributes]()
    
            let sections = self.collectionView?.numberOfSections ?? 0
            var section = 0
            while section < sections {
                let items = self.collectionView?.numberOfItems(inSection: section) ?? 0
                var item = 0
                while item < items {
                    let indexPath = IndexPath(row: item, section: section)
    
                    if let attributes = layoutAttributesForItem(at: indexPath), attributes.frame.intersects(rect) {
                        updatedAttributes.append(attributes)
                    }
    
                    let headerKind = UICollectionElementKindSectionHeader
                    if let headerAttributes = layoutAttributesForSupplementaryView(ofKind: headerKind, at: indexPath) {
                        updatedAttributes.append(headerAttributes)
                    }
    
                    let footerKind = UICollectionElementKindSectionFooter
                    if let footerAttributes = layoutAttributesForSupplementaryView(ofKind: footerKind, at: indexPath) {
                        updatedAttributes.append(footerAttributes)
                    }
    
                    item += 1
                }
    
                section += 1
            }
    
            return updatedAttributes
        }
    
        override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
            if let attributes = attrCache[indexPath] {
                return attributes
            }
    
            // Find the other items in the same "row"
            var rowBuddies = [UICollectionViewLayoutAttributes]()
    
            // Calculate the available width to center stuff within
            // sectionInset is NOT applicable here because a) we're centering stuff
            // and b) Flow layout has arranged the cells to respect the inset. We're
            // just hijacking the X position.
            var collectionViewWidth: CGFloat = 0
            if let collectionView = collectionView {
                collectionViewWidth = collectionView.bounds.width - collectionView.contentInset.left
                        - collectionView.contentInset.right
            }
    
            // To find other items in the "row", we need a rect to check intersects against.
            // Take the item attributes frame (from vanilla flow layout), and stretch it out
            var rowTestFrame: CGRect = super.layoutAttributesForItem(at: indexPath)?.frame ?? .zero
            rowTestFrame.origin.x = 0
            rowTestFrame.size.width = collectionViewWidth
    
            let totalRows = self.collectionView?.numberOfItems(inSection: indexPath.section) ?? 0
    
            // From this item, work backwards to find the first item in the row
            // Decrement the row index until a) we get to 0, b) we reach a previous row
            var rowStartIDX = indexPath.row
            while true {
                let prevIDX = rowStartIDX - 1
    
                if prevIDX < 0 {
                    break
                }
    
                let prevPath = IndexPath(row: prevIDX, section: indexPath.section)
                let prevFrame: CGRect = super.layoutAttributesForItem(at: prevPath)?.frame ?? .zero
    
                // If the item intersects the test frame, it's in the same row
                if prevFrame.intersects(rowTestFrame) {
                    rowStartIDX = prevIDX
                } else {
                    // Found previous row, escape!
                    break
                }
            }
    
            // Now, work back UP to find the last item in the row
            // For each item in the row, add it's attributes to rowBuddies
            var buddyIDX = rowStartIDX
            while true {
                if buddyIDX > totalRows - 1 {
                    break
                }
    
                let buddyPath = IndexPath(row: buddyIDX, section: indexPath.section)
    
                if let buddyAttributes = super.layoutAttributesForItem(at: buddyPath),
                   buddyAttributes.frame.intersects(rowTestFrame),
                   let buddyAttributesCopy = buddyAttributes.copy() as? UICollectionViewLayoutAttributes {
                    // If the item intersects the test frame, it's in the same row
                    rowBuddies.append(buddyAttributesCopy)
                    buddyIDX += 1
                } else {
                    // Encountered next row
                    break
                }
            }
    
            let flowDelegate = self.collectionView?.delegate as? UICollectionViewDelegateFlowLayout
            let selector = #selector(UICollectionViewDelegateFlowLayout.collectionView(_:layout:minimumInteritemSpacingForSectionAt:))
            let delegateSupportsInteritemSpacing = flowDelegate?.responds(to: selector) ?? false
    
            // x-x-x-x ... sum up the interim space
            var interitemSpacing = minimumInteritemSpacing
    
            // Check for minimumInteritemSpacingForSectionAtIndex support
            if let collectionView = collectionView, delegateSupportsInteritemSpacing && rowBuddies.count > 0 {
                interitemSpacing = flowDelegate?.collectionView?(collectionView,
                        layout: self,
                        minimumInteritemSpacingForSectionAt: indexPath.section) ?? 0
            }
    
            let aggregateInteritemSpacing = interitemSpacing * CGFloat(rowBuddies.count - 1)
    
            // Sum the width of all elements in the row
            var aggregateItemWidths: CGFloat = 0
            for itemAttributes in rowBuddies {
                aggregateItemWidths += itemAttributes.frame.width
            }
    
            // Build an alignment rect
            // |  |x-x-x-x|  |
            let alignmentWidth = aggregateItemWidths + aggregateInteritemSpacing
            let alignmentXOffset: CGFloat = (collectionViewWidth - alignmentWidth) / 2
    
            // Adjust each item's position to be centered
            var previousFrame: CGRect = .zero
            for itemAttributes in rowBuddies {
                var itemFrame = itemAttributes.frame
    
                if previousFrame.equalTo(.zero) {
                    itemFrame.origin.x = alignmentXOffset
                } else {
                    itemFrame.origin.x = previousFrame.maxX + interitemSpacing
                }
    
                itemAttributes.frame = itemFrame
                previousFrame = itemFrame
    
                // Finally, add it to the cache
                attrCache[itemAttributes.indexPath] = itemAttributes
            }
    
            return attrCache[indexPath]
        }
    }
    
    0 讨论(0)
  • In case anyone has a CollectionView that has 2 columns, and if number of items is odd, the last item should be center aligned. Then use this

    DNLastItemCenteredLayout

    - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
        NSArray *attributes = [super layoutAttributesForElementsInRect:rect];
    
        for (UICollectionViewLayoutAttributes *attribute in attributes) {
            NSInteger itemCount = [self.collectionView.dataSource collectionView:self.collectionView
                                                          numberOfItemsInSection:attribute.indexPath.section];
            if (itemCount % 2 == 1 && attribute.indexPath.item == itemCount - 1) {
                CGRect originalFrame = attribute.frame;
                attribute.frame = CGRectMake(self.collectionView.bounds.size.width/2-originalFrame.size.width/2,
                                             originalFrame.origin.y,
                                             originalFrame.size.width,
                                             originalFrame.size.height);
            }
        }
    
        return attributes;
    }
    
    0 讨论(0)
提交回复
热议问题