Uicollectionview Horizontal Paging Not Centered

UICollectionView Horizontal Paging not centered

Remove spaces between items. For horizontal scrolling collection view set minimum line spacing to 0. You can do this with interface builder or with method of UICollectionViewDelegateFlowLayout protocol:

- (CGFloat)collectionView:(UICollectionView *)collectionView 
layout:(UICollectionViewLayout *)collectionViewLayout
minimumLineSpacingForSectionAtIndex:(NSInteger)section {
return 0;
}

Sample Image

Another way is making your cell's width less than collectionView's width for a value of horizontal space between items. Then add section insets with left and right insets that equal a half of horizontal space between items. For example, your minimum line spacing is 10:

- (CGFloat)collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout *)collectionViewLayout
minimumLineSpacingForSectionAtIndex:(NSInteger)section {
return 10;
}

- (CGSize)collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout *)collectionViewLayout
sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
return CGSizeMake(collectionView.frame.size.width - 10, collectionView.frame.size.height);
}

- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout *)collectionViewLayout
insetForSectionAtIndex:(NSInteger)section {
return UIEdgeInsetsMake(0, 5, 0, 5);
}

Sample Image

And third way: manipulate collectionView scroll in scrollViewDidEndDecelerating: method:

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
if (scrollView == self.collectionView) {
CGPoint currentCellOffset = self.collectionView.contentOffset;
currentCellOffset.x += self.collectionView.frame.size.width / 2;
NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:currentCellOffset];
[self.collectionView scrollToItemAtIndexPath:indexPath
atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally
animated:YES];
}
}

Sample Image

UICollectionView horizontal paging not centring after rotation

You don't seem to have handled rotations. You need to reloadData when a rotation occurs. You'll get the event in UIViewController from there you can call reloadData on the instance of your UICollectionView.

class GalleryViewController: UIViewController {
//...
var galleryScrollView: GalleryScrollView
//...
override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
super.willTransition(to: newCollection, with: coordinator)
galleryScrollView.collectionViewLayout.invalidateLayout()
}
//...
}

Then in GalleryScrollView:

class GalleryScrollView: UIView {
var currentVisibleIndexPath = IndexPath(row: 0, section: 0)
//....
}

extension GalleryScrollView: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
//...
func collectionView(_ collectionView: UICollectionView, targetContentOffsetForProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
let attributes = collectionView.layoutAttributesForItem(at: currentVisibleIndexPath)
let newOriginForOldIndex = attributes?.frame.origin
return newOriginForOldIndex ?? proposedContentOffset
}

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {

let center = CGPoint(x: scrollView.contentOffset.x + (scrollView.frame.width / 2), y: (scrollView.frame.height / 2))
if let indexPath = self.screenView.indexPathForItem(at: center) {
currentVisibleIndexPath = indexPath
}
}
}

UICollectionView horizontal paging with space between pages

Solution one:

  1. collectionView.isPagingEnabled = false
  2. add a minimumLineSpacing for the distance between pages
  3. implement targetContentOffsetForProposedContentOffset:withScrollingVelocity: to move the contentOffset to the closest page. You can calculate the page with simple math based on your itemSize and minimumLineSpacing, but it can take a little work to get it right.

Solution Two:

  1. collectionView.isPagingEnabled = true
  2. add a minimumLineSpacing for the distance between pages
  3. the paging size is based on the bounds of the collectionView. So make the collectionView larger then then screenSize. For example, if you have a minimumLineSpacing of 10 then set the frame of the collectionView to be {0,-5, width+10, height}
  4. set a contentInset equal to the minimumLineSpacing to make the first and last item appear correctly.

UICollectionView align logic missing in horizontal paging scrollview

The fundamental issue is Flow Layout is not designed to support the paging. To achieve the paging effect, you will have to sacrifice the space between cells. And carefully calculate the cells frame and make it can be divided by the collection view frame without remainders. I will explain the reason.

Saying the following layout is what you wanted.

Sample Image

Notice, the most left margin (green) is not part of the cell spacing. It is determined by the flow layout section inset. Since flow layout doesn't support heterogeneous spacing value. It is not a trivial task.

Therefore, after setting the spacing and inset. The following layout is what you will get.

Sample Image

After scroll to next page. Your cells are obviously not aligned as what you expected.

