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

时间:2012-12-08 17:49:34

标签: 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

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

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

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

10 个答案:

答案 0 :(得分:33)

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

答案 1 :(得分:28)

我已经把头发拉过来好几天了,并找到了解决我的情况可能会有所帮助的方法。 在我的情况下,我有一个折叠的照片布局,就像在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)));
}

答案 2 :(得分: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];
}

答案 3 :(得分:7)

回答我自己的问题。

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

  1. 动画片段
  2. 30+宽松曲线(由Warren Moore的AHEasing library提供)
  3. 多种内容偏移模式
  4. 自定义缓动曲线可以定义为AHEasingFunction个函数。最终内容偏移量可以使用Minimal,Center,Top,Left,Bottom或Right放置选项的一个或多个索引路径指定。

    要了解我的意思,请尝试在示例工作区中运行Resize demo并使用选项。

    用法是这样的。首先,配置视图控制器以返回TLTransitionLayout

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

    然后,不要拨打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];
    

    此调用启动交互式转换,并在内部启动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;
    

答案 4 :(得分: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];

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

答案 5 :(得分:3)

swift 3

UICollectionViewLayout 子类。 cdemiris99 建议的方式:

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

我使用自己的自定义layout

自行测试

答案 6 :(得分:3)

2019实际解决方案

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

假设您有三个。

CarsLayout1: UICollectionViewLayout { ...
CarsLayout2: UICollectionViewLayout { ...
CarsLayout3: UICollectionViewLayout { ...

在布局之间设置动画时, 会跳转

这只是Apple不可否认的错误。毫无疑问,它会在您动画时跳转。

解决方法是这样:

您必须具有全局浮点数以及以下基类:

var avoidAppleFuckupCarsLayouts: CGPoint? = nil

class FixerForCarsLayouts: UICollectionViewLayout {

    override func prepareForTransition(from oldLayout: UICollectionViewLayout) {
        avoidAppleFuckupCarsLayouts = collectionView?.contentOffset
    }

    override func targetContentOffset(
        forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
        if avoidAppleFuckupCarsLayouts != nil {
            return avoidAppleFuckupCarsLayouts!
        }
        return super.targetContentOffset(forProposedContentOffset: proposedContentOffset)
    }
}

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

CarsLayout1: FixerForCarsLayouts { ...
CarsLayout2: FixerForCarsLayouts { ...
CarsLayout3: FixerForCarsLayouts { ...

就是这样。

现在可以使用了。

Apple,感谢您提出了另一个本不应该存在的神秘问题。

脚注

  1. 令人难以置信的是,您可能有不同的“集合”布局(用于汽车,狗,房屋等),它们可能会发生冲突。因此,对于每个“集合”,都需要有一个上述的全局和基类。

2。这是由多年前传递用户@Isaacliu发明的。谢谢。

  1. 在Isaacliu的代码片段finalizeLayoutTransition中添加了详细信息FWIW。实际上,这在逻辑上是不必要的。

事实是,直到Apple改变其工作方式之后,每次在集合视图布局之间进行动画处理时,您都必须这样做。这就是生活!

答案 7 :(得分:2)

如果您只是在寻找从布局转换时不会更改的内容偏移量,您可以创建自定义布局并覆盖几个方法以跟踪旧的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中清除它。很简单

答案 8 :(得分:1)

如果它有助于增加体验:无论我的内容大小,我是否设置了内容插入或任何其他明显因素,我都会持续遇到此问题。所以我的解决方案有点激烈。首先,我将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 --;
}

我并不为此感到骄傲,但唯一可行的解​​决方案似乎是完全拒绝集合视图的任何尝试,以设置由于调用setCollectionViewLayout:animated:而产生的内容偏移量。根据经验,看起来这种变化直接发生在即时调用中,这显然不是由接口或文档保证,但从核心动画的角度来看是有意义的,所以我可能只有50%的假设不舒服。

然而还有第二个问题:UICollectionView现在为那些在新的集合视图布局上停留在同一位置的视图添加了一点跳转 - 将它们向下推动约240个点,然后将它们设置为原始位置。我不清楚为什么但是我修改了我的代码来处理它然后通过切断已添加到任何实际上没有移动的单元格的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点,并用它们动画到正确的位置)。

所以这是我目前的解决方案。

答案 9 :(得分:-1)

这最终对我有用(Swift 3)

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