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:
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
scrollViewWillEndDragging
insetForSectionAtIndex
Key code of animate the size is
- 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