在UICollectionView上dynamic设置布局会导致无法解释的contentOffset更改

根据苹果公司的文档(并且在WWDC 2012上),可以dynamic地在UICollectionView上设置布局,甚至可以animation变化:

您通常在创build集合视图时指定布局对象,但也可以dynamic更改集合视图的布局。 布局对象存储在collectionViewLayout属性中。 设置这个属性可以直接更新布局,而不需要animation改变。 如果要为更改设置animation,则必须调用setCollectionViewLayout:animated:方法。

但是,在实践中,我发现UICollectionViewcontentOffset进行了无法解释甚至无效的更改,导致单元格错误移动,导致该function几乎无法使用。 为了说明这个问题,我将下面的示例代码放在一起,这些代码可以附加到放入故事板的默认集合视图控制器中:

 #import <UIKit/UIKit.h> @interface MyCollectionViewController : UICollectionViewController @end @implementation MyCollectionViewController - (void)viewDidLoad { [super viewDidLoad]; [self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"CELL"]; self.collectionView.collectionViewLayout = [[UICollectionViewFlowLayout alloc] init]; } - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return 1; } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { UICollectionViewCell *cell = [self.collectionView dequeueReusableCellWithReuseIdentifier:@"CELL" forIndexPath:indexPath]; cell.backgroundColor = [UIColor whiteColor]; return cell; } - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { NSLog(@"contentOffset=(%f, %f)", self.collectionView.contentOffset.x, self.collectionView.contentOffset.y); [self.collectionView setCollectionViewLayout:[[UICollectionViewFlowLayout alloc] init] animated:YES]; NSLog(@"contentOffset=(%f, %f)", self.collectionView.contentOffset.x, self.collectionView.contentOffset.y); } @end 

控制器在viewDidLoad设置一个默认的UICollectionViewFlowLayout ,并在屏幕上显示一个单元。 当选中单元格时,控制器将创build另一个默认UICollectionViewFlowLayout并使用animated:YES标志将其设置在集合视图上。 预期的行为是单元格不移动。 然而,实际的行为是,单元格在屏幕之外滚动,甚至无法将单元格滚动回屏幕上。

看着控制台日志显示,contentOffset莫名其妙地改变了(在我的项目中,从(0,0)到(0,205))。 我发布了解决scheme的非animation案例 ( ie animated:NO ),但因为我需要animation,我很感兴趣,如果有人有一个解决scheme或解决scheme的animation案件。

作为一个侧面说明,我已经testing了自定义布局并获得相同的行为。

我一直拉着我的头发好几天,并find了解决scheme,可以帮助我的情况。 在我的情况下,我有一个崩溃的照片布局就像在iPad上的照片应用程序。 它显示相册上面的相册,当你点击一个相册,它扩大了照片。 所以我有两个单独的UICollectionViewLayouts,并在它们之间切换[self.collectionView setCollectionViewLayout:myLayout animated:YES]我有你的确切的问题与animation之前跳跃的细胞,并意识到它是contentOffset 。 我用contentOffset尝试了所有的东西,但是在animation过程中它仍然contentOffset 。 上面的泰勒的解决scheme工作,但它仍然搞乱animation。

然后我注意到,只有当屏幕上有几张专辑时才会发生这种情况,而不足以填满屏幕。 我的布局覆盖-(CGSize)collectionViewContentSizebuild议。 当只有less数专辑时,collections视图内容大小小于视图内容大小。 当我在集合布局之间切换时,会导致跳转。

所以我在我的布局上设置了一个名为minHeight的属性,并将其设置为集合视图父级的高度。 然后我检查之前,我的高度-(CGSize)collectionViewContentSize我确保高度是> =最小高度。

不是一个真正的解决scheme,但它现在工作正常。 我会尝试设置您的集合视图的contentSize至less是它包含视图的长度。

