在旋转界面方向时保持UICollectionView的contentOffset

77

我正在尝试在UICollectionViewController中处理界面方向的更改。我的目标是希望在界面旋转后,相同的contentOffset保持不变,也就是说,它应该根据bounds的比例进行相应的更改。

从纵向开始,内容偏移为{ bounds.size.width * 2,0 } …

UICollectionView in portait

… 在横向上的内容偏移量也应该是{bounds.size.width * 2, 0}(反之亦然)。

UICollectionView in landscape

计算新的偏移量并不是问题,但我不知道在哪里(或何时)设置它,才能获得平滑的动画效果。到目前为止,我所做的无效的方法是在willRotateToInterfaceOrientation:duration:中无效布局,并在didRotateFromInterfaceOrientation:中重置内容偏移。

- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
                                duration:(NSTimeInterval)duration;
{
    self.scrollPositionBeforeRotation = CGPointMake(self.collectionView.contentOffset.x / self.collectionView.contentSize.width,
                                                    self.collectionView.contentOffset.y / self.collectionView.contentSize.height);
    [self.collectionView.collectionViewLayout invalidateLayout];
}

- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation;
{
    CGPoint newContentOffset = CGPointMake(self.scrollPositionBeforeRotation.x * self.collectionView.contentSize.width,
                                           self.scrollPositionBeforeRotation.y * self.collectionView.contentSize.height);
    [self.collectionView newContentOffset animated:YES];
}

这将在旋转后更改内容偏移量。

如何在旋转期间设置它?我尝试在willAnimateRotationToInterfaceOrientation:duration:中设置新的内容偏移量,但结果非常奇怪。

您可以在GitHub上找到我的项目的示例。

24个回答

41

您可以在视图控制器中执行此操作:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)

    guard let collectionView = collectionView else { return }
    let offset = collectionView.contentOffset
    let width = collectionView.bounds.size.width

    let index = round(offset.x / width)
    let newOffset = CGPoint(x: index * size.width, y: offset.y)

    coordinator.animate(alongsideTransition: { (context) in
        collectionView.reloadData()
        collectionView.setContentOffset(newOffset, animated: false)
    }, completion: nil)
}

或者在布局本身中: https://dev59.com/hGYr5IYBdhLWcg3wq7-I#54868999


确切而精确的答案。非常感谢! - Kenan Begić
经过相当多的研究,这是最优雅的解决方案! - iDoc
它可以工作。你只需要确保viewWillTransition被调用了。另一个要点是,如果下一页不是“整个”页面,你只需要在索引变量上去掉round即可。 - Nathan Barreto
你应该使用 floor 函数而不是 round 吗? - Mario
@Mario 我只使用四舍五入。 - Gurjinder Singh
你可以调用 collectionView.collectionViewLayout.invalidateLayout() 来代替调用 collectionView.reloadData() - dronpopdev

18

解决方案1,“只需捕捉”

如果您需要确保contentOffset以正确的位置结束,您可以创建一个UICollectionViewLayout子类并实现targetContentOffsetForProposedContentOffset:方法。例如,您可以像这样计算页面:

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset
{
    NSInteger page = ceil(proposedContentOffset.x / [self.collectionView frame].size.width);
    return CGPointMake(page * [self.collectionView frame].size.width, 0);
}

但是你会面临的问题是,该转换的动画非常奇怪。我在我的情况下(几乎与你的情况相同)所做的是:

解决方案2,“平滑动画”

1)首先,我设置了单元格大小,可以通过collectionView:layout:sizeForItemAtIndexPath:代理方法进行管理,如下所示:

- (CGSize)collectionView:(UICollectionView *)collectionView
                  layout:(UICollectionViewLayout  *)collectionViewLayout
  sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
    return [self.view bounds].size;
}

注意,[self.view bounds]将根据设备旋转而更改。

