动态设置上UICollectionView布局引起莫名contentOffset变化(Dynamic

2019-06-21 18:46发布

根据苹果公司的文件(在WWDC 2012吹捧),也可以设置布局UICollectionView动态,甚至动画的变化:

通常,您创建一个集合视图时指定一个布局对象,但你也可以改变一个集合视图的布局动态。 布局对象被存储在collectionViewLayout属性。 设置此属性直接将立即更新布局,没有动画的变化。 替代方法:如果你想动画的变化,你必须调用setCollectionViewLayout:动画。

然而,在实践中,我发现, 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))。 我贴了一个解决方案的非动画情况下的解决方案 ( ie animated:NO ),但因为我需要动画,我非常想知道如果任何人有动画情况下的解决方案或替代方法。

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

Answer 1:

我一直在拉我的头发在这几天,已经找到了我的情况的解决方案,可以帮助。 在我来说,我有一个崩溃的照片布局就像在iPad上的照片应用。 它显示了照片上彼此之上,当你点击它扩展了照片的相册专辑。 因此,我已经是两个独立的UICollectionViewLayouts和我之间切换使用[self.collectionView setCollectionViewLayout:myLayout animated:YES]我有你与细胞动画之前跳跃意识到这是确切的问题contentOffset 。 我试图用一切contentOffset但动画过程中它仍然跳了下去。 上述泰勒的解决方案工作,但它仍然与动画搞乱。

突然,我注意到它发生,只有当出现了屏幕上的几张专辑,不足以填满整个屏幕。 我的布局覆盖-(CGSize)collectionViewContentSize建议。 当只有几张专辑集合视图内容大小小于观点内容大小。 当我收集的布局之间切换是造成跳。

所以我把我的布局被称为一个了minHeight属性,并将其设置为集合看待父母的身高。 然后我检查高度之前,我在返回-(CGSize)collectionViewContentSize我确保高度为> =最小高度。

不是一个真正的解决方案,但它现在工作的罚款。 我会尝试设置contentSize您的收藏视图成为至少长度也包含视图。

编辑:Manicaesar增加了一个简单的解决办法,如果你从UICollectionViewFlowLayout继承:

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


Answer 2:

UICollectionViewLayout包含重写方法targetContentOffsetForProposedContentOffset:它允许您提供适当的内容布局的变化过程中的偏移,这将正确动画。 这在iOS版7.0及更高版本



Answer 3:

这个问题咬了我以及和它似乎是在过渡代码中的错误。 从我可以告诉它试图把重点放在最接近转型前视图布局的中心细胞。 但是,如果不发生是在视图前的过渡中心的细胞则仍然尝试中心,细胞会转变之后。 这是很清楚如果设置alwaysBounceVertical /水平为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];
}


Answer 4:

有迟到的回答我自己的问题的走马观花。

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

  1. 动画时长
  2. 30+缓和曲线(沃伦·摩尔的礼貌AHEasing库 )
  3. 多种内容偏移模式

定制缓和曲线可以定义为AHEasingFunction功能。 最终的内容偏移可以以最小的,中心,上,左,下或右位置选项的一个或多个索引的路径来指定。

要明白我的意思,尝试运行调整尺寸演示的实例工作区,并与选项玩耍。

用法是这样的。 首先,配置您的视图控制器返回的实例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;


Answer 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不变。



Answer 6:

SWIFT 3。

UICollectionViewLayout子类。 该方法cdemiris99建议:

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

我用我自己的自定义测试它自己的布局



Answer 7:

如果你只是要查找的内容偏移时,从布局的过渡,你可以创建自定义布局不会改变,并覆盖了几个方法来跟踪旧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 。 很简单



Answer 8:

如果它有助于增加经验的身体:坚持不管我遇到这个问题我的内容的大小,我是否已确定内容的插图或任何其他明显的因素。 所以我的解决方法是有点激烈。 首先,我子类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点,以动画到正确的位置与他们)。

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



Answer 9:

我大概花了两个星期左右,现在试图让各种布局平滑地彼此之间过渡。 我发现,覆盖提出抵消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)
})


Answer 10:

这终于为我工作(SWIFT 3)

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


文章来源: Dynamically setting layout on UICollectionView causes inexplicable contentOffset change