Sample Image

Making the cell spacing 0 can solve this issue. However, it limits your design if you want the extra margin on the page, especially if the margin is different from the cell spacing. It also requires the view frame must be divisible by the cell frame. Sometimes, it is a pain if your view frame is not fixed (considering the rotation case).

The real solution is to subclass UICollectionViewFlowLayout and override following methods

- (CGSize)collectionViewContentSize
{
// Only support single section for now.
// Only support Horizontal scroll
NSUInteger count = [self.collectionView.dataSource collectionView:self.collectionView
numberOfItemsInSection:0];

CGSize canvasSize = self.collectionView.frame.size;
CGSize contentSize = canvasSize;
if (self.scrollDirection == UICollectionViewScrollDirectionHorizontal)
{
NSUInteger rowCount = (canvasSize.height - self.itemSize.height) / (self.itemSize.height + self.minimumInteritemSpacing) + 1;
NSUInteger columnCount = (canvasSize.width - self.itemSize.width) / (self.itemSize.width + self.minimumLineSpacing) + 1;
NSUInteger page = ceilf((CGFloat)count / (CGFloat)(rowCount * columnCount));
contentSize.width = page * canvasSize.width;
}

return contentSize;
}

- (CGRect)frameForItemAtIndexPath:(NSIndexPath *)indexPath
{
CGSize canvasSize = self.collectionView.frame.size;

NSUInteger rowCount = (canvasSize.height - self.itemSize.height) / (self.itemSize.height + self.minimumInteritemSpacing) + 1;
NSUInteger columnCount = (canvasSize.width - self.itemSize.width) / (self.itemSize.width + self.minimumLineSpacing) + 1;

CGFloat pageMarginX = (canvasSize.width - columnCount * self.itemSize.width - (columnCount > 1 ? (columnCount - 1) * self.minimumLineSpacing : 0)) / 2.0f;
CGFloat pageMarginY = (canvasSize.height - rowCount * self.itemSize.height - (rowCount > 1 ? (rowCount - 1) * self.minimumInteritemSpacing : 0)) / 2.0f;

NSUInteger page = indexPath.row / (rowCount * columnCount);
NSUInteger remainder = indexPath.row - page * (rowCount * columnCount);
NSUInteger row = remainder / columnCount;
NSUInteger column = remainder - row * columnCount;

CGRect cellFrame = CGRectZero;
cellFrame.origin.x = pageMarginX + column * (self.itemSize.width + self.minimumLineSpacing);
cellFrame.origin.y = pageMarginY + row * (self.itemSize.height + self.minimumInteritemSpacing);
cellFrame.size.width = self.itemSize.width;
cellFrame.size.height = self.itemSize.height;

if (self.scrollDirection == UICollectionViewScrollDirectionHorizontal)
{
cellFrame.origin.x += page * canvasSize.width;
}

return cellFrame;
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewLayoutAttributes * attr = [super layoutAttributesForItemAtIndexPath:indexPath];
attr.frame = [self frameForItemAtIndexPath:indexPath];
return attr;
}

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray * originAttrs = [super layoutAttributesForElementsInRect:rect];
NSMutableArray * attrs = [NSMutableArray array];

[originAttrs enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes * attr, NSUInteger idx, BOOL *stop) {
NSIndexPath * idxPath = attr.indexPath;
CGRect itemFrame = [self frameForItemAtIndexPath:idxPath];
if (CGRectIntersectsRect(itemFrame, rect))
{
attr = [self layoutAttributesForItemAtIndexPath:idxPath];
[attrs addObject:attr];
}
}];

return attrs;
}

Notice, above code snippet only supports single section and horizontal scroll direction. But it is not hard to expand.

Also, if you don't have millions of cells. Caching those UICollectionViewLayoutAttributes may be a good idea.

UICollectionView horizontal paging with 3 items

Edit:
Demo link: https://github.com/raheelsadiq/UICollectionView-horizontal-paging-with-3-items

