43

考虑一个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:and相关finalLayoutAttributesForDisappearingItemAtIndexPath:,但我无法准确指出问题所在。
  • 就我而言,f(n)取决于每页的列数和行数。除了自己硬编码之外,有没有什么方法可以从中提取这些信息UICollectionViewFlowLayout?我想过用集合视图的边界进行查询layoutAttributesForElementsInRect:,并从那里推导出行和列,但这也感觉效率低下。
4

4 回答 4

15

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

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

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

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

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

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

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

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

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

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

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

于 2013-04-09T07:09:38.890 回答
4

拥有两个带有标准的集合视图不是一个简单的解决方案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中,如果它是第二个集合视图,您将需要从具有偏移量的模型中获取数据。

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

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

于 2013-04-05T14:09:03.023 回答
2

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

编辑:好的,我看到了问题。这是解决方案:http ://www.skeuo.com/uicollectionview-custom-layout-tutorial ,具体步骤17到25。这不是一个巨大的工作量,并且可以很容易地重复使用。

于 2013-03-30T22:33:49.437 回答
0

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

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];
    ...
}
于 2015-03-22T16:16:45.427 回答