How to create a centered UICollectionView like in Spotify's Player How to create a centered UICollectionView like in Spotify's Player ios ios

How to create a centered UICollectionView like in Spotify's Player


In order to create an horizontal carousel layout, you'll have to subclass UICollectionViewFlowLayout then override targetContentOffset(forProposedContentOffset:withScrollingVelocity:), layoutAttributesForElements(in:) and shouldInvalidateLayout(forBoundsChange:).

The following Swift 5 / iOS 12.2 complete code shows how to implement them.


CollectionViewController.swift

import UIKitclass CollectionViewController: UICollectionViewController {    let collectionDataSource = CollectionDataSource()    let flowLayout = ZoomAndSnapFlowLayout()    override func viewDidLoad() {        super.viewDidLoad()        title = "Zoomed & snapped cells"        guard let collectionView = collectionView else { fatalError() }        //collectionView.decelerationRate = .fast // uncomment if necessary        collectionView.dataSource = collectionDataSource        collectionView.collectionViewLayout = flowLayout        collectionView.contentInsetAdjustmentBehavior = .always        collectionView.register(CollectionViewCell.self, forCellWithReuseIdentifier: "Cell")    }}

ZoomAndSnapFlowLayout.swift

import UIKitclass ZoomAndSnapFlowLayout: UICollectionViewFlowLayout {    let activeDistance: CGFloat = 200    let zoomFactor: CGFloat = 0.3    override init() {        super.init()        scrollDirection = .horizontal        minimumLineSpacing = 40        itemSize = CGSize(width: 150, height: 150)    }    required init?(coder aDecoder: NSCoder) {        fatalError("init(coder:) has not been implemented")    }    override func prepare() {        guard let collectionView = collectionView else { fatalError() }        let verticalInsets = (collectionView.frame.height - collectionView.adjustedContentInset.top - collectionView.adjustedContentInset.bottom - itemSize.height) / 2        let horizontalInsets = (collectionView.frame.width - collectionView.adjustedContentInset.right - collectionView.adjustedContentInset.left - itemSize.width) / 2        sectionInset = UIEdgeInsets(top: verticalInsets, left: horizontalInsets, bottom: verticalInsets, right: horizontalInsets)        super.prepare()    }    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {        guard let collectionView = collectionView else { return nil }        let rectAttributes = super.layoutAttributesForElements(in: rect)!.map { $0.copy() as! UICollectionViewLayoutAttributes }        let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.frame.size)        // Make the cells be zoomed when they reach the center of the screen        for attributes in rectAttributes where attributes.frame.intersects(visibleRect) {            let distance = visibleRect.midX - attributes.center.x            let normalizedDistance = distance / activeDistance            if distance.magnitude < activeDistance {                let zoom = 1 + zoomFactor * (1 - normalizedDistance.magnitude)                attributes.transform3D = CATransform3DMakeScale(zoom, zoom, 1)                attributes.zIndex = Int(zoom.rounded())            }        }        return rectAttributes    }    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {        guard let collectionView = collectionView else { return .zero }        // Add some snapping behaviour so that the zoomed cell is always centered        let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView.frame.width, height: collectionView.frame.height)        guard let rectAttributes = super.layoutAttributesForElements(in: targetRect) else { return .zero }        var offsetAdjustment = CGFloat.greatestFiniteMagnitude        let horizontalCenter = proposedContentOffset.x + collectionView.frame.width / 2        for layoutAttributes in rectAttributes {            let itemHorizontalCenter = layoutAttributes.center.x            if (itemHorizontalCenter - horizontalCenter).magnitude < offsetAdjustment.magnitude {                offsetAdjustment = itemHorizontalCenter - horizontalCenter            }        }        return CGPoint(x: proposedContentOffset.x + offsetAdjustment, y: proposedContentOffset.y)    }    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {        // Invalidate layout so that every cell get a chance to be zoomed when it reaches the center of the screen        return true    }    override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {        let context = super.invalidationContext(forBoundsChange: newBounds) as! UICollectionViewFlowLayoutInvalidationContext        context.invalidateFlowLayoutDelegateMetrics = newBounds.size != collectionView?.bounds.size        return context    }}

CollectionDataSource.swift

import UIKitclass CollectionDataSource: NSObject, UICollectionViewDataSource {    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {        return 9    }    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell        return cell    }}

CollectionViewCell.swift

import UIKitclass CollectionViewCell: UICollectionViewCell {    override init(frame: CGRect) {        super.init(frame: frame)        contentView.backgroundColor = .green    }    required init?(coder aDecoder: NSCoder) {        fatalError("init(coder:) has not been implemented")    }}

Expected result:

enter image description here


Source:


Well, I made UICollectionview moving just like this, yesterday.

I can share my code with you :)

Here's my storyboard

make sure uncheck 'Paging Enabled'

Here's my code.

@interface FavoriteViewController () <UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>{    NSMutableArray * mList;    CGSize cellSize;}@property (weak, nonatomic) IBOutlet UICollectionView *cv;@end@implementation FavoriteViewController- (void) viewWillAppear:(BOOL)animated{    [super viewWillAppear:animated];    // to get a size.    [self.view setNeedsLayout];    [self.view layoutIfNeeded];    CGRect screenFrame = [[UIScreen mainScreen] bounds];    CGFloat width = screenFrame.size.width*self.cv.frame.size.height/screenFrame.size.height;    cellSize = CGSizeMake(width, self.cv.frame.size.height);    // if cell's height is exactly same with collection view's height, you get an warning message.    cellSize.height -= 1;    [self.cv reloadData];    // setAlpha is for hiding looking-weird at first load    [self.cv setAlpha:0];}- (void) viewDidAppear:(BOOL)animated{    [super viewDidAppear:animated];    [self scrollViewDidScroll:self.cv];    [self.cv setAlpha:1];}#pragma mark - scrollview delegate- (void) scrollViewDidScroll:(UIScrollView *)scrollView{    if(mList.count > 0)    {        const CGFloat centerX = self.cv.center.x;        for(UICollectionViewCell * cell in [self.cv visibleCells])        {            CGPoint pos = [cell convertPoint:CGPointZero toView:self.view];            pos.x += cellSize.width/2.0f;            CGFloat distance = fabs(centerX - pos.x);// If you want to make side-cell's scale bigger or smaller,// change the value of '0.1f'            CGFloat scale = 1.0f - (distance/centerX)*0.1f;            [cell setTransform:CGAffineTransformMakeScale(scale, scale)];        }    }}- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{ // for custom paging    CGFloat movingX = velocity.x * scrollView.frame.size.width;    CGFloat newOffsetX = scrollView.contentOffset.x + movingX;    if(newOffsetX < 0)    {        newOffsetX = 0;    }    else if(newOffsetX > cellSize.width * (mList.count-1))    {        newOffsetX = cellSize.width * (mList.count-1);    }    else    {        NSUInteger newPage = newOffsetX/cellSize.width + ((int)newOffsetX%(int)cellSize.width > cellSize.width/2.0f ? 1 : 0);        newOffsetX = newPage*cellSize.width;    }    targetContentOffset->x = newOffsetX;}#pragma mark - collectionview delegate- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{    return mList.count;}- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{    UICollectionViewCell * cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"list" forIndexPath:indexPath];    NSDictionary * dic = mList[indexPath.row];    UIImageView * iv = (UIImageView *)[cell.contentView viewWithTag:1];    UIImage * img = [UIImage imageWithData:[dic objectForKey:kKeyImg]];    [iv setImage:img];    return cell;}- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath{    return cellSize;}- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section{    CGFloat gap = (self.cv.frame.size.width - cellSize.width)/2.0f;    return UIEdgeInsetsMake(0, gap, 0, gap);}- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section{    return 0;}- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section{    return 0;}

Key code of make cell centered is

  1. scrollViewWillEndDragging

  2. insetForSectionAtIndex

Key code of animate the size is

  1. scrollviewDidScroll

I wish this helps you

P.S. If you want to change alpha just like the image that you uploaded, add [cell setalpha] in scrollViewDidScroll


As you have said in the comment you want that in the Objective-c code, there is a very famous library called iCarousel which can be helpful in completing your requirement.Link: https://github.com/nicklockwood/iCarousel

You may use 'Rotary' or 'Linear' or some other style with little or no modification to implement the custom view

To implement it you have implement only some delegate methods of it and it's working for ex:

//specify the type you want to use in viewDidLoad_carousel.type = iCarouselTypeRotary;//Set the following delegate methods- (NSInteger)numberOfItemsInCarousel:(iCarousel *)carousel{    //return the total number of items in the carousel    return [_items count];}- (UIView *)carousel:(iCarousel *)carousel viewForItemAtIndex:(NSInteger)index reusingView:(UIView *)view{    UILabel *label = nil;    //create new view if no view is available for recycling    if (view == nil)    {        //don't do anything specific to the index within        //this `if (view == nil) {...}` statement because the view will be        //recycled and used with other index values later        view = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 200.0f, 200.0f)];        ((UIImageView *)view).image = [UIImage imageNamed:@"page.png"];        view.contentMode = UIViewContentModeCenter;        label = [[UILabel alloc] initWithFrame:view.bounds];        label.backgroundColor = [UIColor clearColor];        label.textAlignment = NSTextAlignmentCenter;        label.font = [label.font fontWithSize:50];        label.tag = 1;        [view addSubview:label];    }    else    {        //get a reference to the label in the recycled view        label = (UILabel *)[view viewWithTag:1];    }    //set item label    label.text = [_items[index] stringValue];    return view;}- (CGFloat)carousel:(iCarousel *)carousel valueForOption:(iCarouselOption)option withDefault:(CGFloat)value{    if (option == iCarouselOptionSpacing)    {        return value * 1.1;    }    return value;}

You can check the full working demo from 'Examples/Basic iOS Example' which is included with the Github repository link

As it is old and popular you can find some related tutorials for it and it will also be much stable than the custom code implementation