71

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

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

然而,在实践中,我发现这UICollectionView会对contentOffset. 为了说明这个问题,我将以下示例代码放在一起,这些代码可以附加到放入故事板中的默认集合视图控制器:

#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

控制器设置默认值并UICollectionViewFlowLayoutviewDidLoad屏幕上显示单个单元格。当单元格被选中时,控制器会创建另一个默认值UICollectionViewFlowLayout并将其设置在带有animated:YES标志的集合视图上。预期的行为是单元格不移动。然而,实际行为是单元格滚动到屏幕外,此时甚至不可能将单元格滚动回屏幕上。

查看控制台日志显示 contentOffset 发生了莫名其妙的变化(在我的项目中,从 (0, 0) 到 (0, 205))。我发布了针对非动画案例i.e. animated:NO

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

4

10 回答 10

38

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

于 2014-08-20T23:48:59.553 回答
33

几天来,我一直在努力解决这个问题,并找到了可能对我的情况有所帮助的解决方案。就我而言,我有一个折叠的照片布局,就像在 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)));
}
于 2013-03-25T15:34:02.727 回答
7

对我自己的问题的回答迟到了。

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

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

自定义缓动曲线可以定义为AHEasingFunction函数。最终内容偏移量可以根据一个或多个索引路径指定,具有最小、中心、顶部、左侧、底部或右侧放置选项。

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

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

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

然后,在类别中定义的setCollectionViewLayoutcall不是 call ,而是:transitionToCollectionViewLayout:toLayoutUICollectionView-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;
于 2014-02-17T00:29:46.353 回答
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];
}
于 2012-12-28T20:21:08.100 回答
7

2019实际解决方案

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

假设你有三个。

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

当您在布局之间制作动画时,它会跳转。

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

修复是这样的:

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

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)
    }
}

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

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

而已。现在可以了。


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

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

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

事实上,在 Apple 改变它的工作方式之前,每次你在集合视图布局之间制作动画时,你都必须这样做。这就是生活!

于 2019-09-26T02:47:02.877 回答
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不变。

于 2015-02-06T05:35:31.537 回答
3

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

于 2016-02-06T19:49:01.767 回答
2

如果它有助于增加体验:无论我的内容大小,我是否设置了内容插入或任何其他明显因素,我都会遇到这个问题。所以我的解决方法有点激烈。首先,我将 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已添加到实际上没有移动的任何单元格的 s 来处理它:

- (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 点并与它们一起动画到正确的位置)。

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

于 2014-02-13T21:16:58.860 回答
0

我现在可能已经花了大约两个星期的时间来尝试让各种布局在彼此之间顺利过渡。我发现覆盖建议的偏移量在 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)
})
于 2017-02-23T22:28:59.203 回答
-3

这终于对我有用(Swift 3)

self.collectionView.collectionViewLayout = UICollectionViewFlowLayout()
self.collectionView.setContentOffset(CGPoint(x: 0, y: -118), animated: true)
于 2017-08-29T02:08:01.727 回答