重新排序 UICollectionView 的单元格

Posted

技术标签:

【中文标题】重新排序 UICollectionView 的单元格【英文标题】:Reorder cells of UICollectionView 【发布时间】:2013-03-30 13:43:09 【问题描述】:

考虑一个具有流式布局和水平方向的UICollectionView。默认情况下,单元格按从上到下、从左到右的顺序排列。像这样:

1 4 7 10 13 16
2 5 8 11 14 17
3 6 9 12 15 18

在我的例子中,collection view 是分页的,并且它的设计使得特定数量的单元格适合每个页面。因此,更自然的排序是:

1 2 3   10 11 12
4 5 6 - 13 14 15
7 8 9   16 17 18

除了实现我自己的自定义布局之外,最简单的实现方式是什么?特别是,我不想放弃 UICollectionViewFlowLayout 免费提供的任何功能(例如插入/删除动画)。

或者一般来说,您如何在流布局上实现重新排序功能f(n)?例如,这同样适用于从右到左的排序。

到目前为止我的方法

我的第一种方法是继承 UICollectionViewFlowLayout 并覆盖 layoutAttributesForItemAtIndexPath:

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath

    NSIndexPath *reorderedIndexPath = [self reorderedIndexPathOfIndexPath:indexPath];
    UICollectionViewLayoutAttributes *layout = [super layoutAttributesForItemAtIndexPath:reorderedIndexPath];
    layout.indexPath = indexPath;
    return layout;

reorderedIndexPathOfIndexPath:f(n)。通过调用super,我不必手动计算每个元素的布局。

此外,我必须重写 layoutAttributesForElementsInRect:,这是布局用来选择要显示哪些元素的方法。

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect

    NSMutableArray *result = [NSMutableArray array];
    NSInteger sectionCount = 1;
    if ([self.collectionView.dataSource respondsToSelector:@selector(numberOfSectionsInCollectionView:)])
    
        sectionCount = [self.collectionView.dataSource numberOfSectionsInCollectionView:self.collectionView];
    
    for (int s = 0; s < sectionCount; s++)
    
        NSInteger itemCount = [self.collectionView.dataSource collectionView:self.collectionView numberOfItemsInSection:s];
        for (int i = 0; i < itemCount; i++)
        
            NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:s];
            UICollectionViewLayoutAttributes *layout = [self layoutAttributesForItemAtIndexPath:indexPath];
            if (CGRectIntersectsRect(rect, layout.frame))
            
                [result addObject:layout];
            
        
    
    return result;

在这里我只是尝试每个元素,如果它在给定的rect 内,我会返回它。

如果这种方法是可行的方法,我有以下更具体的问题:

有什么方法可以简化 layoutAttributesForElementsInRect: 覆盖,或者提高效率? 我错过了什么吗?至少交换不同页面的单元格会产生奇怪的结果。我怀疑它与initialLayoutAttributesForAppearingItemAtIndexPath:finalLayoutAttributesForDisappearingItemAtIndexPath: 有关,但我无法确定到底是什么问题。 就我而言,f(n) 取决于每页的列数和行数。有没有办法从UICollectionViewFlowLayout 中提取这些信息,而不是自己硬编码?我想到了 querying layoutAttributesForElementsInRect: 与集合视图的边界,并从那里推导出行和列,但这也感觉效率低下。

【问题讨论】:

我想知道在分页滚动视图中拥有多个集合视图是否更容易,这样第一个集合视图将包含水平布局的项目 1 到 9,第二个集合视图包含项目 10 -18 等 @rdelmar 感觉就像重新实现了很多我通过UICollectionView 免费获得的东西。不过,没有尝试将集合分成几部分。 不幸的是,您似乎必须重新实现很多...请参阅我的回答。但至少它会非常可重用。 :) 【参考方案1】:

我对您的问题考虑了很多,并得出以下考虑:

子类化 FlowLayout 似乎是对单元格重新排序和利用流布局动画的最正确和最有效的方法。而且你的方法很有效,除了两件重要的事情:

    假设您有一个只有 2 个单元格的集合视图,并且您已将页面设计为可以包含 9 个单元格。第一个单元格将位于视图的左上角,就像在原始流布局中一样。但是,您的第二个单元格应位于视图的顶部,并且它具有索引路径 [0, 1]。重新排序的索引路径将是 [0, 3](将在其位置上的原始流布局单元的索引路径)。在你的layoutAttributesForItemAtIndexPath 覆盖中,你会发送类似[super layoutAttributesForItemAtIndexPath:[0, 3]] 的消息,你会得到一个nil 对象,因为只有2 个单元格:[0,0] 和[0,1]。这将是您最后一页的问题。 尽管您可以通过覆盖targetContentOffsetForProposedContentOffset:withScrollingVelocity: 并手动设置itemSizeminimumLineSpacingminimumInteritemSpacing 等属性来实现分页行为,但要使您的项目对称、定义分页边界等仍然需要做很多工作开。

