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

Tim*_*ose 68 iphone ios ios6 uicollectionview

根据Apple的文档(并在WWDC 2012上吹捧),可以UICollectionView动态设置布局,甚至可以为更改设置动画:

通常在创建集合视图时指定布局对象,但也可以动态更改集合视图的布局.布局对象存储在collectionViewLayout属性中.设置此属性会立即直接更新布局,而无需为更改设置动画.如果要为更改设置动画,则必须调用setCollectionViewLayout:animated:方法.

然而,在实践中,我发现它UICollectionViewcontentOffset导致莫名其妙的甚至无效的更改,导致单元格移动不正确,使得该功能几乎无法使用.为了说明这个问题,我将以下示例代码放在一起,这些代码可以附加到放入故事板的默认集合视图控制器中:

#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
Run Code Online (Sandbox Code Playgroud)

所述控制器设置一个默认的UICollectionViewFlowLayoutviewDidLoad与显示屏幕上的一个单元格.选择单元格后,控制器会创建另一个默认值,UICollectionViewFlowLayout并使用该animated:YES标志将其设置在集合视图上.预期的行为是细胞不移动.然而,实际的行为是单元格在屏幕外滚动,此时甚至无法在屏幕上滚动单元格.

查看控制台日志显示contentOffset已经莫名其妙地改变了(在我的项目中,从(0,0)到(0,205)).我发布了针对非动画案例(i.e. animated:NO)的解决方案的解决方案,但由于我需要动画,我很有兴趣知道是否有人有动画案例的解决方案或解决方法.

作为旁注,我测试了自定义布局并获得了相同的行为.

cde*_*s99 38

UICollectionViewLayout包含可覆盖的方法targetContentOffsetForProposedContentOffset:,允许您在更改布局期间提供适当的内容偏移,这将正确动画.这在iOS 7.0及更高版本中可用


tas*_*ari 30

我已经把头发拉过来好几天了,并找到了解决我的情况可能会有所帮助的方法.在我的情况下,我有一个折叠的照片布局,就像在iPad上的照片应用程序.它显示相册顶部的相册,当您点击相册时,它会展开照片.所以我所拥有的是两个独立的UICollectionViewLayouts并且正在他们之间进行切换[self.collectionView setCollectionViewLayout:myLayout animated:YES]我遇到了你在动画之前跳过的细胞的确切问题,并意识到它是contentOffset.我尝试了所有的东西,contentOffset但它仍然在动画期间跳跃.上面的泰勒的解决方案有效,但它仍然在搞乱动画.

然后我注意到它只在屏幕上有几张专辑时才会出现,不足以填满屏幕.我的布局覆盖-(CGSize)collectionViewContentSize了推荐值.当只有少数专辑时,集合视图的内容大小小于视图内容大小.当我在集合布局之间切换时,这会导致跳转.

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

不是一个真正的解决方案,但它现在工作正常.我会尝试将contentSize您的集合视图设置为至少包含视图的长度.

编辑: 如果从UICollectionViewFlowLayout继承,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)));
}
Run Code Online (Sandbox Code Playgroud)

  • 如果你继承UICollectionViewFlowLayout它很简单,只要: - (CGSize)collectionViewContentSize {//解决方法CGSize超大= [超级collectionViewContentSize]; CGRect frame = self.collectionView.frame; 返回CGSizeMake(fmaxf(superSize.width,CGRectGetWidth(frame)),fmaxf(superSize.height,CGRectGetHeight(frame))); } (5认同)
  • 看一下targetContentOffsetForProposedContentOffset:和targetContentOffsetForProposedContentOffset:withScrollingVelocity:在你的布局子类中. (5认同)

tyl*_*ler 7

这个问题也困扰我,它似乎是转换代码中的一个错误.据我所知,它试图关注最靠近转换前视图布局中心的单元格.但是,如果在转换前的视图中心没有碰巧出现一个单元格,那么它仍然会尝试将单元格转换为转移后的中心位置.如果将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];
}
Run Code Online (Sandbox Code Playgroud)

  • 检查UICollectionViewLayout方法 - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset.Apples对我的建议是将我想要关注的单元格的目标索引填充到布局中(在我的自定义布局扩展上公开NSIndexPath prop),然后使用此信息返回相应的targetContentOffset.这个解决方案对我有用. (12认同)
  • 看起来他们已经添加了API来控制iOS7中的这种行为. (2认同)

