ios 在旋转界面方向时将 contentOffset 保留在 UICollectionView 中

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/13490065/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-30 21:15:47  来源:igfitidea点击:

Keeping the contentOffset in a UICollectionView while rotating Interface Orientation

iosobjective-ccocoa-touchuiscrollviewuicollectionview

提问by Tobias Kr?ntzer

I'm trying to handle interface orientation changes in a UICollectionViewController. What I'm trying to achieve is, that I want to have the samecontentOffset after an interface rotation. Meaning, that it should be changed corresponding to the ratio of the bounds change.

我正在尝试处理 UICollectionViewController 中的界面方向更改。我想要实现的是,我希望在界面旋转后具有相同的contentOffset。意思是,它应该根据边界变化的比例而改变。

Starting in portrait with a content offset of {bounds.size.width* 2, 0} …

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

UICollectionView in portait

肖像中的 UICollectionView

… should result to the content offset in landscape also with {bounds.size.width* 2, 0} (and vice versa).

... 应该导致横向内容偏移也与 { bounds.size.width* 2, 0} (反之亦然)。

UICollectionView in landscape

横向的 UICollectionView

Calculating the new offset is not the problem, but don't know, where (or when) to set it, to get a smooth animation. What I'm doing so fare is invalidating the layout in willRotateToInterfaceOrientation:duration:and resetting the content offset in didRotateFromInterfaceOrientation::

计算新的偏移量不是问题,但不知道在哪里(或何时)设置它,以获得流畅的动画。到目前为止,我正在做的是使布局无效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];
}

This changes the content offset after the rotation.

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

How can I set it during the rotation? I tried to set the new content offset in willAnimateRotationToInterfaceOrientation:duration:but this results into a very strange behavior.

如何在旋转过程中设置它?我试图设置新的内容偏移量,willAnimateRotationToInterfaceOrientation:duration:但这会导致非常奇怪的行为。

An example can be found in my Project on GitHub.

可以在我的GitHub项目中找到一个示例。

回答by Gurjinder Singh

You can either do this in the view controller:

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

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

Or in the layout itself: https://stackoverflow.com/a/54868999/308315

或者在布局本身:https: //stackoverflow.com/a/54868999/308315

回答by fz.

Solution 1, "just snap"

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

If what you need is only to ensure that the contentOffset ends in a right position, you can create a subclass of UICollectionViewLayout and implement targetContentOffsetForProposedContentOffset:method. For example you could do something like this to calculate the page:

如果您只需要确保 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);
}

But the problem that you'll face is that the animation for that transition is extremely weird. What I'm doing on my case (which is almost the same as yours) is:

但是您将面临的问题是该过渡的动画非常奇怪。我在我的情况下(这几乎和你的一样)是:

Solution 2, "smooth animation"

方案二、“平滑动画”

1) First I set the cell size, which can be managed by collectionView:layout:sizeForItemAtIndexPath:delegate method as follows:

1)首先我设置单元格大小,可以通过collectionView:layout:sizeForItemAtIndexPath:delegate方法管理如下:

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

Note that [self.view bounds] will change according to the device rotation.

请注意,[self.view bounds] 会根据设备旋转而变化。

2) When the device is about to rotate, I'm adding an imageView on top of the collection view with all resizing masks. This view will actually hide the collectionView weirdness (because it is on top of it) and since the willRotatoToInterfaceOrientation: method is called inside an animation block it will rotate accordingly. I'm also keeping the next contentOffset according to the shown indexPath so I can fix the contentOffset once the rotation is done:

2)当设备即将旋转时,我在集合视图的顶部添加了一个带有所有调整大小蒙版的图像视图。这个视图实际上会隐藏 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];
}

Note that my subclass of UICollectionViewCell has a public imageView property.

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

3) Finally, the last step is to "snap" the content offset to a valid page and remove the temporary imageview.

3) 最后,最后一步是将内容偏移“对齐”到有效页面并删除临时图像视图。

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

回答by troppoli

The "just snap" answer above didn't work for me as it frequently didn't end on the item that was in view before the rotate. So I derived a flow layout that uses a focus item (if set) for calculating the content offset. I set the item in willAnimateRotationToInterfaceOrientation and clear it in didRotateFromInterfaceOrientation. The inset adjustment seems to be need on IOS7 because the Collection view can layout under the top bar.

上面的“just snap”答案对我不起作用,因为它经常不会以旋转前可见的项目结束。所以我派生了一个流布局,它使用焦点项(如果设置)来计算内容偏移量。我在 willAnimateRotationToInterfaceOrientation 中设置项目并在 didRotateFromInterfaceOrientation 中清除它。IOS7 上好像需要调整inset,因为Collection view 可以在top bar 下布局。

