How to create a centered UICollectionView like in

2019-01-13 01:05发布

I am have a lot of difficulty trying to create a UICollectionView like in Spotify's Player that acts like this:

a busy cat

The problem for me is two fold.

1) How do I center the cells so that you can see the middle cell as well as the one of the left and right.

  • If I create cells that are square and add spacing between each cell, the cells are correctly displayed but are not centered.

2) With pagingEnabled = YES, the collectionview correctly swipes from one page to another. However, without the cells being centered, it simply moves the collection view over a page which is the width of the screen. So the question is how do you make the pages move so you get the effect above.

3) how do you animate the size of the cells as they move

  • I don't want to worry about this too much. If I can get that to work it would be great, but the harder problems are 1 and 2.

The code I have currently is a simple UICollectionView with normal delegate setup and custom UICollectionview cells that are squares. Maybe I neeed to subclass UICollectionViewFlowLayout? Or maybe I need to turn pagingEnabled to NO and then use custom swipe events? Would love any help!

5条回答
兄弟一词,经得起流年.
2楼-- · 2019-01-13 01:19

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 4.1 - iOS 11 complete code shows how to implement them.


CollectionViewController.swift

import UIKit

class 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.register(CollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
    }

}

ZoomAndSnapFlowLayout.swift

import UIKit

class 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 UIKit

class 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 UIKit

class 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:

查看更多
Fickle 薄情
3楼-- · 2019-01-13 01:21

pagingEnabled should not be enabled as it needs each cell to be the width of you view which will not work for you since you need to see the edges of other cells. For your points 1 and 2. I think you'll find what you need here from one of my late answers to another question.

The animation of the cell sizes can be achieved by subclassing UIcollectionviewFlowLayout and overriding layoutAttributesForItemAtIndexPath: Within that modify the layout attributes provided by first calling super and then modify the layout attributes size based on the position as it relates to the window centre.

Hopefully this helps.

查看更多
家丑人穷心不美
4楼-- · 2019-01-13 01:23

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

查看更多
闹够了就滚
5楼-- · 2019-01-13 01:24

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

查看更多
放荡不羁爱自由
6楼-- · 2019-01-13 01:26

I wanted similar behavior a little while back, and with the help of @Mike_M I was able to figure it out. Although there are many, many way to do this, this particular implementation is to create a custom UICollectionViewLayout.

Code below(gist can be found here: https://gist.github.com/mmick66/9812223)

Now it's important to set the following: *yourCollectionView*.decelerationRate = UIScrollViewDecelerationRateFast, this prevents cells being skipped by a quick swipe.

That should cover part 1 and 2. Now, for part 3 you could incorporate that in the custom collectionView by constantly invalidating and updating, but it's a bit of a hassle if you ask me. So another approach would be to to set a CGAffineTransformMakeScale( , ) in the UIScrollViewDidScroll where you dynamically update the cell's size based on it's distance from the center of the screen.

You can get the indexPaths of the visible cells of the collectionView using [*youCollectionView indexPathsForVisibleItems] and then getting the cells for these indexPaths. For every cell, calculate the distance of its center to the center of yourCollectionView

The center of the collectionView can be found using this nifty method: CGPoint point = [self.view convertPoint:*yourCollectionView*.center toView:*yourCollectionView];

Now set up a rule, that if the cell's center is further than x away, the size of the cell is for example the 'normal size', call it 1. and the closer it gets to the center, the closer it gets to twice the normal size 2.

then you can use the following if/else idea:

 if (distance > x) {
        cell.transform = CGAffineTransformMakeScale(1.0f, 1.0f);
 } else if (distance <= x) {

        float scale = MIN(distance/x) * 2.0f;
        cell.transform = CGAffineTransformMakeScale(scale, scale);
 }

What happens is that the cell's size will exactly follow your touch. Let me know if you have any more questions as I'm writing most of this out of the top of my head).

- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)offset 
                             withScrollingVelocity:(CGPoint)velocity {

CGRect cvBounds = self.collectionView.bounds;
CGFloat halfWidth = cvBounds.size.width * 0.5f;
CGFloat proposedContentOffsetCenterX = offset.x + halfWidth;

NSArray* attributesArray = [self layoutAttributesForElementsInRect:cvBounds];

UICollectionViewLayoutAttributes* candidateAttributes;
for (UICollectionViewLayoutAttributes* attributes in attributesArray) {

    // == Skip comparison with non-cell items (headers and footers) == //
    if (attributes.representedElementCategory != 
        UICollectionElementCategoryCell) {
        continue;
    }

    // == First time in the loop == //
    if(!candidateAttributes) {
        candidateAttributes = attributes;
        continue;
    }

    if (fabsf(attributes.center.x - proposedContentOffsetCenterX) < 
        fabsf(candidateAttributes.center.x - proposedContentOffsetCenterX)) {
        candidateAttributes = attributes;
    }
}

return CGPointMake(candidateAttributes.center.x - halfWidth, offset.y);

}
查看更多
登录 后发表回答