Tim*_*ose 7

对我自己的问题迟到了.

TLLayoutTransitioning库通过重新分配iOS7s互动过渡API进行非交互式,版面布局的转变提供了一个很好的解决这个问题.它有效地提供了一种替代方案setCollectionViewLayout,解决了内容偏移问题并添加了几个功能:

  1. 动画时长
  2. 30多条宽松曲线(由Warren Moore的AHEasing库提供)
  3. 多种内容偏移模式

自定义缓动曲线可以定义为AHEasingFunction函数.可以使用Minimal,Center,Top,Left,Bottom或Right放置选项,根据一个或多个索引路径指定最终内容偏移量.

要了解我的意思,请尝试在"示例"工作区中运行" 调整大小"演示并使用这些选项.

用法是这样的.首先,配置视图控制器以返回以下实例TLTransitionLayout:

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

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

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];
Run Code Online (Sandbox Code Playgroud)

此调用启动交互式转换,并在内部启动CADisplayLink回调,该回调以指定的持续时间和缓动功能驱动转换进度.

下一步是指定最终内容偏移量.您可以指定任意值,但toContentOffsetForLayout定义的方法UICollectionView-TLLayoutTransitioning提供了一种优雅的方法来计算相对于一个或多个索引路径的内容偏移量.例如,为了使特定单元格尽可能靠近集合视图的中心,请在以下情况后立即进行以下调用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;
Run Code Online (Sandbox Code Playgroud)


Fat*_*tie 7

2019年实际解决方案

假设您的“汽车”视图有多种布局。

假设你有三个。

CarsLayout1: UICollectionViewLayout { ...
CarsLayout2: UICollectionViewLayout { ...
CarsLayout3: UICollectionViewLayout { ...
Run Code Online (Sandbox Code Playgroud)

当您在布局之间设置动画时,它会跳跃

这只是苹果不可否认的错误。毫无疑问,它会在您制作动画时跳跃。

修复方法是这样的:

您必须有一个全局浮点数,以及以下基类:

var avoidAppleMessupCarsLayouts: CGPoint? = nil

class FixerForCarsLayouts: UICollectionViewLayout {
    
    override func prepareForTransition(from oldLayout: UICollectionViewLayout) {
        avoidAppleMessupCarsLayouts = collectionView?.contentOffset
    }
    
    override func targetContentOffset(
        forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
        if avoidAppleMessupCarsLayouts != nil {
            return avoidAppleMessupCarsLayouts!
        }
        return super.targetContentOffset(forProposedContentOffset: proposedContentOffset)
    }
}
Run Code Online (Sandbox Code Playgroud)

所以这里是“汽车”屏幕的三种布局:

CarsLayout1: FixerForCarsLayouts { ...
CarsLayout2: FixerForCarsLayouts { ...
CarsLayout3: FixerForCarsLayouts { ...
Run Code Online (Sandbox Code Playgroud)

就是这样。它现在可以工作了。


  1. 令人难以置信的是,您可能有不同的“组”布局(用于汽车、狗、房屋等),它们可能(可以想象)发生冲突。出于这个原因,每个“集合”都有一个如上所述的全局和基类。

  2. 这是多年前通过用户@Isaacliu 发明的。

  3. 添加了一个细节,Isaacliu 的代码片段中的 FWIW finalizeLayoutTransition。事实上,逻辑上没有必要。

事实是,除非 Apple 改变它的工作方式,否则每次在集合视图布局之间设置动画时,都必须这样做。这就是生活!

  • @iWheelBuy,十年内只有两个人找到了正确的技术:)你就是其中之一。您的答案中有一个小错误,我刚刚更正它并更新了语法。非常感谢您的解决方案!!!!!!! (2认同)

nik*_*sky 5

简单.

在同一动画块中为新布局和collectionView的contentOffset设置动画.

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

它会保持self.collectionView.contentOffset不变.