如何将 UICollectionView 的单元格居中对齐?

我目前使用 UICollectionView作为用户界面网格,它工作得很好。但是,我希望能够启用水平滚动。网格支持每页8个项目,当项目总数为,比如说4,这是项目应该如何排列,启用水平滚动方向:

0 0 x x
0 0 x x

此处0-> 收集项目 和 x-> 空牢房

有没有办法让它们居中对齐,比如:

x 0 0 x
x 0 0 x

这样内容看起来更干净?

下面的安排也可能是一个解决方案,我期待。

0 0 0 0
x x x x
110110 次浏览

I think you can achieve the single line look by implementing something like this:

- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {
return UIEdgeInsetsMake(0, 100, 0, 0);
}

You will have to play around with that number to figure out how to force the content into a single line. The first 0, is the top edge argument, you could adjust that one too, if you want to center the content vertically in the screen.

I have a tag bar in my app which uses an UICollectionView & UICollectionViewFlowLayout, with one row of cells center aligned.

To get the correct indent, you subtract the total width of all the cells (including spacing) from the width of your UICollectionView, and divide by two.

[........Collection View.........]
[..Cell..][..Cell..]
[____indent___] / 2


=


[_____][..Cell..][..Cell..][_____]

The problem is this function -

- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section;

is called before...

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath;

...so you can't iterate over your cells to determine the total width.

Instead you need to calculate the width of each cell again, in my case I use [NSString sizeWithFont: ... ] as my cell widths are determined by the UILabel itself.

- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section
{
CGFloat rightEdge = 0;
CGFloat interItemSpacing = [(UICollectionViewFlowLayout*)collectionViewLayout minimumInteritemSpacing];


for(NSString * tag in _tags)
rightEdge += [tag sizeWithFont:[UIFont systemFontOfSize:14]].width+interItemSpacing;


// To center the inter spacing too
rightEdge -= interSpacing/2;


// Calculate the inset
CGFloat inset = collectionView.frame.size.width-rightEdge;


// Only center align if the inset is greater than 0
// That means that the total width of the cells is less than the width of the collection view and need to be aligned to the center.
// Otherwise let them align left with no indent.


if(inset > 0)
return UIEdgeInsetsMake(0, inset/2, 0, 0);
else
return UIEdgeInsetsMake(0, 0, 0, 0);
}

Not sure if this is new in Xcode 5 or not, but you can now open the Size Inspector through Interface Builder and set an inset there. That'll prevent you from having to write custom code to do this for you and should drastically increase the speed at which you find the proper offsets.

It's easy to calculate insets dynamically, this code will always center your cells:

NSInteger const SMEPGiPadViewControllerCellWidth = 332;


...


- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section
{


NSInteger numberOfCells = self.view.frame.size.width / SMEPGiPadViewControllerCellWidth;
NSInteger edgeInsets = (self.view.frame.size.width - (numberOfCells * SMEPGiPadViewControllerCellWidth)) / (numberOfCells + 1);


return UIEdgeInsetsMake(0, edgeInsets, 0, edgeInsets);
}


- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
[super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
[self.collectionView.collectionViewLayout invalidateLayout];
}

The top solutions here did not work for me out-of-the-box, so I came up with this which should work for any horizontal scrolling collection view with flow layout and only one section:

- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section
{
// Add inset to the collection view if there are not enough cells to fill the width.
CGFloat cellSpacing = ((UICollectionViewFlowLayout *) collectionViewLayout).minimumLineSpacing;
CGFloat cellWidth = ((UICollectionViewFlowLayout *) collectionViewLayout).itemSize.width;
NSInteger cellCount = [collectionView numberOfItemsInSection:section];
CGFloat inset = (collectionView.bounds.size.width - (cellCount * cellWidth) - ((cellCount - 1)*cellSpacing)) * 0.5;
inset = MAX(inset, 0.0);
return UIEdgeInsetsMake(0.0, inset, 0.0, 0.0);
}

Put this into your collection view delegate. It considers more of the the basic flow layout settings than the other answers and is thereby more generic.

- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section
{
NSInteger cellCount = [collectionView.dataSource collectionView:collectionView numberOfItemsInSection:section];
if( cellCount >0 )
{
CGFloat cellWidth = ((UICollectionViewFlowLayout*)collectionViewLayout).itemSize.width+((UICollectionViewFlowLayout*)collectionViewLayout).minimumInteritemSpacing;
CGFloat totalCellWidth = cellWidth*cellCount + spacing*(cellCount-1);
CGFloat contentWidth = collectionView.frame.size.width-collectionView.contentInset.left-collectionView.contentInset.right;
if( totalCellWidth<contentWidth )
{
CGFloat padding = (contentWidth - totalCellWidth) / 2.0;
return UIEdgeInsetsMake(0, padding, 0, padding);
}
}
return UIEdgeInsetsZero;
}

swift version (thanks g0ld2k):

extension CommunityConnectViewController: UICollectionViewDelegateFlowLayout {
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAtIndex section: Int) -> UIEdgeInsets {


// Translated from Objective-C version at: http://stackoverflow.com/a/27656363/309736


let cellCount = CGFloat(viewModel.getNumOfItemsInSection(0))


if cellCount > 0 {
let flowLayout = collectionViewLayout as! UICollectionViewFlowLayout
let cellWidth = flowLayout.itemSize.width + flowLayout.minimumInteritemSpacing
let totalCellWidth = cellWidth*cellCount + spacing*(cellCount-1)
let contentWidth = collectionView.frame.size.width - collectionView.contentInset.left - collectionView.contentInset.right


if (totalCellWidth < contentWidth) {
let padding = (contentWidth - totalCellWidth) / 2.0
return UIEdgeInsetsMake(0, padding, 0, padding)
}
}


return UIEdgeInsetsZero
}
}

You can use https://github.com/keighl/KTCenterFlowLayout like this:

KTCenterFlowLayout *layout = [[KTCenterFlowLayout alloc] init];


[self.collectionView setCollectionViewLayout:layout];

Similar to other answers, this is a dynamic answer that should work for static sized cells. The one modification I made is that I put the padding on both sides. If I didn't do this, I experienced problems.


Objective-C

- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewFlowLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {


NSInteger numberOfItems = [collectionView numberOfItemsInSection:section];
CGFloat combinedItemWidth = (numberOfItems * collectionViewLayout.itemSize.width) + ((numberOfItems - 1) * collectionViewLayout.minimumInteritemSpacing);
CGFloat padding = (collectionView.frame.size.width - combinedItemWidth) / 2;


return UIEdgeInsetsMake(0, padding, 0, padding);
}

Swift

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
let flowLayout = collectionViewLayout as! UICollectionViewFlowLayout
let numberOfItems = CGFloat(collectionView.numberOfItems(inSection: section))
let combinedItemWidth = (numberOfItems * flowLayout.itemSize.width) + ((numberOfItems - 1)  * flowLayout.minimumInteritemSpacing)
let padding = (collectionView.frame.width - combinedItemWidth) / 2
return UIEdgeInsets(top: 0, left: padding, bottom: 0, right: padding)
}

Also, if you are still are experiencing problems, make sure that you set both the minimumInteritemSpacing and minimumLineSpacing to the same values since these values seem to be interrelated.

UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout;
flowLayout.minimumInteritemSpacing = 20.0;
flowLayout.minimumLineSpacing = 20.0;

In Swift, the following will distribute cells evenly by applying the correct amount of padding on the sides of each cell. Of course, you need to know/set your cell width first.

func collectionView(collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
insetForSectionAtIndex section: Int) -> UIEdgeInsets {


var cellWidth : CGFloat = 110;


var numberOfCells = floor(self.view.frame.size.width / cellWidth);
var edgeInsets = (self.view.frame.size.width - (numberOfCells * cellWidth)) / (numberOfCells + 1);


return UIEdgeInsetsMake(0, edgeInsets, 60.0, edgeInsets);
}

Another way of doing this is setting the collectionView.center.x, like so:

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()


if let
collectionView = collectionView,
layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout
{
collectionView.center.x = view.center.x + layout.sectionInset.right / 2.0
}
}

In this case, only respecting the right inset which works for me.

Swift 2.0 Works fine for me!

 func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAtIndex section: Int) -> UIEdgeInsets {
let edgeInsets = (screenWight - (CGFloat(elements.count) * 50) - (CGFloat(elements.count) * 10)) / 2
return UIEdgeInsetsMake(0, edgeInsets, 0, 0);
}

