5

问题

我想让 UICollectionView 对特定项目进行动画滚动。

这在大多数情况下都有效,但有时我试图滚动到的项目最终不会显示

代码

- (void)onClick {
  // (Possibly recompute the _items array.)
  NSInteger target_idx = // (...some valid index of _items)
  NSIndexPath *item_idx = [NSIndexPath indexPathForItem:target_idx inSection:0];
  [self scrollToItem:item_idx];
}

- (void)scrollToItem:(NSIndexPath*)item_idx {
  // Make sure our view is up-to-date with the data we want to show.
  NSInteger num_items = [self.collection_view numberOfItemsInSection:0];
  if (num_items != _items.count) {
    [self.collection_view reloadData];
  }

  [self.collection_view 
    scrollToItemAtIndexPath:item_idx
           atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally
                   animated:YES];
}

细节

  • self.collection_view是一个 UICollectionView,由单行项目组成,具有标准流布局和启用水平滚动。
  • 我需要在滚动之前调用,因为自从 UICollectionView 上次显示它以来reloadData,它可能已经发生了变化。_items
  • 这个问题只发生在动画滚动上;如果我通过animated:NO了,那么一切都会按预期进行。
  • 当问题发生时,后滚动调用indexPathsForVisibleItems显示 UICollectionView不认为目标项是可见的

任何想法为什么有时会默默地滚动到一个项目失败?


更新:问题似乎来自快速连续重新加载和滚动;如果没有重新加载,滚动将按预期进行。加载新数据后是否有任何用于滚动到项目的习语?

4

3 回答 3

12

在将项目添加到UICollectionView.

发生了什么

问题似乎是紧随其后,[collectionView reloadData]或者[collectionView insertItemsAtIndexPaths: @[newItemIndexPath]]集合视图的内容大小尚未更新

如果您然后尝试滚动可见的添加项目,它将失败,因为内容矩形尚未包含新项目的空间

一个修复

有一个简单且相当健壮的解决方法,即将滚动事件发布到运行循环的下一次迭代,如下所示:

const NSUInteger newIndex = 
  [self collectionView: self.collectionView numberOfItemsInSection: 0] - 1;

NSIndexPath *const newPath = 
  [NSIndexPath indexPathForItem: newIndex
                      inSection: 0];

[self.collectionView insertItemsAtIndexPaths: @[newPath]];

UICollectionViewLayoutAttributes *const layoutAttributes =
  [self.collectionView layoutAttributesForItemAtIndexPath: newPath];

dispatch_async(dispatch_get_main_queue(), ^{
    [self.collectionView scrollRectToVisible: layoutAttributes.frame 
                                    animated: YES];
});

没有更好的解决办法吗?

虽然它有效,但这种“在下一个运行循环滴答声上发布滚动调用”的恶作剧感觉很老套。

如果UICollectionView可以在完成更新内容矩形时调用回调,那将是更可取的。UIKit 为其他对其模型执行异步更新的方法提供了这种风格的回调。例如,用于UIViewController过渡和UIView动画的完成块。

UICollectionView不提供此回调。据我所知,当它完成更新时,没有其他简单的干净方法可以找到。如果没有这个,下一个运行循环滴答是我们更喜欢使用的回调独角兽的可行马代理。

还有什么要知道的吗?

知道UITableView也有这个问题可能很有用。类似的解决方法也应该在那里工作。

于 2015-04-29T13:13:07.140 回答
3

在@NicholasHart 的帮助下,我想我理解了这个问题。

reloadData只要重新加载使集合变大,尝试然后将动画滚动到新位置是有意义的(并且似乎有效) 。

但是,当重新加载缩小集合时,动画滚动的起点可能不再存在。这使得动画很麻烦。

例如,如果您从一直向右滚动的视图开始(以便最右边的项目可见),然后重新加载并丢失一半的项目,则不清楚动画的起点应该是什么。在这种情况下尝试进行动画滚动会导致无操作或跳转到一个非常奇怪的位置。

一种看起来相当不错的解决方案是仅在集合变得更大时才进行动画处理:

- (void)scrollToItem:(NSIndexPath*)item_idx {
  // Make sure our view is up-to-date with the data we want to show.
  NSInteger old_num_items = [self.collection_view numberOfItemsInSection:0];
  NSInteger new_num_items = _items.count;
  if (old_num_items != new_num_items) {
    [self.collection_view reloadData];
  }

  // Animating if we're getting smaller doesn't really make sense, and doesn't 
  // seem to be supported.
  BOOL is_expanding = new_num_items >= old_num_items;
  [self.collection_view 
    scrollToItemAtIndexPath:item_idx
           atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally
                   animated:is_expanding];
}
于 2013-07-20T00:11:14.540 回答
0

这对我有用:

[UIView performWithoutAnimation:^{
    [self.collectionView performBatchUpdates:^{
        [self.collectionView reloadData];
    } completion:^(BOOL finished) {
        if (finished) {
            [self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:messages.count - 1 inSection:0]
                                        atScrollPosition:UICollectionViewScrollPositionBottom
                                                animated:NO];
        }
    }];
}];
于 2017-11-01T09:32:40.317 回答