简体   繁体   English

如何像 Spotify 的播放器一样创建一个居中的 UICollectionView

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

I am have a lot of difficulty trying to create a UICollectionView like in Spotify's Player that acts like this:我在尝试创建像 Spotify 播放器这样的 UICollectionView 时遇到了很多困难:

一只忙碌的猫

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. 1)如何将单元格居中,以便您可以看到中间的单元格以及左侧和右侧的单元格。

  • 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. 2) 使用 pagingEnabled = YES,collectionview 正确地从一页滑动到另一页。 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 3)你如何在移动时为单元格的大小设置动画

  • 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.如果我能让它工作,那就太好了,但更难的问题是 1 和 2。

The code I have currently is a simple UICollectionView with normal delegate setup and custom UICollectionview cells that are squares.我目前拥有的代码是一个简单的 UICollectionView,具有正常的委托设置和自定义 UICollectionview 正方形单元格。 Maybe I neeed to subclass UICollectionViewFlowLayout?也许我需要继承 UICollectionViewFlowLayout? Or maybe I need to turn pagingEnabled to NO and then use custom swipe events?或者我可能需要将 pagingEnabled 设置为 NO 然后使用自定义滑动事件? Would love any help!会喜欢任何帮助!

In order to create an horizontal carousel layout, you'll have to subclass UICollectionViewFlowLayout then override targetContentOffset(forProposedContentOffset:withScrollingVelocity:) , layoutAttributesForElements(in:) and shouldInvalidateLayout(forBoundsChange:) .为了创建水平轮播布局,您必须UICollectionViewFlowLayout然后覆盖targetContentOffset(forProposedContentOffset:withScrollingVelocity:)layoutAttributesForElements(in:)shouldInvalidateLayout(forBoundsChange:)

The following Swift 5 / iOS 12.2 complete code shows how to implement them.以下 Swift 5 / iOS 12.2 完整代码展示了如何实现它们。


CollectionViewController.swift 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.contentInsetAdjustmentBehavior = .always
        collectionView.register(CollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
    }

}

ZoomAndSnapFlowLayout.swift 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 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 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:预期结果:

在此处输入图片说明


Source:来源:

Well, I made UICollectionview moving just like this, yesterday.好吧,昨天我让 UICollectionview 像这样移动。

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 scrollViewWillEndDragging

  2. insetForSectionAtIndex insetForSectionAtIndex

Key code of animate the size is动画尺寸的关键代码是

  1. scrollviewDidScroll滚动视图DidScroll

I wish this helps you我希望这对你有帮助

PS If you want to change alpha just like the image that you uploaded, add [cell setalpha] in scrollViewDidScroll PS如果你想像你上传的图片一样改变alpha,在scrollViewDidScroll中添加[cell setalpha]

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正如您在评论中所说,您希望在 Objective-c 代码中,有一个非常有名的名为 iCarousel 的库,它可以帮助完成您的要求。链接: 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您可以使用 'Rotary' 或 'Linear' 或一些其他样式,只需很少或无需修改即可实现自定义视图

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您可以从 Github 存储库链接中包含的“ 示例/基本 iOS 示例”中查看完整的工作演示

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由于它很老而且很流行,你可以找到一些相关的教程,它也会比自定义代码实现稳定得多

I wanted similar behavior a little while back, and with the help of @Mike_M I was able to figure it out.不久前我想要类似的行为,在@Mike_M 的帮助下,我能够弄清楚。 Although there are many, many way to do this, this particular implementation is to create a custom UICollectionViewLayout.尽管有很多很多方法可以做到这一点,但这个特定的实现是创建一个自定义的 UICollectionViewLayout。