2)当设备即将旋转时,我会在集合视图顶部添加一个带有所有调整大小掩码的imageView。该视图实际上会隐藏collectionView的怪异行为(因为它在其上方),并且由于willRotatoToInterfaceOrientation:方法在动画块中被调用,因此它将相应地旋转。我还根据显示的indexPath保留下一个contentOffset,以便在旋转完成后可以修复contentOffset:

- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
                                duration:(NSTimeInterval)duration
{
    // Gets the first (and only) visible cell.
    NSIndexPath *indexPath = [[self.collectionView indexPathsForVisibleItems] firstObject];
    KSPhotoViewCell *cell = (id)[self.collectionView cellForItemAtIndexPath:indexPath];

    // Creates a temporary imageView that will occupy the full screen and rotate.
    UIImageView *imageView = [[UIImageView alloc] initWithImage:[[cell imageView] image]];
    [imageView setFrame:[self.view bounds]];
    [imageView setTag:kTemporaryImageTag];
    [imageView setBackgroundColor:[UIColor blackColor]];
    [imageView setContentMode:[[cell imageView] contentMode]];
    [imageView setAutoresizingMask:0xff];
    [self.view insertSubview:imageView aboveSubview:self.collectionView];

    // Invalidate layout and calculate (next) contentOffset.
    contentOffsetAfterRotation = CGPointMake(indexPath.item * [self.view bounds].size.height, 0);
    [[self.collectionView collectionViewLayout] invalidateLayout];
}

请注意,我的UICollectionViewCell子类具有公共imageView属性。

3) 最后一步是将内容偏移“捕捉”到有效页面并删除临时imageView。

- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
    [self.collectionView setContentOffset:contentOffsetAfterRotation];
    [[self.view viewWithTag:kTemporaryImageTag] removeFromSuperview];
}

1
你的答案很棒!但是你错过了在willRotateToInterfaceOrientation和didRotateFromInterfaceOrientation中调用super。 - Valeriy Van
这对我来说不起作用,我改为旋转后不计算偏移量,而是在获取单元格信息时存储NSIndexPath,并且不设置偏移量,而是滚动到该位置而没有动画。如果有人感兴趣,我可以编写一个实际的答案和代码(Swift)。 - mwright

16

上面的“只需轻按”答案对我没用,因为它经常不能停留在旋转前所看到的项目上。所以我设计了一个流式布局,它使用一个焦点项(如果设置了)来计算内容偏移量。我在willAnimateRotationToInterfaceOrientation中设置该项,并在didRotateFromInterfaceOrientation中清除它。由于CollectionView可以在顶部栏下进行布局,所以在IOS7上似乎需要进行插图调整。

@interface HintedFlowLayout : UICollectionViewFlowLayout
@property (strong)NSIndexPath* pathForFocusItem;
@end

@implementation HintedFlowLayout

-(CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset
{
    if (self.pathForFocusItem) {
        UICollectionViewLayoutAttributes* layoutAttrs = [self layoutAttributesForItemAtIndexPath:self.pathForFocusItem];
        return CGPointMake(layoutAttrs.frame.origin.x - self.collectionView.contentInset.left, layoutAttrs.frame.origin.y-self.collectionView.contentInset.top);
    }else{
        return [super targetContentOffsetForProposedContentOffset:proposedContentOffset];
    }
}
@end

迄今为止最简单的解决方案 - Just a coder

14

Swift 4.2子类:

class RotatableCollectionViewFlowLayout: UICollectionViewFlowLayout {

    private var focusedIndexPath: IndexPath?

    override func prepare(forAnimatedBoundsChange oldBounds: CGRect) {
        super.prepare(forAnimatedBoundsChange: oldBounds)
        focusedIndexPath = collectionView?.indexPathsForVisibleItems.first
    }

    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
        guard let indexPath = focusedIndexPath
            , let attributes = layoutAttributesForItem(at: indexPath)
            , let collectionView = collectionView else {
                return super.targetContentOffset(forProposedContentOffset: proposedContentOffset)
        }
        return CGPoint(x: attributes.frame.origin.x - collectionView.contentInset.left,
                       y: attributes.frame.origin.y - collectionView.contentInset.top)
    }

    override func finalizeAnimatedBoundsChange() {
        super.finalizeAnimatedBoundsChange()
        focusedIndexPath = nil
    }
}

1
这正是我想要的。我希望旋转代码包含在我的自定义UICollectionView子类中,这样就不需要在整个应用程序中重复旋转代码。但这确实会导致滚动发生,这意味着单元格重用会启动。这可能会产生意外的副作用。 - Daniel Williams
很遗憾,targetContentOffset 对我来说没有被调用。 - aheze
1
现在有了 UICollectionViewCompositionalLayout,这个答案还有效吗?因为我无法这样做。 - Ajay Singh Thakur
这是正确的解决方案!但要小心,在prepare(forAnimatedBoundsChange oldBounds: CGRect) {}中,collectionView?.bounds将是新的边界。如果你想手动计算焦点的indexPath,你应该使用oldBoundscollectionView?.contentOffset,在旋转时默认情况下不会改变。 - undefined

10

若您使用的是iOS 8+,willRotateToInterfaceOrientationdidRotateFromInterfaceOrientation 已被弃用。

现在应该使用以下内容:

/* 
This method is called when the view controller's view's size is changed by its parent (i.e. for the root view controller when its window rotates or is resized). 
If you override this method, you should either call super to propagate the change to children or manually forward the change to children.
*/
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator 
{
    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];

    [coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
        // Update scroll position during rotation animation
        self.collectionView.contentOffset = (CGPoint){contentOffsetX, contentOffsetY};
    } completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
        // Whatever you want to do when the rotation animation is done
    }];
}

Swift 3:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)

    coordinator.animate(alongsideTransition: { (context:UIViewControllerTransitionCoordinatorContext) in
        // Update scroll position during rotation animation
    }) { (context:UIViewControllerTransitionCoordinatorContext) in
        // Whatever you want to do when the rotation animation is done
    }
}

3
全屏单元格怎么样?当我滑动到第二或第三个单元格并旋转时,我会在最后一个看到上一个单元格,在第二个看到最后一个单元格 :( - iTux

8

我认为正确的解决方案是在子类化的UICollectionViewFlowLayout中重写targetContentOffsetForProposedContentOffset:方法。

根据文档:

在布局更新期间或在布局之间转换时,集合视图调用此方法,使您有机会更改在动画结束时要使用的建议内容偏移量。如果动画或转换可能导致项目的定位不适合您的设计,则可以覆盖此方法。


5

为了借鉴troppoli的解决方案,您可以在自定义类中设置偏移量,而无需担心记得在视图控制器中实现代码。当您旋转设备时,应调用prepareForAnimatedBoundsChange,然后在旋转完成后调用finalizeAnimatedBoundsChange

@interface OrientationFlowLayout ()

@property (strong)NSIndexPath* pathForFocusItem;

@end

@implementation OrientationFlowLayout

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset {
    if (self.pathForFocusItem) {
        UICollectionViewLayoutAttributes* layoutAttrs = [self layoutAttributesForItemAtIndexPath:
                                                         self.pathForFocusItem];
        return CGPointMake(layoutAttrs.frame.origin.x - self.collectionView.contentInset.left,
                           layoutAttrs.frame.origin.y - self.collectionView.contentInset.top);
    }
    else {
        return [super targetContentOffsetForProposedContentOffset:proposedContentOffset];
    }
}

- (void)prepareForAnimatedBoundsChange:(CGRect)oldBounds {
    [super prepareForAnimatedBoundsChange:oldBounds];
    self.pathForFocusItem = [[self.collectionView indexPathsForVisibleItems] firstObject];
}

- (void)finalizeAnimatedBoundsChange {
    [super finalizeAnimatedBoundsChange];
    self.pathForFocusItem = nil;
}

@end

调用super是必要的吗? - Just a coder
我在Xamarin中实现了这个,它在iOS 11上运行得很好。 - Patrick Roberts

3

我使用fz的变体答案(iOS 7和8):

旋转之前:

  1. 存储当前可见的IndexPath
  2. 创建collectionView的快照
  3. 在collectionView上方放置一个带有快照的UIImageView

旋转之后:

  1. Scroll to the stored index
  2. Remove the image view.

    @property (nonatomic) NSIndexPath *indexPath;
    
    - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
                                    duration:(NSTimeInterval)duration {
        self.indexPathAfterRotation = [[self.collectionView indexPathsForVisibleItems] firstObject];
    
        // Creates a temporary imageView that will occupy the full screen and rotate.
        UIGraphicsBeginImageContextWithOptions(self.collectionView.bounds.size, YES, 0);
        [self.collectionView drawViewHierarchyInRect:self.collectionView.bounds afterScreenUpdates:YES];
        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
    
        UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
        [imageView setFrame:[self.collectionView bounds]];
        [imageView setTag:kTemporaryImageTag];
        [imageView setBackgroundColor:[UIColor blackColor]];
        [imageView setContentMode:UIViewContentModeCenter];
        [imageView setAutoresizingMask:0xff];
        [self.view insertSubview:imageView aboveSubview:self.collectionView];
    
        [[self.collectionView collectionViewLayout] invalidateLayout];
    }
    
    - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
        [self.collectionView scrollToItemAtIndexPath:self.indexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:NO];
    
        [[self.view viewWithTag:kTemporaryImageTag] removeFromSuperview];
    }
    