Where: screenWight: basically its my collection's width (full screen width) - I made constants: let screenWight:CGFloat = UIScreen.mainScreen().bounds.width because self.view.bounds shows every-time 600 - coz of SizeClasses elements - array of cells 50 - my manual cell width 10 - my distance between cells

My solution for static sized collection view cells which need to have padding on left and right-

func collectionView(collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
insetForSectionAtIndex section: Int) -> UIEdgeInsets {


let flowLayout = (collectionViewLayout as! UICollectionViewFlowLayout)
let cellSpacing = flowLayout.minimumInteritemSpacing
let cellWidth = flowLayout.itemSize.width
let cellCount = CGFloat(collectionView.numberOfItemsInSection(section))


let collectionViewWidth = collectionView.bounds.size.width


let totalCellWidth = cellCount * cellWidth
let totalCellSpacing = cellSpacing * (cellCount - 1)


let totalCellsWidth = totalCellWidth + totalCellSpacing


let edgeInsets = (collectionViewWidth - totalCellsWidth) / 2.0


return edgeInsets > 0 ? UIEdgeInsetsMake(0, edgeInsets, 0, edgeInsets) : UIEdgeInsetsMake(0, cellSpacing, 0, cellSpacing)
}

Based on user3676011 answer, I can suggest more detailed one with small correction. This solution works great on Swift 2.0.

enum CollectionViewContentPosition {
case Left
case Center
}


var collectionViewContentPosition: CollectionViewContentPosition = .Left


func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout,
insetForSectionAtIndex section: Int) -> UIEdgeInsets {


if collectionViewContentPosition == .Left {
return UIEdgeInsetsZero
}


// Center collectionView content


let itemsCount: CGFloat = CGFloat(collectionView.numberOfItemsInSection(section))
let collectionViewWidth: CGFloat = collectionView.bounds.width


let itemWidth: CGFloat = 40.0
let itemsMargin: CGFloat = 10.0


let edgeInsets = (collectionViewWidth - (itemsCount * itemWidth) - ((itemsCount-1) * itemsMargin)) / 2


return UIEdgeInsetsMake(0, edgeInsets, 0, 0)
}

There was an issue in

(CGFloat(elements.count) * 10))

where should be additional -1 mentioned.

For those looking for a solution to center-aligned, dynamic-width collectionview cells, as I was, I ended up modifying Angel's answer for a left-aligned version to create a center-aligned subclass for UICollectionViewFlowLayout.

CenterAlignedCollectionViewFlowLayout

// NOTE: Doesn't work for horizontal layout!
class CenterAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {
    

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let superAttributes = super.layoutAttributesForElements(in: rect) else { return nil }
// Copy each item to prevent "UICollectionViewFlowLayout has cached frame mismatch" warning
guard let attributes = NSArray(array: superAttributes, copyItems: true) as? [UICollectionViewLayoutAttributes] else { return nil }
        

// Constants
let leftPadding: CGFloat = 8
let interItemSpacing = minimumInteritemSpacing
        

// Tracking values
var leftMargin: CGFloat = leftPadding // Modified to determine origin.x for each item
var maxY: CGFloat = -1.0 // Modified to determine origin.y for each item
var rowSizes: [[CGFloat]] = [] // Tracks the starting and ending x-values for the first and last item in the row
var currentRow: Int = 0 // Tracks the current row
attributes.forEach { layoutAttribute in
            

// Each layoutAttribute represents its own item
if layoutAttribute.frame.origin.y >= maxY {
                

// This layoutAttribute represents the left-most item in the row
leftMargin = leftPadding
                

// Register its origin.x in rowSizes for use later
if rowSizes.count == 0 {
// Add to first row
rowSizes = [[leftMargin, 0]]
} else {
// Append a new row
rowSizes.append([leftMargin, 0])
currentRow += 1
}
}
            

layoutAttribute.frame.origin.x = leftMargin
            

leftMargin += layoutAttribute.frame.width + interItemSpacing
maxY = max(layoutAttribute.frame.maxY, maxY)
            

// Add right-most x value for last item in the row
rowSizes[currentRow][1] = leftMargin - interItemSpacing
}
        

// At this point, all cells are left aligned
// Reset tracking values and add extra left padding to center align entire row
leftMargin = leftPadding
maxY = -1.0
currentRow = 0
attributes.forEach { layoutAttribute in
            

// Each layoutAttribute is its own item
if layoutAttribute.frame.origin.y >= maxY {
                

// This layoutAttribute represents the left-most item in the row
leftMargin = leftPadding
                

// Need to bump it up by an appended margin
let rowWidth = rowSizes[currentRow][1] - rowSizes[currentRow][0] // last.x - first.x
let appendedMargin = (collectionView!.frame.width - leftPadding  - rowWidth - leftPadding) / 2
leftMargin += appendedMargin
                

currentRow += 1
}
            

layoutAttribute.frame.origin.x = leftMargin
            

leftMargin += layoutAttribute.frame.width + interItemSpacing
maxY = max(layoutAttribute.frame.maxY, maxY)
}
        

return attributes
}
}