我认为,子类化流布局正在为您准备很多实现,因为您想要的不再是流布局。但是让我们一起考虑一下。 关于您的问题:

您的layoutAttributesForElementsInRect: 覆盖正是原始苹果实现的方式,因此无法简化它。但是,对于您的情况,您可以考虑以下内容:如果每页有 3 行项目,并且第一行中的项目框架与矩形框架相交,那么(如果所有项目的大小相同)第二和第三行项目的框架与此矩形相交。 对不起,我没听懂你的第二个问题 在我的例子中,重新排序函数如下所示:(a 是 整数 每页上的行/列数,rows=columns)

f(n) = (n % a²) + (a - 1)(col - row) + a²(n / a²); col = (n % a²) % a;行 = (n % a²) / a;

在回答这个问题时,流布局不知道每列中有多少行,因为这个数字可能因每个项目的大小而异。它也可以不说每页上的列数,因为它取决于滚动位置并且也可以变化。因此,没有比查询layoutAttributesForElementsInRect 更好的方法了,但这也包括单元格,这些单元格仅部分可见。由于您的单元格大小相同,理论上您可以通过水平滚动方向了解您的集合视图有多少行:通过开始迭代每个单元格对它们进行计数并在它们的frame.origin.x 更改时中断。

所以,我认为,您有两种选择来实现您的目的:

    子类UICollectionViewLayout。实现所有这些方法似乎需要做很多工作,但这是唯一有效的方法。例如,您可以拥有itemSizeitemsInOneRow 等属性。然后你可以很容易地找到一个公式来根据它的编号计算每个项目的框架(最好的方法是在 prepareLayout 中执行它并将所有框架存储在数组中,这样你就可以在 layoutAttributesForItemAtIndexPath 中访问你需要的框架)。实现layoutAttributesForItemAtIndexPathlayoutAttributesForItemsInRectcollectionViewContentSize 也非常简单。在initialLayoutAttributesForAppearingItemAtIndexPathfinalLayoutAttributesForDisappearingItemAtIndexPath 中,您只需将 alpha 属性设置为 0.0。这就是标准流布局动画的工作原理。通过覆盖targetContentOffsetForProposedContentOffset:withScrollingVelocity:,您可以实现“分页行为”。

    考虑制作一个带有流式布局的集合视图,pagingEnabled = YES,水平滚动方向和项目大小等于屏幕大小。每个屏幕尺寸一个项目。对于每个单元格,您可以将新的集合视图设置为具有垂直流布局的子视图以及与其他集合视图相同的数据源,但具有偏移量。它非常有效,因为这样您就可以重用包含 9 个(或其他)单元格块的整个集合视图,而不是使用标准方法重用每个单元格。所有动画都应该可以正常工作。

Here 您可以使用布局子类化方法下载示例项目。 (#2)

【讨论】:

感谢博格丹的回复。你确定 layoutAttributesForItemAtIndexPath: 不存在的返回 nil 吗?我相信我试过了,布局仍然计算了属性。 方法 2 在理论上听起来很性感,但它不允许我轻松地将单元格从一页移动到另一页。这和你第一次回复的问题一样。 我不明白为什么我需要覆盖targetContentOffsetForProposedContentOffset:withScrollingVelocity。将 pagingEnabled 设置为 YES 就可以了。 不客气。当我尝试它时,没有可见的单元格,重新排序的索引路径引用了一个不存在的单元格。也许它对你有用,因为你比我有另一个重新排序功能。如果pagingEnabled = YES 有效,那么您当然不必自己实现分页。我想,如果你将pagingEnabled 设置为 YES,滚动视图只会停留在每个单元格的边界上,这不是你想要的(是吗?)。我认为,即使您尝试将单元格从一个视图移动到另一个视图,方法 #2 也应该有效。我稍后会尝试一下。尝试方法#1,真的很值得 我一定遗漏了一些东西,但我不明白方法 1 与我已经在做的有什么不同?【参考方案2】:

拥有 2 个具有标准 UICollectionViewFlowLayout 的集合视图不是一个简单的解决方案吗?

甚至更好:拥有一个水平滚动的页面视图控制器,每个页面都是一个具有正常流布局的集合视图。

这个想法如下:在您的UICollectionViewController -init method 中,您创建第二个集合视图,其框架向右偏移原始集合视图宽度。然后将其作为子视图添加到原始集合视图。要在集合视图之间切换,只需添加一个滑动识别器。要计算偏移值,您可以将集合视图的原始帧存储在 ivar cVFrame 中。要识别您的集合视图,您可以使用标签。

init 方法示例:

CGRect cVFrame = self.collectionView.frame;
UICollectionView *secondView = [[UICollectionView alloc] 
              initWithFrame:CGRectMake(cVFrame.origin.x + cVFrame.size.width, 0, 
                             cVFrame.size.width, cVFrame.size.height) 
       collectionViewLayout:[UICollectionViewFlowLayout new]];
    [secondView setBackgroundColor:[UIColor greenColor]];
    [secondView setTag:1];
    [secondView setDelegate:self];
    [secondView setDataSource:self];
    [self.collectionView addSubview:secondView];

UISwipeGestureRecognizer *swipeRight = [[UISwipeGestureRecognizer alloc]
                      initWithTarget:self action:@selector(swipedRight)];
    [swipeRight setDirection:UISwipeGestureRecognizerDirectionRight];

UISwipeGestureRecognizer *swipeLeft = [[UISwipeGestureRecognizer alloc] 
                       initWithTarget:self action:@selector(swipedLeft)];
    [swipeLeft setDirection:UISwipeGestureRecognizerDirectionLeft];

    [self.collectionView addGestureRecognizer:swipeRight];
    [self.collectionView addGestureRecognizer:swipeLeft];

swipeRightswipeLeft 方法的示例:

-(void)swipedRight 
    // Switch to left collection view
    [self.collectionView setContentOffset:CGPointMake(0, 0) animated:YES];


-(void)swipedLeft 
    // Switch to right collection view
    [self.collectionView setContentOffset:CGPointMake(cVFrame.size.width, 0) 
                                 animated:YES];

然后实现 DataSource 方法不是什么大问题(在您的情况下,您希望每个页面上有 9 个项目):

-(NSInteger)collectionView:(UICollectionView *)collectionView 
    numberOfItemsInSection:(NSInteger)section 
    if (collectionView.tag == 1) 
         // Second collection view
         return self.dataArray.count % 9;
     else 
         // Original collection view
         return 9; // Or whatever

在方法-collectionView:cellForRowAtIndexPath 中,如果它是第二个集合视图,您将需要从具有偏移量的模型中获取数据。

也不要忘记为您的第二个集合视图注册可重用单元格的类。您还可以只创建一个手势识别器并识别左右滑动。这取决于你。

我想,现在应该可以了,试试看 :-)