After a lot searching I did it, find the next point to scroll to and disable the paging. In scrollviewWillEndDragging scroll to next cell x.

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{

float pageWidth = 480 + 50; // width + space

float currentOffset = scrollView.contentOffset.x;
float targetOffset = targetContentOffset->x;
float newTargetOffset = 0;

if (targetOffset > currentOffset)
newTargetOffset = ceilf(currentOffset / pageWidth) * pageWidth;
else
newTargetOffset = floorf(currentOffset / pageWidth) * pageWidth;

if (newTargetOffset < 0)
newTargetOffset = 0;
else if (newTargetOffset > scrollView.contentSize.width)
newTargetOffset = scrollView.contentSize.width;

targetContentOffset->x = currentOffset;
[scrollView setContentOffset:CGPointMake(newTargetOffset, scrollView.contentOffset.y) animated:YES];
}

I also had to make the left and right small and center large, so i did it with transform.
The issue was finding the index, so that was very difficult to find.

For transform left and right in this same method use the newTargetOffset

int index = newTargetOffset / pageWidth;

if (index == 0) { // If first index
UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:index inSection:0]];

[UIView animateWithDuration:ANIMATION_SPEED animations:^{
cell.transform = CGAffineTransformIdentity;
}];
cell = [self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:index + 1 inSection:0]];
[UIView animateWithDuration:ANIMATION_SPEED animations:^{
cell.transform = TRANSFORM_CELL_VALUE;
}];
}else{
UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:index inSection:0]];
[UIView animateWithDuration:ANIMATION_SPEED animations:^{
cell.transform = CGAffineTransformIdentity;
}];

index --; // left
cell = [self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:index inSection:0]];
[UIView animateWithDuration:ANIMATION_SPEED animations:^{
cell.transform = TRANSFORM_CELL_VALUE;
}];

index ++;
index ++; // right
cell = [self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:index inSection:0]];
[UIView animateWithDuration:ANIMATION_SPEED animations:^{
cell.transform = TRANSFORM_CELL_VALUE;
}];
}

And in cellForRowAtIndex add

if (indexPath.row == 0 && isfirstTimeTransform) { // make a bool and set YES initially, this check will prevent fist load transform
isfirstTimeTransform = NO;
}else{
cell.transform = TRANSFORM_CELL_VALUE; // the new cell will always be transform and without animation
}

Add these two macros too or as u wish to handle both

#define TRANSFORM_CELL_VALUE CGAffineTransformMakeScale(0.8, 0.8)
#define ANIMATION_SPEED 0.2

The end result is

Sample Image

Centring a cell of a horizontally scrollable collection view when paging is enabled and making other cells partly visible, too

Actually, the solution isn't very hard. Thanks to this article that shows the right way to implement this feature, I finally implemented it.

The main thing to understand is that the paging, built in a collection view, won't work in this case, because the size of cells is less than the size of the screen. So, we need to implement our custom paging technology.

First, inside of a custom helper struct, we define a property that will hold the index of the cell before dragging :

var indexOfCellBeforeDragging: Int = 0

Then, we create a method for calculating the section inset of cells:

 //Setting the inset
func calculateSectionInset(forCollectionViewLayout collectionViewLayout: UICollectionViewFlowLayout, numberOfCells: Int) -> CGFloat {
let inset = (collectionViewLayout.collectionView!.frame.width) / CGFloat(numberOfCells)
return inset
}

Now we can use the method above, to set the size of items of the collection view.

  //Setting the item size of the collection view
func configureCollectionViewLayoutItemSize(forCollectionViewLayout collectionViewLayout: UICollectionViewFlowLayout) {

let inset: CGFloat = calculateSectionInset(forCollectionViewLayout: collectionViewLayout, numberOfCells: 5)

collectionViewLayout.sectionInset = UIEdgeInsets(top: 0, left: inset/4, bottom: 0, right: inset/4)

collectionViewLayout.itemSize = CGSize(width: collectionViewLayout.collectionView!.frame.size.width - inset / 2, height: collectionViewLayout.collectionView!.frame.size.height)
collectionViewLayout.collectionView?.reloadData()
}

We need to get the index of the major cell (the cell that is in the biggest right now). To do so, we use the information about items width and collection view's contentOffset on the x axis:

//Getting the index of the major cell
func indexOfMajorCell(in collectionViewLayout: UICollectionViewFlowLayout) -> Int {
let itemWidth = collectionViewLayout.itemSize.width
let proportionalLayout = collectionViewLayout.collectionView!.contentOffset.x / itemWidth
return Int(round(proportionalLayout))
}

