ios 如何在 Spotify 的播放器中创建一个居中的 UICollectionView

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/35045155/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-31 08:22:10  来源:igfitidea点击:

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

iosobjective-cuiscrollviewuicollectionviewuicollectionviewlayout

提问by evenodd

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

我在尝试创建一个 UICollectionView 时遇到了很多困难,就像在 Spotify 的播放器中一样,它的行为是这样的:

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.

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. 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.

2) 使用 pagingEnabled = YES,collectionview 正确地从一个页面滑动到另一个页面。但是,如果单元格没有居中,它只是将集合视图移动到屏幕宽度的页面上。所以问题是如何让页面移动,从而获得上述效果。

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. 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!

我目前拥有的代码是一个简单的 UICollectionView,带有普通的委托设置和正方形的自定义 UICollectionview 单元格。也许我需要继承 UICollectionViewFlowLayout?或者我可能需要将 pagingEnabled 设置为 NO 然后使用自定义滑动事件?会喜欢任何帮助!

采纳答案by HardikDG

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

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

回答by Imanou Petit

In order to create an horizontal carousel layout, you'll have to subclass UICollectionViewFlowLayoutthen 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 { 
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
    }

}
.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 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")
    }

}

CollectionViewCell.swift

CollectionViewCell.swift

@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;
}


Expected result:

预期结果:

enter image description here

在此处输入图片说明



Source:

来源:

回答by negaipro

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.

这是我的代码。

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

Key code of make cell centered is

使单元格居中的关键代码是

  1. scrollViewWillEndDragging

  2. insetForSectionAtIndex

  1. scrollViewWillEndDragging

  2. insetForSectionAtIndex

Key code of animate the size is

动画尺寸的关键代码是

  1. scrollviewDidScroll
  1. 滚动视图DidScroll

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

PS如果你想像你上传的图片一样改变alpha,在scrollViewDidScroll中添加[cell setalpha]

回答by trdavidson

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.

不久前我想要类似的行为,在@Mike_M 的帮助下,我能够弄清楚。尽管有很多很多方法可以做到这一点,但这个特定的实现是创建一个自定义的 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. So another approach would be to to set a CGAffineTransformMakeScale( , )in the UIScrollViewDidScrollwhere you dynamically update the cell's size based on it's distance from the center of the screen.

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

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

您可以使用[*youCollectionView indexPathsForVisibleItems]然后获取这些 indexPaths 的单元格来获取collectionView 的可见单元格的indexPaths。对于每个单元格,计算其中心到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 想法:

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

}

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

发生的情况是单元格的大小将完全跟随您的触摸。如果您有任何其他问题,请告诉我,因为我正在写下大部分内容)。

##代码##

回答by Dallas Johnson

pagingEnabledshould 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 herefrom one of my late answers to another question.

pagingEnabled不应启用,因为它需要每个单元格都是您视图的宽度,这对您不起作用,因为您需要查看其他单元格的边缘。为了您的点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 和覆盖 Inside 来实现,layoutAttributesForItemAtIndexPath:即修改首先调用 super 提供的布局属性,然后根据与窗口中心相关的位置修改布局属性大小。

Hopefully this helps.

希望这会有所帮助。