NSCollectionViewFlowLayout produces a layout with items justified on the right margin or, if the container is only wide enough for one item, centres items. I was expecting an alignment option, e.g. on the delegate, but am not finding anything in the docs. Does it require subclassing NSCollectionViewFlowLayout to achieve this?
NSCollectionViewFlowLayout - left alignment
Asked Answered
Here is a subclass that produces a left justified flow layout:
class LeftFlowLayout: NSCollectionViewFlowLayout {
override func layoutAttributesForElementsInRect(rect: CGRect) -> [NSCollectionViewLayoutAttributes] {
let defaultAttributes = super.layoutAttributesForElementsInRect(rect)
if defaultAttributes.isEmpty {
// we rely on 0th element being present,
// bail if missing (when there's no work to do anyway)
return defaultAttributes
}
var leftAlignedAttributes = [NSCollectionViewLayoutAttributes]()
var xCursor = self.sectionInset.left // left margin
// if/when there is a new row, we want to start at left margin
// the default FlowLayout will sometimes centre items,
// i.e. new rows do not always start at the left edge
var lastYPosition = defaultAttributes[0].frame.origin.y
for attributes in defaultAttributes {
if attributes.frame.origin.y > lastYPosition {
// we have changed line
xCursor = self.sectionInset.left
lastYPosition = attributes.frame.origin.y
}
attributes.frame.origin.x = xCursor
// by using the minimumInterimitemSpacing we no we'll never go
// beyond the right margin, so no further checks are required
xCursor += attributes.frame.size.width + minimumInteritemSpacing
leftAlignedAttributes.append(attributes)
}
return leftAlignedAttributes
}
}
That fix does not work when the next item has a height that is less than the preceding item. If Item1 starts at 0 and is 200.0 tall but Item2 is 100.0 tall, then Item2's Y-origin will be 50.0 (by default it's centered in the row). That will meet the ">lastYPosition" check and Item2's X-origin will be incorrectly set. The answer is to ask: "Is this item's Y-origin greater than the last Item's Y-origin PLUS the last item's height?" If yes, then we have started a new row. –
Ranket
@Obliquely's answer fails when the collectionViewItems
are not uniform in height. Here is their code modified to handle non-uniformly-sized items in Swift 4.2:
class CollectionViewLeftFlowLayout: NSCollectionViewFlowLayout
{
override func layoutAttributesForElements(in rect: CGRect) -> [NSCollectionViewLayoutAttributes]
{
let defaultAttributes = super.layoutAttributesForElements(in: rect)
if defaultAttributes.isEmpty {
return defaultAttributes
}
var leftAlignedAttributes = [NSCollectionViewLayoutAttributes]()
var xCursor = self.sectionInset.left // left margin
var lastYPosition = defaultAttributes[0].frame.origin.y // if/when there is a new row, we want to start at left margin
var lastItemHeight = defaultAttributes[0].frame.size.height
for attributes in defaultAttributes
{
// copy() Needed to avoid warning from CollectionView that cached values are mismatched
guard let newAttributes = attributes.copy() as? NSCollectionViewLayoutAttributes else {
continue;
}
if newAttributes.frame.origin.y > (lastYPosition + lastItemHeight)
{
// We have started a new row
xCursor = self.sectionInset.left
lastYPosition = newAttributes.frame.origin.y
}
newAttributes.frame.origin.x = xCursor
xCursor += newAttributes.frame.size.width + minimumInteritemSpacing
lastItemHeight = newAttributes.frame.size.height
leftAlignedAttributes.append(newAttributes)
}
return leftAlignedAttributes
}
}
A shorter solution for swift 4.2:
class CollectionViewLeftFlowLayout: NSCollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: NSRect) -> [NSCollectionViewLayoutAttributes] {
let attributes = super.layoutAttributesForElements(in: rect)
if attributes.isEmpty { return attributes }
var leftMargin = sectionInset.left
var lastYPosition = attributes[0].frame.maxY
for itemAttributes in attributes {
if itemAttributes.frame.origin.y > lastYPosition { // NewLine
leftMargin = sectionInset.left
}
itemAttributes.frame.origin.x = leftMargin
leftMargin += itemAttributes.frame.width + minimumInteritemSpacing
lastYPosition = itemAttributes.frame.maxY
}
return attributes
}
}
In case your items have the same width...
In the other delegate method, you should change the frame of the NSCollectionViewLayoutAttributes
- (NSCollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
NSCollectionViewLayoutAttributes *attributes = [[super layoutAttributesForItemAtIndexPath:indexPath] copy];
NSRect modifiedFrame = [attributes frame];
modifiedFrame.origin.x = floor(modifiedFrame.origin.x / (modifiedFrame.size.width + [self minimumInteritemSpacing])) * (modifiedFrame.size.width + [self minimumInteritemSpacing]);
[attributes setFrame:modifiedFrame];
return [attributes autorelease];
}
© 2022 - 2024 — McMap. All rights reserved.
if attributes.frame.origin.y != lastYPosition
should beif attributes.frame.origin.y > lastYPosition
to prevent a scenario where the new position is less than the last position. This causes UI elements to be drawn overlapping one another. (my edit was rejected - posting this fix as a comment instead) – Wanhsien