@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

回答by Neilc

For those using iOS 8+, willRotateToInterfaceOrientationand didRotateFromInterfaceOrientationare deprecated.

对于使用 iOS 8+ 的用户,不推荐使用willRotateToInterfaceOrientationdidRotateFromInterfaceOrientation

You should use the following now:

您现在应该使用以下内容:

/* 
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:

斯威夫特 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
    }
}

回答by 2cupsOfTech

I think the correct solution is to override targetContentOffsetForProposedContentOffset:method in a subclassed UICollectionViewFlowLayout

我认为正确的解决方案是在子类中覆盖targetContentOffsetForProposedContentOffset:方法UICollectionViewFlowLayout

From the docs:

从文档:

During layout updates, or when transitioning between layouts, the collection view calls this method to give you the opportunity to change the proposed content offset to use at the end of the animation. You might override this method if the animations or transition might cause items to be positioned in a way that is not optimal for your design.

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

回答by Bogdan Novikov

Swift 4.2 subclass:

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

回答by Joe

To piggy back off troppoli'ssolution you can set the offset in your custom class without having to worry about remembering to implement the code in your view controller. prepareForAnimatedBoundsChangeshould get called when you rotate the device then finalizeAnimatedBoundsChangeafter its done rotating.

为了利用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

回答by dulgan

I use a variant of fz. answer (iOS 7 & 8) :

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

Before rotation :

轮换前:

  1. Store the current visible index path
  2. Create a snapshot of the collectionView
  3. Put an UIImageView with it on top of the collection view
  1. 存储当前可见的索引路径
  2. 创建 collectionView 的快照
  3. 将带有它的 UIImageView 放在集合视图的顶部

After rotation :

旋转后:

  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. 滚动到存储的索引
  2. 删除图像视图。

    @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];
    }
    

回答by mafiOSo

This problem bothered me for a bit as well. The highest voted answered seemed a bit too hacky for me so I just dumbed it down a bit and just change the alpha of the collection view respectively before and after rotation. I also don't animate the content offset update.

这个问题也困扰了我一段时间。投票最高的答案对我来说似乎有点太老套了,所以我只是把它简化了一点,只是在旋转前后分别更改了集合视图的 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;
}

Fairly smooth and less hacky.

相当流畅,不那么笨拙。

回答by Goston Wu

After rotate interface orientation the UICollectionViewCell usually move to another position, because we won't update contentSize and contentOffset.

旋转界面方向后 UICollectionViewCell 通常会移动到另一个位置,因为我们不会更新 contentSize 和 contentOffset。

So the visible UICollectionViewCell always not locate at expected position.

所以可见的 UICollectionViewCell 总是找不到预期的位置。

The visible UICollectionView which we expected image as follow

我们期望图像的可见 UICollectionView 如下

Orientation which we expected

我们预期的方向

UICollectionView must delegate the function [collectionView sizeForItemAtIndexPath] of『UICollectionViewDelegateFlowLayout』.

UICollectionView 必须委托『UICollectionViewDelegateFlowLayout』的函数[collectionView sizeForItemAtIndexPath]。

And you should calculate the item Size in this function.

您应该在此函数中计算项目大小。

The custom UICollectionViewFlowLayout must override the functions as follow.

自定义 UICollectionViewFlowLayout 必须覆盖以下功能。

  1. -(void)prepareLayout

    . Set itemSize, scrollDirection and others.

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

    . Calculate page number or calculate visible content offset.

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

    . Return visual content offset.

  4. -(CGSize)collectionViewContentSize

    . Return the total content size of collectionView.

  1. -(void)prepareLayout

    .设置 itemSize、scrollDirection 等。

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

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

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

    .返回视觉内容偏移量。

  4. -(CGSize)collectionViewContentSize

    .返回 collectionView 的总内容大小。

Your viewController must override 『willRotateToInterfaceOrientation』and in this function you should call the function [XXXCollectionVew.collectionViewLayout invalidateLayout];

你的 viewController 必须覆盖『willRotateToInterfaceOrientation』,在这个函数中你应该调用函数 [XXXCollectionVew.collectionViewLayout invalidateLayout];

But 『willRotateToInterfaceOrientation』 is deprecated in iOS 9, or you could call the function [XXXCollectionVew.collectionViewLayout invalidateLayout] in difference way.

但是 『willRotateToInterfaceOrientation』 在 iOS 9 中已被弃用,或者您可以以不同的方式调用函数 [XXXCollectionVew.collectionViewLayout invalidateLayout]。

There's an example as follow : https://github.com/bcbod2002/CollectionViewRotationTest

有一个例子如下:https: //github.com/bcbod2002/CollectionViewRotationTest