Keeping the contentOffset in a UICollectionView wh

2019-01-12 19:15发布

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

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

UICollectionView in portait

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

UICollectionView in landscape

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

- (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.

An example can be found in my Project on GitHub.

23条回答
可以哭但决不认输i
2楼-- · 2019-01-12 19:30

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.

@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
查看更多
够拽才男人
3楼-- · 2019-01-12 19:30

I use a variant of fz. answer (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

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];
    }
    
查看更多
我欲成王,谁敢阻挡
4楼-- · 2019-01-12 19:33

I have a similar case in which i use this

- (void)setFrame:(CGRect)frame
{
    CGFloat currentWidth = [self frame].size.width;
    CGFloat offsetModifier = [[self collectionView] contentOffset].x / currentWidth;

    [super setFrame:frame];

    CGFloat newWidth = [self frame].size.width;

    [[self collectionView] setContentOffset:CGPointMake(offsetModifier * newWidth, 0.0f) animated:NO];
}

This is a view that contains a collectionView. In the superview I also do this

- (void)setFrame:(CGRect)frame
{    
    UICollectionViewFlowLayout *collectionViewFlowLayout = (UICollectionViewFlowLayout *)[_collectionView collectionViewLayout];

    [collectionViewFlowLayout setItemSize:frame.size];

    [super setFrame:frame];
}

This is to adjust the cell sizes to be full screen (full view to be exact ;) ). If you do not do this here a lot of error messages may appear about that the cell size is bigger than the collectionview and that the behaviour for this is not defined and bla bla bla.....

These to methods can off course be merged into one subclass of the collectionview or in the view containing the collectionview but for my current project was this the logical way to go.

查看更多
看我几分像从前
5楼-- · 2019-01-12 19:35

I think the correct solution is to override - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset method in a subclassed 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.

查看更多
太酷不给撩
6楼-- · 2019-01-12 19:35

in Swift 3.

you should track which cell item(Page) is being presented before rotate by indexPath.item, the x coordinate or something else. Then, in your UICollectionView:

override func collectionView(_ collectionView: UICollectionView, targetContentOffsetForProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {

    let page:CGFloat = pageNumber // your tracked page number eg. 1.0
    return CGPoint(x: page * collectionView.frame.size.width, y: -(topInset))
    //the 'y' value would be '0' if you don't have any top EdgeInset
}

In my case I invalidate the layout in viewDidLayoutSubviews() so the collectionView.frame.size.width is the width of the collectionVC's view that has been rotated.

查看更多
孤傲高冷的网名
7楼-- · 2019-01-12 19:35

You might want to hide the collectionView during it's (incorrect) animation and show a placeholder view of the cell that rotates correctly instead.

For a simple photo gallery I found a way to do it that looks quite good. See my answer here: How to rotate a UICollectionView similar to the photos app and keep the current view centered?

查看更多
登录 后发表回答