CenterAlignedCollectionViewFlowLayout

This is how i obtained a collection view that was center aligned with a fixed Interitem spacing

#define maxInteritemSpacing 6
#define minLineSpacing 3


@interface MyFlowLayout()<UICollectionViewDelegateFlowLayout>


@end


@implementation MyFlowLayout


- (instancetype)init
{
self = [super init];
if (self) {
self.minimumInteritemSpacing = 3;
self.minimumLineSpacing = minLineSpacing;
self.scrollDirection = UICollectionViewScrollDirectionVertical;
}
return self;
}


- (NSArray *) layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *answer = [super layoutAttributesForElementsInRect:rect];


if (answer.count > 0) {
NSMutableArray<NSMutableArray<UICollectionViewLayoutAttributes *> *> *rows = [NSMutableArray array];
CGFloat maxY = -1.0f;
NSInteger currentRow = 0;


//add first item to row and set its maxY
[rows addObject:[@[answer[0]] mutableCopy]];
maxY = CGRectGetMaxY(((UICollectionViewLayoutAttributes *)answer[0]).frame);


for(int i = 1; i < [answer count]; ++i) {
//handle maximum spacing
UICollectionViewLayoutAttributes *currentLayoutAttributes = answer[i];
UICollectionViewLayoutAttributes *prevLayoutAttributes = answer[i - 1];
NSInteger maximumSpacing = maxInteritemSpacing;
NSInteger origin = CGRectGetMaxX(prevLayoutAttributes.frame);


if(origin + maximumSpacing + currentLayoutAttributes.frame.size.width < self.collectionViewContentSize.width) {
CGRect frame = currentLayoutAttributes.frame;
frame.origin.x = origin + maximumSpacing;
currentLayoutAttributes.frame = frame;
}


//handle center align
CGFloat currentY = currentLayoutAttributes.frame.origin.y;
if (currentY >= maxY) {
[self shiftRowToCenterForCurrentRow:rows[currentRow]];


//next row
[rows addObject:[@[currentLayoutAttributes] mutableCopy]];
currentRow++;
}
else {
//same row
[rows[currentRow] addObject:currentLayoutAttributes];
}


maxY = MAX(CGRectGetMaxY(currentLayoutAttributes.frame), maxY);
}


//mark sure to shift 1 row items
if (currentRow == 0) {
[self shiftRowToCenterForCurrentRow:rows[currentRow]];
}
}


return answer;
}


- (void)shiftRowToCenterForCurrentRow:(NSMutableArray<UICollectionViewLayoutAttributes *> *)currentRow
{
//shift row to center
CGFloat endingX = CGRectGetMaxX(currentRow.lastObject.frame);
CGFloat shiftX = (self.collectionViewContentSize.width - endingX) / 2.f;
for (UICollectionViewLayoutAttributes *attr in currentRow) {
CGRect shiftedFrame = attr.frame;
shiftedFrame.origin.x += shiftX;
attr.frame = shiftedFrame;
}
}


@end

Working version of kgaidis's Objective C answer using Swift 3.0:

let flow = UICollectionViewFlowLayout()


func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
let numberOfItems = collectionView.numberOfItems(inSection: 0)
let combinedItemWidth:CGFloat = (CGFloat(numberOfItems) * flow.itemSize.width) + ((CGFloat(numberOfItems) - 1) * flow.minimumInteritemSpacing)
let padding = (collectionView.frame.size.width - combinedItemWidth) / 2


return UIEdgeInsetsMake(0, padding, 0, padding)
}