1
我最终也使用了类似的东西,但真希望我先看到了你的。 - mwright

3

这个问题也困扰了我一段时间。最高票的答案对我来说有点太hacky了,所以我简化了它,并在旋转前后分别更改集合视图的alpha值。我还没有动画更新内容偏移量。

- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
self.collectionView.alpha = 0;
[self.collectionView.collectionViewLayout invalidateLayout];

self.scrollPositionBeforeRotation = CGPointMake(self.collectionView.contentOffset.x / self.collectionView.contentSize.width,
                                                self.collectionView.contentOffset.y / self.collectionView.contentSize.height);
}

- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation;
{
CGPoint newContentOffset = CGPointMake(self.scrollPositionBeforeRotation.x * self.collectionView.contentSize.width,
                                       self.scrollPositionBeforeRotation.y * self.collectionView.contentSize.height);

[self.collectionView setContentOffset:newContentOffset animated:NO];
self.collectionView.alpha = 1;
}

相对来说较为流畅,且不那么粗糙。

2
旋转界面方向后,UICollectionViewCell通常会移动到另一个位置,因为我们不会更新contentSize和contentOffset。
因此,可见的UICollectionViewCell总是没有位于预期位置。
我们期望图片如下所示的可见的UICollectionView: 我们期望的方向 UICollectionView必须委托『UICollectionViewDelegateFlowLayout』的函数[collectionView sizeForItemAtIndexPath]。
并且你应该在这个函数中计算item Size。
自定义的UICollectionViewFlowLayout必须重写以下函数:
  1. -(void)prepareLayout

    .设置itemSize、scrollDirection等。

  2. -(CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity

    .计算页码或计算可见内容偏移量。

  3. -(CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset

    .返回可视内容偏移量。

  4. -(CGSize)collectionViewContentSize

    .返回collectionView的总内容大小。

您的viewController必须重写『willRotateToInterfaceOrientation』函数,在此函数中,您应该调用函数[XXXCollectionVew.collectionViewLayout invalidateLayout]。
但是,在iOS 9中,『willRotateToInterfaceOrientation』已被弃用,您可以以不同的方式调用函数 [XXXCollectionVew.collectionViewLayout invalidateLayout]。
以下是一个示例: https://github.com/bcbod2002/CollectionViewRotationTest

1
请描述您所做的或者讲述有关您的项目的任何内容! - Alex Cio

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接