【讨论】:

如果你这样做至少你会失去集合视图之间的插入/删除动画。此外,它还为视图控制器添加了很多布局逻辑。不过,感谢您的回答。 好吧,我想,既然你一次只能看到一个集合视图,那没关系。另一种解决方案是有两个集合视图控制器,每个都有滑动手势识别器。 好吧,如果您删除第 N 页的最后一个/第一个元素,这很重要。 当你删除模型中的元素,然后在performBathUpdates方法中调用reloadItemsAtIndexPaths:deletedIndexPaths,你会看到动画。 要删除第N页的最后一个元素(并获取动画):首先我需要删除第N页的最后一个元素,然后删除第N+1页的第一个元素,最后将该元素插入第 N 页的最后一个位置。一个删除操作变成了 3 个操作。复杂吗?【参考方案3】:

您有一个实现UICollectionViewDataSource 协议的对象。 在collectionView:cellForItemAtIndexPath: 内只需返回您想要返回的正确项目。 不明白哪里有问题。

编辑:好的,我看到了问题。解决方法如下:http://www.skeuo.com/uicollectionview-custom-layout-tutorial,具体是第 17 到 25 步。工作量不大,很容易重复使用。

【讨论】:

两个问题,我想不到。 1)插入和删除动画将无法正常工作。 2)如果你没有足够的元素来填充页面,你必须创建假的空元素。【参考方案4】:

这是我的解决方案,我没有使用动画进行测试,但我认为稍作改动就可以了,希望对您有所帮助

CELLS_PER_ROW = 4;
CELLS_PER_COLUMN = 5;
CELLS_PER_PAGE = CELLS_PER_ROW * CELLS_PER_COLUMN;

UICollectionViewFlowLayout* flowLayout = (UICollectionViewFlowLayout*)collectionView.collectionViewLayout;
flowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
flowLayout.itemSize = new CGSize (size.width / CELLS_PER_ROW - delta, size.height / CELLS_PER_COLUMN - delta);
flowLayout.minimumInteritemSpacing = ..;
flowLayout.minimumLineSpacing = ..;
..

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

    NSInteger index = indexPath.row;
    NSInteger page = index / CELLS_PER_PAGE;
    NSInteger indexInPage = index - page * CELLS_PER_PAGE;
    NSInteger row = indexInPage % CELLS_PER_COLUMN;
    NSInteger column = indexInPage / CELLS_PER_COLUMN;

    NSInteger dataIndex = row * CELLS_PER_ROW + column + page * CELLS_PER_PAGE;

    id cellData = _data[dataIndex];
    ...

【讨论】:

以上是关于重新排序 UICollectionView 的单元格的主要内容,如果未能解决你的问题,请参考以下文章

如何在锁定其中一些单元格时重新排序 UICollectionView 单元格

保存重新排序的uicollectionview单元格无法正常工作

UICollectionView 单元格的水平重新排序

如何防止重新排序 UICollectionView 中的最后一个单元格?

UICollectionView 在刷新时重复和重新排序单元格内容

使用全宽和动态高度单元格重新排序 UICollectionView 时的 UI 问题