Here is how you can do it and it works fine

func refreshCollectionView(_ count: Int) {
let collectionViewHeight = collectionView.bounds.height
let collectionViewWidth = collectionView.bounds.width
let numberOfItemsThatCanInCollectionView = Int(collectionViewWidth / collectionViewHeight)
if numberOfItemsThatCanInCollectionView > count {
let totalCellWidth = collectionViewHeight * CGFloat(count)
let totalSpacingWidth: CGFloat = CGFloat(count) * (CGFloat(count) - 1)
// leftInset, rightInset are the global variables which I am passing to the below function
leftInset = (collectionViewWidth - CGFloat(totalCellWidth + totalSpacingWidth)) / 2;
rightInset = -leftInset
} else {
leftInset = 0.0
rightInset = -collectionViewHeight
}
collectionView.reloadData()
}


func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsetsMake(0, leftInset, 0, rightInset)
}

Here is my solution with a few assumptions:

  • there is only one section
  • left and right insets are equal
  • cell height is the same

Feel free to adjust to meet your needs.

Centered layout with variable cell width:

protocol HACenteredLayoutDelegate: UICollectionViewDataSource {
func getCollectionView() -> UICollectionView
func sizeOfCell(at index: IndexPath) -> CGSize
func contentInsets() -> UIEdgeInsets
}


class HACenteredLayout: UICollectionViewFlowLayout {
weak var delegate: HACenteredLayoutDelegate?
private var cache = [UICollectionViewLayoutAttributes]()
private var contentSize = CGSize.zero
override var collectionViewContentSize: CGSize { return self.contentSize }


required init(delegate: HACenteredLayoutDelegate) {
self.delegate = delegate
super.init()
}


required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}


override func invalidateLayout() {
cache.removeAll()
super.invalidateLayout()
}


override func prepare() {
if cache.isEmpty && self.delegate != nil && self.delegate!.collectionView(self.delegate!.getCollectionView(), numberOfItemsInSection: 0) > 0 {
let insets = self.delegate?.contentInsets() ?? UIEdgeInsets.zero
var rows: [(width: CGFloat, count: Int)] = [(0, 0)]
let viewWidth: CGFloat = UIScreen.main.bounds.width
var y = insets.top
var unmodifiedIndexes = [IndexPath]()
for itemNumber in 0 ..< self.delegate!.collectionView(self.delegate!.getCollectionView(), numberOfItemsInSection: 0) {
let indexPath = IndexPath(item: itemNumber, section: 0)
let cellSize = self.delegate!.sizeOfCell(at: indexPath)
let potentialRowWidth = rows.last!.width + (rows.last!.count > 0 ? self.minimumInteritemSpacing : 0) + cellSize.width + insets.right + insets.left
if potentialRowWidth > viewWidth {
let leftOverSpace = max((viewWidth - rows[rows.count - 1].width)/2, insets.left)
for i in unmodifiedIndexes {
self.cache[i.item].frame.origin.x += leftOverSpace
}
unmodifiedIndexes = []
rows.append((0, 0))
y += cellSize.height + self.minimumLineSpacing
}
unmodifiedIndexes.append(indexPath)
let attribute = UICollectionViewLayoutAttributes(forCellWith: indexPath)
rows[rows.count - 1].count += 1
rows[rows.count - 1].width += rows[rows.count - 1].count > 1 ? self.minimumInteritemSpacing : 0
attribute.frame = CGRect(x: rows[rows.count - 1].width, y: y, width: cellSize.width, height: cellSize.height)
rows[rows.count - 1].width += cellSize.width
cache.append(attribute)
}
let leftOverSpace = max((viewWidth - rows[rows.count - 1].width)/2, insets.left)
for i in unmodifiedIndexes {
self.cache[i.item].frame.origin.x += leftOverSpace
}
self.contentSize = CGSize(width: viewWidth, height: y + self.delegate!.sizeOfCell(at: IndexPath(item: 0, section: 0)).height + insets.bottom)
}
}


override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttributes = [UICollectionViewLayoutAttributes]()


for attributes in cache {
if attributes.frame.intersects(rect) {
layoutAttributes.append(attributes)
}
}
return layoutAttributes
}


override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
if indexPath.item < self.cache.count {
return self.cache[indexPath.item]
}
return nil
}
}

Result:

enter image description here