编辑:如果从UICollectionViewFlowLayoutinheritance,Manicaesar添加了一个简单的解决方法:

 -(CGSize)collectionViewContentSize { //Workaround CGSize superSize = [super collectionViewContentSize]; CGRect frame = self.collectionView.frame; return CGSizeMake(fmaxf(superSize.width, CGRectGetWidth(frame)), fmaxf(superSize.height, CGRectGetHeight(frame))); } 

UICollectionViewLayout包含可UICollectionViewLayout的方法targetContentOffsetForProposedContentOffset:它允许您在布局更改期间提供适当的内容偏移,并且这将正确地animation化。 这在iOS 7.0及更高版本中可用

这个问题也困扰我,它似乎是转换代码中的一个错误。 从我可以告诉它试图专注于最接近转换前视图布局中心的单元格。 但是,如果在转换前视图的中心没有出现一个单元格,那么它仍然会尝试将该单元格转换到后面的位置。 如果将alwaysBounceVertical / Horizo​​ntal设置为YES,那么这非常清晰,只需使用单个单元格加载该视图,然后执行布局转换即可。

我能够通过明确地告诉集合在触发布局更新之后专注于特定的单元格(在这个例子中是第一个单元可见单元格)来解决这个问题。

 [self.collectionView setCollectionViewLayout:[self generateNextLayout] animated:YES]; // scroll to the first visible cell if ( 0 < self.collectionView.indexPathsForVisibleItems.count ) { NSIndexPath *firstVisibleIdx = [[self.collectionView indexPathsForVisibleItems] objectAtIndex:0]; [self.collectionView scrollToItemAtIndexPath:firstVisibleIdx atScrollPosition:UICollectionViewScrollPositionCenteredVertically animated:YES]; } 

跳入我自己的问题迟到的答案。

TLLayoutTransitioning库提供了一个很好的解决scheme,通过重新分配iOS7s交互式转换API来执行非交互式布局转换。 它有效地提供了一个替代setCollectionViewLayout ,解决内容偏移问题,并添加几个function:

  1. animation持续时间
  2. 30多条缓和曲线(Warren Moore的AHEasing图书馆提供 )
  3. 多种内容偏移模式

自定义缓动曲线可以定义为AHEasingFunction函数。 最终的内容偏移量可以用一个或多个索引path来指定,包括最小,中心,顶部,左侧,底部或右侧放置选项。

要明白我的意思,请尝试在“示例”工作区中运行“ resize”演示 ,并使用这些选项。

用法就是这样。 首先,configuration你的视图控制器返回一个TLTransitionLayout的实例:

 - (UICollectionViewTransitionLayout *)collectionView:(UICollectionView *)collectionView transitionLayoutForOldLayout:(UICollectionViewLayout *)fromLayout newLayout:(UICollectionViewLayout *)toLayout { return [[TLTransitionLayout alloc] initWithCurrentLayout:fromLayout nextLayout:toLayout]; } 

然后,不是调用setCollectionViewLayout ,而是调用UICollectionView-TLLayoutTransitioning类中定义的transitionToCollectionViewLayout:toLayout

 UICollectionViewLayout *toLayout = ...; // the layout to transition to CGFloat duration = 2.0; AHEasingFunction easing = QuarticEaseInOut; TLTransitionLayout *layout = (TLTransitionLayout *)[collectionView transitionToCollectionViewLayout:toLayout duration:duration easing:easing completion:nil]; 

此调用启动交互式转换,并在内部启动一个CADisplayLinkcallback,以指定的持续时间CADisplayLink来驱动转换进度。

下一步是指定一个最终的内容偏移量。 您可以指定任意值,但在UICollectionView-TLLayoutTransitioning定义的toContentOffsetForLayout方法提供了一种计算相对于一个或多个索引path的内容偏移量的方法。 例如,为了使特定单元尽可能靠近集合视图的中心,请在transitionToCollectionViewLayout之后立即进行以下调用:

 NSIndexPath *indexPath = ...; // the index path of the cell to center TLTransitionLayoutIndexPathPlacement placement = TLTransitionLayoutIndexPathPlacementCenter; CGPoint toOffset = [collectionView toContentOffsetForLayout:layout indexPaths:@[indexPath] placement:placement]; layout.toContentOffset = toOffset; 

简单。

在同一个animation块中将新布局和collectionView的contentOffsetanimation化。

 [UIView animateWithDuration:0.3 animations:^{ [self.collectionView setCollectionViewLayout:self.someLayout animated:YES completion:nil]; [self.collectionView setContentOffset:CGPointMake(0, -64)]; } completion:nil]; 

它将保持self.collectionView.contentOffset常量。

如果只是在从布局转换时查找内容偏移量以便不更改,则可以创build自定义布局并覆盖一些方法来跟踪旧的contentOffset并重用它:

 @interface CustomLayout () @property (nonatomic) NSValue *previousContentOffset; @end @implementation CustomLayout - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset { CGPoint previousContentOffset = [self.previousContentOffset CGPointValue]; CGPoint superContentOffset = [super targetContentOffsetForProposedContentOffset:proposedContentOffset]; return self.previousContentOffset != nil ? previousContentOffset : superContentOffset ; } - (void)prepareForTransitionFromLayout:(UICollectionViewLayout *)oldLayout { self.previousContentOffset = [NSValue valueWithCGPoint:self.collectionView.contentOffset]; return [super prepareForTransitionFromLayout:oldLayout]; } - (void)finalizeLayoutTransition { self.previousContentOffset = nil; return [super finalizeLayoutTransition]; } @end 

所有这一切都是在prepareForTransitionFromLayout保存之前的内容偏移量,覆盖targetContentOffsetForProposedContentOffset的新内容偏移量,并在finalizeLayoutTransition中将其清除。 非常简单

迅速3

UICollectionViewLayout子类。 cdemiris99的build议是:

 override public func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint { return collectionView?.contentOffset ?? CGPoint.zero } 

我使用自己的自定义布局testing了自己

如果它有助于增加经验的话:无论我的内容大小,我是否设置了内容插入或任何其他明显的因素,我都会一直遇到这个问题。 所以我的解决方法有点激烈。 首先,我将UICollectionView分类并添加到不适当的内容偏移设置:

 - (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated { if(_declineContentOffset) return; [super setContentOffset:contentOffset]; } - (void)setContentOffset:(CGPoint)contentOffset { if(_declineContentOffset) return; [super setContentOffset:contentOffset]; } - (void)setCollectionViewLayout:(UICollectionViewLayout *)layout animated:(BOOL)animated { _declineContentOffset ++; [super setCollectionViewLayout:layout animated:animated]; _declineContentOffset --; } - (void)setContentSize:(CGSize)contentSize { _declineContentOffset ++; [super setContentSize:contentSize]; _declineContentOffset --; } 

我并不自豪,但唯一可行的解​​决scheme似乎完全拒绝集合视图的任何企图设置自己的内容偏移导致setCollectionViewLayout:animated:调用setCollectionViewLayout:animated: 实际上,这种变化看起来像直接调用中直接发生的,显然这不是由接口或文档保证的,但从核心animation的angular度来看是有意义的,所以我可能只有50%的不舒服。

然而,还有第二个问题:UICollectionView现在增加了一些跳转到那些在一个新的集合视图布局停留在同一个地方的意见 – 推动他们约240点,然后animation回到原来的位置。 我不清楚为什么,但我修改了我的代码来处理它,但是切断了添加到实际上没有移动的单元格的CAAnimation

 - (void)setCollectionViewLayout:(UICollectionViewLayout *)layout animated:(BOOL)animated { // collect up the positions of all existing subviews NSMutableDictionary *positionsByViews = [NSMutableDictionary dictionary]; for(UIView *view in [self subviews]) { positionsByViews[[NSValue valueWithNonretainedObject:view]] = [NSValue valueWithCGPoint:[[view layer] position]]; } // apply the new layout, declining to allow the content offset to change _declineContentOffset ++; [super setCollectionViewLayout:layout animated:animated]; _declineContentOffset --; // run through the subviews again... for(UIView *view in [self subviews]) { // if UIKit has inexplicably applied animations to these views to move them back to where // they were in the first place, remove those animations CABasicAnimation *positionAnimation = (CABasicAnimation *)[[view layer] animationForKey:@"position"]; NSValue *sourceValue = positionsByViews[[NSValue valueWithNonretainedObject:view]]; if([positionAnimation isKindOfClass:[CABasicAnimation class]] && sourceValue) { NSValue *targetValue = [NSValue valueWithCGPoint:[[view layer] position]]; if([targetValue isEqualToValue:sourceValue]) [[view layer] removeAnimationForKey:@"position"]; } } } 

这看起来并不妨碍实际移动的视图,或导致移动不正确的视图(就好像他们期望他们周围的一切都下降了大约240点并使其与正确的位置一起animation)。

所以这是我目前的解决scheme。

我可能花了大约两个星期的时间试图让各种布局之间顺利过渡。 我发现覆盖提出的偏移量是在iOS 10.2中工作,但在之前的版本中,我仍然遇到问题。 让我的情况更糟糕的是我需要转换到另一个布局作为滚动的结果,所以视图是同时滚动和转换。

汤米的回答是在10.2版本之前唯一对我有用的东西。 我现在正在做下面的事情。

 class HackedCollectionView: UICollectionView { var ignoreContentOffsetChanges = false override func setContentOffset(_ contentOffset: CGPoint, animated: Bool) { guard ignoreContentOffsetChanges == false else { return } super.setContentOffset(contentOffset, animated: animated) } override var contentOffset: CGPoint { get { return super.contentOffset } set { guard ignoreContentOffsetChanges == false else { return } super.contentOffset = newValue } } override func setCollectionViewLayout(_ layout: UICollectionViewLayout, animated: Bool) { guard ignoreContentOffsetChanges == false else { return } super.setCollectionViewLayout(layout, animated: animated) } override var contentSize: CGSize { get { return super.contentSize } set { guard ignoreContentOffsetChanges == false else { return } super.contentSize = newValue } } } 

然后,当我设置布局,我这样做…

 let theContentOffsetIActuallyWant = CGPoint(x: 0, y: 100) UIView.animate(withDuration: animationDuration, delay: 0, options: animationOptions, animations: { collectionView.setCollectionViewLayout(layout, animated: true, completion: { completed in // I'm also doing something in my layout, but this may be redundant now layout.overriddenContentOffset = nil }) collectionView.ignoreContentOffsetChanges = true }, completion: { _ in collectionView.ignoreContentOffsetChanges = false collectionView.setContentOffset(theContentOffsetIActuallyWant, animated: false) }) 

这终于为我工作了(Swift 3)

 self.collectionView.collectionViewLayout = UICollectionViewFlowLayout() self.collectionView.setContentOffset(CGPoint(x: 0, y: -118), animated: true)