Uicollectionview Horizontal Paging - How to Use Flow Layout

UICollectionView horizontal paging - can I use Flow Layout?

You're right – that's not how a stock horizontally-scrolling collection view lays out cells. I'm afraid that you're going to have to implement your own custom UICollectionViewLayout subclass. Either that, or separate your models into sections.

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

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 scroll, horizontal layout?

1st approach

What about using UIPageViewController with an array of UICollectionViewControllers? You'd have to fetch proper number of items in each UICollectionViewController, but it shouldn't be hard. You'd get exactly the same look as the Springboard has.

2nd approach

I've thought about this and in my opinion you have to set:

self.collectionView.pagingEnabled = YES;

and create your own collection view layout by subclassing UICollectionViewLayout. From the custom layout object you can access self.collectionView, so you'll know what is the size of the collection view's frame, numberOfSections and numberOfItemsInSection:. With that information you can calculate cells' frames (in prepareLayout) and collectionViewContentSize. Here're some articles about creating custom layouts:

  • https://developer.apple.com/library/content/documentation/WindowsViews/Conceptual/CollectionViewPGforIOS/CreatingCustomLayouts/CreatingCustomLayouts.html
  • http://www.objc.io/issue-3/collection-view-layouts.html

3rd approach

You can do this (or an approximation of it) without creating the custom layout. Add UIScrollView in the blank view, set paging enabled in it. In the scroll view add the a collection view. Then add to it a width constraint, check in code how many items you have and set its constant to the correct value, e.g. (self.view.frame.size.width * numOfScreens). Here's how it looks (numbers on cells show the indexPath.row): https://www.dropbox.com/s/ss4jdbvr511azxz/collection_view.mov If you're not satisfied with the way cells are ordered, then I'm afraid you'd have to go with 1. or 2.

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.



Related Topics



Leave a reply



Submit