iosobjective-cscrolluicollectionviewscroll-paging

UICollectionView with CustomFlowLayout How to restricts scroll to only one page per scroll?


I have implemented customFlowLayout in my iOS App. And I have subclassed the targetContentOffsetForProposedContentOffset:withScrollingVelocity with subclassing UICollectionViewFlowLayout. Now my issue is when user scrolls the collectionview it must scroll to only next index. Right now it scrolled randomly.
So anyone have any idea that how can I make the scroll restricts to only one item per scroll.
Following is my code.

#pragma mark - UICollectionViewLayout (UISubclassingHooks)

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {
  CGSize collectionViewSize = self.collectionView.bounds.size;
  CGFloat proposedContentOffsetCenterX = proposedContentOffset.x + collectionViewSize.width / 2;
  CGRect proposedRect = CGRectMake(proposedContentOffset.x, 0, collectionViewSize.width, collectionViewSize.height);
  UICollectionViewLayoutAttributes *candidateAttributes;
  for (UICollectionViewLayoutAttributes *attributes in [self layoutAttributesForElementsInRect:proposedRect]) {
    if (attributes.representedElementCategory != UICollectionElementCategoryCell) continue;
    if (!candidateAttributes) {
      candidateAttributes = attributes;
      continue;
    }
    if (fabs(attributes.center.x - proposedContentOffsetCenterX) < fabs(candidateAttributes.center.x - proposedContentOffsetCenterX)) {
      candidateAttributes = attributes;
    }
  }

  proposedContentOffset.x = candidateAttributes.center.x - self.collectionView.bounds.size.width / 2;

  CGFloat offset = proposedContentOffset.x - self.collectionView.contentOffset.x;

  if ((velocity.x < 0 && offset > 0) || (velocity.x > 0 && offset < 0)) {
    CGFloat pageWidth = self.itemSize.width + self.minimumLineSpacing;
    proposedContentOffset.x += velocity.x > 0 ? pageWidth : -pageWidth;
  }

  return proposedContentOffset;
}

- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
  return YES;
}

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
  if (!self.scaleItems) return [super layoutAttributesForElementsInRect:rect];

  NSArray *attributesArray = [[NSArray alloc] initWithArray:[super layoutAttributesForElementsInRect:rect] copyItems:YES];

  CGRect visibleRect = (CGRect){self.collectionView.contentOffset, self.collectionView.bounds.size};
  CGFloat visibleCenterX = CGRectGetMidX(visibleRect);

  [attributesArray enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes *attributes, NSUInteger idx, BOOL *stop) {
    CGFloat distanceFromCenter = visibleCenterX - attributes.center.x;
    CGFloat absDistanceFromCenter = MIN(ABS(distanceFromCenter), self.scalingOffset);
    CGFloat scale = absDistanceFromCenter * (self.minimumScaleFactor - 1) / self.scalingOffset + 1;
    attributes.transform3D = CATransform3DScale(CATransform3DIdentity, scale, scale, 1);
  }];

  return attributesArray;
}

Solution

  • Your code looks like it should scroll nicely based on the user request. i.e. if they scroll quickly it will skip a number of items and land nicely on a later item, if they scroll slowly it will proceed to the next or return to the previous item nicely depending on the scroll distance. But, this isn't what you say you want.

    What you want may not be very nice to use when the user tries to scroll quickly...

    Anyway, to get what you want you basically want to use proposedContentOffset only to determine the scroll direction (is it greater than or less than the current content offset).

    Now, once you have that you can get the layout attributes for the items at the next or previous page (rather than the current code which may get attributes many pages away). This is the current offset + or - the view width.

    The rest of your code then stays the same. Ignoring the scroll direction this is something like:

    - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity
    {
        CGSize collectionViewSize = self.collectionView.bounds.size;
        CGFloat width = collectionViewSize.width;
        CGFloat halfWidth = width * 0.5;
    
        CGFloat direction = (proposedContentOffset.x > self.collectionView.contentOffset.x ? 1 : 0);
        CGFloat pageOffsetX = 250.0 * floor(self.collectionView.contentOffset.x / 250.0); 
        CGFloat proposedContentOffsetCenterX = pageOffsetX + (width * direction);
        CGRect proposedRect = CGRectMake(proposedContentOffsetCenterX, 0, collectionViewSize.width, collectionViewSize.height);
    
        UICollectionViewLayoutAttributes *candidateAttributes;
    
        for (UICollectionViewLayoutAttributes *attributes in [self layoutAttributesForElementsInRect:proposedRect]) {
            if (attributes.representedElementCategory != UICollectionElementCategoryCell) continue;
    
            candidateAttributes = attributes;
            break;
        }
    
        proposedContentOffset.x = candidateAttributes.center.x - halfWidth;
    
    //     CGFloat offset = proposedContentOffset.x - self.collectionView.contentOffset.x;
    //     
    //     if ((velocity.x < 0 && offset > 0) || (velocity.x > 0 && offset < 0)) {
    //         CGFloat pageWidth = self.itemSize.width + self.minimumLineSpacing;
    //         proposedContentOffset.x += velocity.x > 0 ? pageWidth : -pageWidth;
    //     }
    
        return proposedContentOffset;
    }
    

    I've commented out the part at the bottom as it shouldn't be required for the initial version. Test it out with a simple version first and then elaborate if you need more control in edge cases.