我无法垂直和水平滚动来使用
NSCollectionView
的自定义布局。
根据文档,在我的子类中,我返回 collectionViewContentSize
,如果太大,则会在集合视图的封闭滚动视图中自动启用滚动。但是,即使我将所有元素排列在水平行中,也只能启用垂直滚动。
这是我的布局代码:
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
}
}
这个错误仍然存在。我已经将 Bo Yuan 的令人讨厌的 hack 转换为 Swift。 (无意冒犯,博源,这不是你的错,我们必须做这个可怕的解决方法!不过,一旦有官方修复,我仍然会立即删除这段代码。目前,这只是为了让我的开发工作继续下去.)
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
}
}
}
请注意,这绝对需要尽快进行,因为滚动时每一个动画帧都会调用它。不太好。拜托,拜托,请有人解决这个问题或找到适当的解决方案。无法水平滚动是荒谬的。
似乎只有 NSCollectionViewFlowLayout 能够指定宽度大于父滚动视图框架的框架。
解决方案是子类化 NSCollectionViewFlowLayout 而不是 NSCollectionViewLayout。像对待任何其他布局子类一样对待该子类,但在prepareLayout()中添加关键的scrollDirection。
这是水平滚动布局的最小实现,只需将所有项目设置为彼此相邻。
-(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;
}
确保您的自定义布局类响应
scrollDirection
消息(这意味着它需要暴露给 objc 运行时):
class MyCustomLayout: NSCollectionViewLayout {
@objc var scrollDirection: NSCollectionView.ScrollDirection {
.horizontal
}
}
花了几个小时后,我知道
NSCollectionViewLayout
类中一定有一个技巧,可以让集合视图知道它应该在水平方向调整自身大小。并且有一个内置布局使用的技巧,未记录并隐藏在 NSCollectionView
的实现中(您可以在调试器中看到堆栈跟踪)。
在 collectionViewContentSize
的布局方法运行期间,会多次调用
NSCollectionView
方法。有一种私有方法称为 NSCollectionView._resizeToFitContentAndClipView
。该方法的名称确实具有解释性,但如果您查看实现细节(调试器中的反汇编代码),您就会明白。首先,它获取布局类实例并检查该实例是否响应 scrollDirection
选择器,以及是否根据该选择器返回的值响应此方法的行为更改。
阅读反汇编代码很有趣。由于 ObjC 的选择器名称没有被破坏,因此该过程有很多提示。另外,我认为必须有一个选项来启用特殊的集合视图调试,因为它会输出一些日志消息,例如
%@(%p): -_resizeToFitContentAndClipView found layout=%@, scrollDirection=%ld
,但我不知道如何启用这些调试消息。
这基本上是 NSCollectionView 中的一个错误。作为解决方法,您可以实现scrollDirection方法(来自NSCollectionViewFlowLayout)并返回NSCollectionViewScrollDirectionHorizontal。
我想出了一个解决办法。我创建了 NSCollectionView 的子类。并使用这个子类来替换默认的NSCollectionView。我创建了函数来覆盖 setFrameSize。瞧!
-(void)setFrameSize:(NSSize)newSize{
if (newSize.width != self.collectionViewLayout.collectionViewContentSize.width) {
newSize.width = self.collectionViewLayout.collectionViewContentSize.width;
}
[super setFrameSize:newSize];
//NSLog(@"setFrameSize%@", CGSizeCreateDictionaryRepresentation(newSize));
}
在我的测试过程中,将调用 setFrameSize ,并且某些机制首先将帧大小设置为 {0,0},然后再次设置它以使宽度与 Clipview 的宽度相同,并保持布局内容大小的高度。
这是我的解决方案,类似于 Umur Gedik 的答案
#import <Cocoa/Cocoa.h>
#import <objc/message.h>
OBJC_EXPORT id objc_msgSendSuper2(void);
@interface _CollectionView : NSCollectionView
@end
@implementation _CollectionView
- (BOOL)_autoConfigureScrollers {
return NO;
}
- (void)_resizeToFitContentAndClipView {
struct objc_super superInfo = { self, [self class] };
((void (*)(struct objc_super *, SEL))objc_msgSendSuper2)(&superInfo, _cmd);
[self setFrameSize:self.collectionViewLayout.collectionViewContentSize];
}
@end