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

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

我很难在Spotify的播放器中创建这样的uiCollectionView:

a busy cat

我的问题是双重的。

1)如何将单元格居中,以便您可以看到中间单元格以及左右单元格之一。

  • 如果创建正方形的单元格并在每个单元格之间添加间距,则单元格将正确显示,但不会居中。

2)pagingabled=yes时,CollectionView会正确地从一个页面滑动到另一个页面。但是,如果单元格不居中,则只需将集合视图移到屏幕宽度的页面上。所以问题是你如何使页面移动,从而得到上面的效果。

3)当单元格移动时,如何设置其大小的动画

  • 我不想为此担心太多。如果我能做到这一点,那就太好了,但更困难的问题是1和2。

我目前拥有的代码是一个简单的uicollectionview,具有正常的委托设置和自定义的uicollectionview单元格,这些单元格是正方形的。也许我需要子类uicollectionviewflowlayout?或者我需要将paginnabled设置为no,然后使用自定义滑动事件?希望有任何帮助!


为了创建水平传送带布局,您必须将UICollectionViewFlowLayout子类化,然后覆盖targetContentOffset(forProposedContentOffset:withScrollingVelocity:)layoutAttributesForElements(in:)shouldInvalidateLayout(forBoundsChange:)

下面的swift 4.1-iOS 11完整代码展示了如何实现它们。

集合视图控制器.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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")
    }

}

缩放和快照流布局.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
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
    }

}

集合数据源.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
    }

}

集合视图单元格.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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")
    }

}

预期结果:

enter image description here

来源:

  • WWDC 2012会话219-"高级集合视图和构建自定义布局"


昨天,我让uiCollectionView像这样移动。

我可以与您共享我的代码:)

这是我的故事板

></P><P>确保取消选中

></P><P>这是我的密码。</P></p>
<div class=

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
@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;
}

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

  • 滚动视图将结束拖动

  • 插入分数索引

  • 动画的关键代码大小是

  • 滚动查看滚动
  • 我希望这能帮助你

    附笔。如果要像上载的图像一样更改alpha,请在ScrollViewDidScroll中添加[cell setalpha]


    不久前,我就想有类似的行为,在@mike_m的帮助下,我终于明白了。尽管有很多种方法可以做到这一点,但这个特定的实现是创建一个自定义的uiCollectionViewLayout。

    下面的代码(可以在这里找到gist:https://gist.github.com/mmick66/9812223)

    现在重要的是要设置以下内容:*yourCollectionView*.decelerationRate = UIScrollViewDecelerationRateFast,这可以防止快速滑动跳过单元格。

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

    您可以使用[*youCollectionView indexPathsForVisibleItems]获取CollectionView可见单元格的索引路径,然后获取这些索引路径的单元格。对于每个单元格,计算其中心到集合视图中心的距离

    集合视图的中心可以用这个漂亮的方法找到:CGPoint point = [self.view convertPoint:*yourCollectionView*.center toView:*yourCollectionView];

    现在设置一个规则,如果单元格的中心距X远,则单元格的大小为"正常大小",称为1。它越靠近中心,就越接近正常尺寸2的两倍。

    然后您可以使用以下if/else思想:

    1
    2
    3
    4
    5
    6
    7
     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);
     }

    会发生的是,手机的大小会完全按照你的触摸。如果你还有什么问题的话,请告诉我,因为我已经把大部分问题都写出来了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    - (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);

    }

    正如您在评论中所说,您希望在目标C代码中,有一个非常著名的库,叫做i卡洛塞尔,它可以帮助您完成您的要求。链接:https://github.com/nicklockwood/i卡洛塞尔

    您可以使用"旋转"或"线性"或其他一些几乎没有修改或没有修改的样式来实现自定义视图。

    要实现它,您只实现了它的一些委托方法,并且它适用于ex:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    //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;
    }

    您可以从Github存储库链接中包含的"示例/基本iOS示例"中查看完整的工作演示。

    由于它是老的和流行的,您可以找到一些相关的教程,它也将比自定义代码实现稳定得多。


    不应启用pagingEnabled,因为它需要每个单元格都是视图的宽度,这对您不起作用,因为您需要查看其他单元格的边缘。对于你的第1和第2点。我想你会从我迟回答的另一个问题中找到你需要的。

    单元大小的动画可以通过子类化uicollectionviewflowlayout和重写layoutAttributesForItemAtIndexPath:来实现,其中修改了由首先调用super提供的布局属性,然后根据与窗口中心相关的位置修改布局属性大小。

    希望这有帮助。