Keeping the contentOffset in a UICollectionView while rotating Interface Orientation

后端 未结 24 1352
野性不改
野性不改 2020-12-04 07:22

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 afte

相关标签:
24条回答
  • 2020-12-04 08:17

    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.

    0 讨论(0)
  • 2020-12-04 08:20

    If found that using targetContentOffsetForProposedContentOffset does not work in all scenarios and the problem with using didRotateFromInterfaceOrientation is that it gives visual artifacts. My perfectly working code is as follows:

    - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
    {
        [super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
        _indexPathOfFirstCell = [self indexPathsForVisibleItems].firstObject;
    }
    
    - (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
        [super willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration];
        if (_indexPathOfFirstCell) {
            [UIView performWithoutAnimation:^{
                [self scrollToItemAtIndexPath:self->_indexPathOfFirstCell atScrollPosition:UICollectionViewScrollPositionTop animated:NO];
            }];
            _indexPathOfFirstCell = nil;
        }
    }
    

    The key is to use the willRotateToInterfaceOrientation method to determine the part in the view that you want to scroll to and willAnimationRotationToInterfaceOrientation to recalculate it when the view has changed its size (the bounds have already changed when this method is called by the framework) and to actually scroll to the new position without animation. In my code I used the index path for the first visual cell to do that, but a percentage of contentOffset.y/contentSize.height would also do the job in slightly different way.

    0 讨论(0)
  • 2020-12-04 08:20

    I solved this problem with Following Steps:

    1. Calculate currently scrolled NSIndexPath
    2. Disable Scrolling and Pagination in UICollectionView
    3. Apply new Flow Layout to UICollectionView
    4. Enable Scrolling and Pagination in UICollectionView
    5. Scroll UICollectionView to current NSIndexPath

    Here is the Code Template demonstrating the Above Steps:

    - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
                                duration:(NSTimeInterval)duration;
    {
         //Calculating Current IndexPath
         CGRect visibleRect = (CGRect){.origin = self.yourCollectionView.contentOffset, .size = self.yourCollectionView.bounds.size};
         CGPoint visiblePoint = CGPointMake(CGRectGetMidX(visibleRect), CGRectGetMidY(visibleRect));
         self.currentIndexPath = [self.yourCollectionView indexPathForItemAtPoint:visiblePoint];
    
         //Disable Scrolling and Pagination
         [self disableScrolling];
    
         //Applying New Flow Layout
         [self setupNewFlowLayout];
    
         //Enable Scrolling and Pagination
         [self enableScrolling];
    }
    
    - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation;
    {
         //You can also call this at the End of `willRotate..` method.
         //Scrolling UICollectionView to current Index Path
         [self.yourCollectionView scrollToItemAtIndexPath:self.currentIndexPath atScrollPosition:UICollectionViewScrollPositionCenteredVertically animated:NO];
    }
    
    - (void) disableScrolling
    {
        self.yourCollectionView.scrollEnabled   = false;
        self.yourCollectionView.pagingEnabled   = false;
    }
    
    - (void) enableScrolling
    {
        self.yourCollectionView.scrollEnabled   = true;
        self.yourCollectionView.pagingEnabled   = true;
    }
    
    - (void) setupNewFlowLayout
    {
        UICollectionViewFlowLayout* flowLayout = [[UICollectionViewFlowLayout alloc] init];
        flowLayout.sectionInset = UIEdgeInsetsMake(0, 0, 0, 0);
        flowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
        flowLayout.minimumInteritemSpacing = 0;
        flowLayout.minimumLineSpacing = 0;
        [flowLayout setItemSize:CGSizeMake(EXPECTED_WIDTH, EXPECTED_HEIGHT)];
    
        [self.yourCollectionView setCollectionViewLayout:flowLayout animated:YES];
        [self.yourCollectionView.collectionViewLayout invalidateLayout];
    }
    

    I hope this helps.

    0 讨论(0)
  • 2020-12-04 08:21

    Solution 1, "just snap"

    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:

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

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

    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:

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

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

    - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
    {
        [self.collectionView setContentOffset:contentOffsetAfterRotation];
        [[self.view viewWithTag:kTemporaryImageTag] removeFromSuperview];
    }
    
    0 讨论(0)
  • 2020-12-04 08:21

    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.

    0 讨论(0)
  • 2020-12-04 08:22

    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

    0 讨论(0)
提交回复
热议问题