Code below(gist can be found here: https://gist.github.com/mmick66/9812223 )下面的代码(要点可以在这里找到: 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.现在设置以下内容很重要: *yourCollectionView*.decelerationRate = UIScrollViewDecelerationRateFast ,这可以防止快速滑动跳过单元格。

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.这应该涵盖第 1 部分和第 2 部分。现在,对于第 3 部分,您可以通过不断地使无效和更新将其合并到自定义 collectionView 中,但是如果您问我,这有点麻烦。 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.因此,另一种方法是在UIScrollViewDidScroll中设置CGAffineTransformMakeScale( , ) ,您可以根据它与屏幕中心的距离动态更新单元格的大小。

You can get the indexPaths of the visible cells of the collectionView using [*youCollectionView indexPathsForVisibleItems] and then getting the cells for these indexPaths.您可以使用[*youCollectionView indexPathsForVisibleItems]获取 collectionView 可见单元格的[*youCollectionView indexPathsForVisibleItems] ,然后获取这些 indexPaths 的单元格。 For every cell, calculate the distance of its center to the center of yourCollectionView对于每个单元格,计算其中心到yourCollectionView中心的距离

The center of the collectionView can be found using this nifty method: CGPoint point = [self.view convertPoint:*yourCollectionView*.center toView:*yourCollectionView];可以使用这个漂亮的方法找到 collectionView 的中心: 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.现在设置一个规则,如果单元格的中心距离 x 远,则单元格的大小是例如“正常大小”,称其为 1。越靠近中心,它越接近两倍正常尺寸 2。

then you can use the following if/else idea:那么您可以使用以下 if/else 想法:

 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);

}

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. pagingEnabled不应启用,因为它需要每个单元格都是您视图的宽度,这对您不起作用,因为您需要查看其他单元格的边缘。 For your points 1 and 2. I think you'll find what you need here from one of my late answers to another question.为了您的点1和2,我想你会发现你所需要的在这里从我已故的回答另一个问题之一。

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.单元格大小的动画可以通过继承 UIcollectionviewFlowLayout 和覆盖layoutAttributesForItemAtIndexPath:来实现layoutAttributesForItemAtIndexPath:在其中修改首先调用 super 提供的布局属性,然后根据与窗口中心相关的位置修改布局属性大小。

Hopefully this helps.希望这会有所帮助。

If you want to have uniform spacing between cells you can replace the following method in ZoomAndSnapFlowLayout from Imanou Petit 's solution:如果你想在单元格之间有统一的间距,你可以从Imanou Petit的解决方案中替换ZoomAndSnapFlowLayout的以下方法:

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)
    let visibleAttributes = rectAttributes.filter { $0.frame.intersects(visibleRect) }

    // Keep the spacing between cells the same.
    // Each cell shifts the next cell by half of it's enlarged size.
    // Calculated separately for each direction.
    func adjustXPosition(_ toProcess: [UICollectionViewLayoutAttributes], direction: CGFloat, zoom: Bool = false) {
        var dx: CGFloat = 0

        for attributes in toProcess {
            let distance = visibleRect.midX - attributes.center.x
            attributes.frame.origin.x += dx

            if distance.magnitude < activeDistance {
                let normalizedDistance = distance / activeDistance
                let zoomAddition = zoomFactor * (1 - normalizedDistance.magnitude)
                let widthAddition = attributes.frame.width * zoomAddition / 2
                dx = dx + widthAddition * direction

                if zoom {
                    let scale = 1 + zoomAddition
                    attributes.transform3D = CATransform3DMakeScale(scale, scale, 1)
                }
            }
        }
    }

    // Adjust the x position first from left to right.
    // Then adjust the x position from right to left.
    // Lastly zoom the cells when they reach the center of the screen (zoom: true).
    adjustXPosition(visibleAttributes, direction: +1)
    adjustXPosition(visibleAttributes.reversed(), direction: -1, zoom: true)

    return rectAttributes
}

You can create a custom UICollectionFlowLayout您可以创建自定义 UICollectionFlowLayout

For more info check this https://medium.com/@sh.soheytizadeh/zoom-uicollectionview-centered-cell-swift-5-e63cad9bcd49有关更多信息,请查看此https://medium.com/@sh.soheytizadeh/zoom-uicollectionview-central-cell-swift-5-e63cad9bcd49

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM