问题
I can't get scrolling both vertically and horizontally to work with a custom layout for NSCollectionView. According to the docs, in my subclass I return the `collectionViewContentSize' and if that is too big, scrolling is automatically enabled in the enclosing scroll view of the collection view. However, even if I order all elements in a horizontal row, only vertical scrolling is enabled.
Here is a screenshot: http://i.stack.imgur.com/tMbCN.png
Here is my layout code:
import Cocoa
class Layout: NSCollectionViewLayout {
var cellSize = CGSize(width: 100, height: 30)
var cellSpacing: CGFloat = 10
var sectionSpacing: CGFloat = 20
private var contentSize = CGSize.zero
private var layoutAttributes = [NSIndexPath: NSCollectionViewLayoutAttributes]()
override func prepareLayout() {
guard let collectionView = collectionView else { return }
let sections = collectionView.numberOfSections
guard sections > 0 else { return }
contentSize.height = cellSize.height
for section in 0..<sections {
let items = collectionView.numberOfItemsInSection(section)
guard items > 0 else { break }
for item in 0..<items {
let origin = CGPoint(x: contentSize.width, y: 0)
let indexPath = NSIndexPath(forItem: item, inSection: section)
let attributes = NSCollectionViewLayoutAttributes(forItemWithIndexPath: indexPath)
attributes.frame = CGRect(origin: origin, size: cellSize)
layoutAttributes[indexPath] = attributes
contentSize.width += cellSize.width + cellSpacing
}
contentSize.width += sectionSpacing
}
}
override var collectionViewContentSize: NSSize {
return contentSize
}
override func layoutAttributesForElementsInRect(rect: NSRect) -> [NSCollectionViewLayoutAttributes] {
return layoutAttributes.values.filter { $0.frame.intersects(rect) }
}
override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> NSCollectionViewLayoutAttributes? {
return layoutAttributes[indexPath]
}
override func shouldInvalidateLayoutForBoundsChange(newBounds: NSRect) -> Bool {
return false
}
}
回答1:
This bug is still ongoing. I've converted Bo Yuan's nasty hack to Swift. (No offence, Bo Yuan, it's not YOUR fault we have to do this horrible workaround! I'm still ripping this code out as soon as there's an official fix though. For the time being this is just going to keep my development efforts going.)
class HackedCollectionView: NSCollectionView {
override func setFrameSize(_ newSize: NSSize) {
let size = collectionViewLayout?.collectionViewContentSize ?? newSize
super.setFrameSize(size)
if let scrollView = enclosingScrollView {
scrollView.hasHorizontalScroller = size.width > scrollView.frame.width
}
}
}
Note that this definitely needs to go ASAP because it's getting called for every single frame of animation when scrolling. Not nice. Please, please, please someone either fix this issue or find a proper solution. Not being able to scroll horizontally is ridiculous.
回答2:
It's a basically bug in NSCollectionView. As a workaround you can implement the scrollDirection method (from NSCollectionViewFlowLayout) and return NSCollectionViewScrollDirectionHorizontal.
回答3:
I figured out a solution. I created a subclass of NSCollectionView. And use this subclass to replace the default NSCollectionView. I created function to override the setFrameSize. Voila!
-(void)setFrameSize:(NSSize)newSize{
if (newSize.width != self.collectionViewLayout.collectionViewContentSize.width) {
newSize.width = self.collectionViewLayout.collectionViewContentSize.width;
}
[super setFrameSize:newSize];
//NSLog(@"setFrameSize%@", CGSizeCreateDictionaryRepresentation(newSize));
}
During my test, the setFrameSize will be invoked and some mechanism set the frame size to {0,0} first then set it again to make the width the same width as the clipview and keep the height from the layout's content size.
回答4:
It seems that only NSCollectionViewFlowLayout is able to dictate a frame that has a width larger than the parent scroll views frame.
The solution is to subclass NSCollectionViewFlowLayout instead of NSCollectionViewLayout. Treat the subclass like any other layout subclass, but add the critical scrollDirection in prepareLayout().
Here is minimum implementation for a layout that scrolls horizontal, and just sets all the items next to one another.
-(void) prepareLayout
{
[super prepareLayout];
self.scrollDirection = NSCollectionViewScrollDirectionHorizontal;
}
-(NSSize) collectionViewContentSize
{
NSSize itemSize = self.itemSize;
NSInteger num = [self.collectionView numberOfItemsInSection:0];
NSSize contentSize = NSMakeSize((num * itemSize.width) + ((num+1) * self.minimumLineSpacing), NSHeight(self.collectionView.frame));
return contentSize;
}
-(BOOL) shouldInvalidateLayoutForBoundsChange:(NSRect)newBounds { return YES; }
-(NSArray<__kindof NSCollectionViewLayoutAttributes *> *) layoutAttributesForElementsInRect:(NSRect)rect
{
int numItems = [self.collectionView numberOfItemsInSection:0];
NSMutableArray* attributes = [NSMutableArray arrayWithCapacity:numItems];
for (int i=0; i<numItems; i++)
{
[attributes addObject:[self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:i inSection:0]]];
}
return attributes;
}
-(NSCollectionViewLayoutAttributes*) layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
NSSize itemSize = self.itemSize;
NSRect fr = NSZeroRect;
fr.size = itemSize;
fr.origin.y = (NSHeight(self.collectionView.frame) - itemSize.height) / 2.0;
fr.origin.x = (indexPath.item+1 * self.minimumLineSpacing) + (indexPath.item * itemSize.width);
NSCollectionViewLayoutAttributes* attr = [NSCollectionViewLayoutAttributes layoutAttributesForItemWithIndexPath:indexPath];
attr.frame = fr;
return attr;
}
来源:https://stackoverflow.com/questions/38016263/nscollectionview-custom-layout-enable-scrolling