Next, we set the index of cell before starting dragging, using the method above:

 //Setting the index of cell before starting dragging
mutating func setIndexOfCellBeforeStartingDragging(indexOfMajorCell: Int) {
indexOfCellBeforeDragging = indexOfMajorCell
}

Now we should handle the end of the dragging. Also, we take care of the possible snapping gesture on cells. Here we use some custom threshold for the swipe velocity.

  //Handling dragging end of a scroll view
func handleDraggingWillEndForScrollView(_ scrollView: UIScrollView, inside collectionViewLayout: UICollectionViewFlowLayout, withVelocity velocity: CGPoint, usingIndexOfMajorCell indexOfMajorCell: Int) {

//Calculating where scroll view should snap
let indexOfMajorCell = indexOfMajorCell

let swipeVelocityThreshold: CGFloat = 0.5

let majorCellIsTheCellBeforeDragging = indexOfMajorCell == indexOfCellBeforeDragging
let hasEnoughVelocityToSlideToTheNextCell = indexOfCellBeforeDragging + 1 < 5 && velocity.x > swipeVelocityThreshold
let hasEnoughVelocityToSlideToThePreviousCell = ((indexOfCellBeforeDragging - 1) >= 0) && (velocity.x < -swipeVelocityThreshold)

let didUseSwipeToSkipCell = majorCellIsTheCellBeforeDragging && (hasEnoughVelocityToSlideToTheNextCell || hasEnoughVelocityToSlideToThePreviousCell)

if didUseSwipeToSkipCell {

let snapToIndex = indexOfCellBeforeDragging + (hasEnoughVelocityToSlideToTheNextCell ? 1 : -1)
let toValue = collectionViewLayout.itemSize.width * CGFloat(snapToIndex)

// Damping equal 1 => no oscillations => decay animation
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: velocity.x, options: .allowUserInteraction, animations: {
scrollView.contentOffset = CGPoint(x: toValue, y: 0)
scrollView.layoutIfNeeded()
}, completion: nil)

} else {
let indexPath = IndexPath(row: indexOfMajorCell, section: 0)
collectionViewLayout.collectionView!.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
}
}

Now, in your view controller inside scrollViewWillBeginDragging(_:) method we need to get the index of the major cell and set it as an index before starting dragging (commentsPagingHelper is an instance of the helper struct):

  func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {

let indexOfMajorCell = commentsPagingHelper.indexOfMajorCell(in: collectionViewLayout)
commentsPagingHelper.setIndexOfCellBeforeStartingDragging(indexOfMajorCell: indexOfMajorCell)
}

Finally, we handle the change of an index of the major cell inside scrollViewWillEndDragging(_:, withVelocity:, targetContentOffset:) method:

 func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {

//Stop scrollView sliding:
targetContentOffset.pointee = scrollView.contentOffset

let indexOfMajorCell = commentsPagingHelper.indexOfMajorCell(in: collectionViewLayout)

commentsPagingHelper.handleDraggingWillEndForScrollView(scrollView, inside: collectionViewLayout, withVelocity: velocity, usingIndexOfMajorCell: indexOfMajorCell)
}

That's it. Hope, this will help someone with similar issue.

Paging in Collection View not centering properly

You have to make sure that your section inset + line spacing + cell width all equals exactly the bounds of the CollectionView. I use the following method on a custom UICollectionViewFlowLayout sublcass:

- (void)adjustSpacingForBounds:(CGRect)newBounds {
NSInteger count = newBounds.size.width / self.itemSize.width - 1;

CGFloat spacing = (newBounds.size.width - (self.itemSize.width * count)) / count;

self.minimumLineSpacing = spacing;

UIEdgeInsets insets = self.sectionInset;
insets.left = spacing/2.0f;
self.sectionInset = insets;
}

You have to remember that UICollectionView is just a sublcass of UIScrollView so it doesn't know how you want your pagination to work. Before UICollectionView you would have to handle all of this math when laying out your subviews of the UIScrollView.

Paging UICollectionView by cells, not screen

OK, so I found the solution here: targetContentOffsetForProposedContentOffset:withScrollingVelocity without subclassing UICollectionViewFlowLayout

I should have searched for targetContentOffsetForProposedContentOffset in the begining.



Related Topics



Leave a reply



Submit