【问题标题】:How to create a centered UICollectionView like in Spotify's Player如何像在 Spotify 的播放器中一样创建居中的 UICollectionView
【发布时间】:2016-05-04 20:44:58
【问题描述】:

尝试在 Spotify 的播放器中创建 UICollectionView 时遇到很多困难:

对我来说有两个问题。

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

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

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

3) 如何在单元格移动时为其大小设置动画

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

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

【问题讨论】:

  • 这是一个在 Github 上找到的例子:github.com/ikemai/ScaledVisibleCellsCollectionView 也许它可以帮助你:)
  • @Lapinou 我在我的 Objective-C 项目中使用它时遇到了麻烦。似乎无法让桥接工作
  • 嗯...通常,这很容易。看看这篇文章:stackoverflow.com/questions/24206732/…让我知道它是否适合你;)
  • @evenodd 在下面的 Objective-C 中写出了详细的答案。对于第 1+2 部分,您可以通过创建 UICollectionViewFlowLayout 子类来简单地复制粘贴代码。第 3 部分需要更多的工作,但非常可行 - 如果这对您有用,请告诉我)

标签: ios objective-c uiscrollview uicollectionview uicollectionviewlayout


【解决方案1】:

您可以创建自定义 UICollectionFlowLayout

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

【讨论】:

    【解决方案2】:

    如果您想在单元格之间有统一的间距,您可以在ZoomAndSnapFlowLayout 的解决方案中替换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
    }
    

    【讨论】:

      【解决方案3】:

      为了创建水平轮播布局,您必须继承UICollectionViewFlowLayout,然后覆盖targetContentOffset(forProposedContentOffset:withScrollingVelocity:)layoutAttributesForElements(in:)shouldInvalidateLayout(forBoundsChange:)

      以下 Swift 5 / iOS 12.2 完整代码展示了如何实现它们。


      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

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

      预期结果:


      来源:

      【讨论】:

      • 如何添加单项滚动
      • 不,我只想一次滑动一个单元格
      • 这是正确的做法,而不是scrollViewDidScroll。感谢分享。
      • pagingEnabled 仅适用于全滚动视图宽度的项目。在显示上一个和下一个项目的边缘时,您必须使用自定义实现。
      • 一流的伙伴,不敢相信它“刚刚工作” - 浏览代码并确保我现在理解它:)
      【解决方案4】:

      正如您在评论中所说,您希望在 Objective-c 代码中,有一个名为 iCarousel 的非常著名的库,它可以帮助您完成您的要求。链接:https://github.com/nicklockwood/iCarousel

      您可以使用 'Rotary' 或 'Linear' 或其他一些几乎没有修改的样式来实现自定义视图

      要实现它,您只需要实现它的一些委托方法,并且它正在为 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;
      }
      

      您可以从 Github 存储库链接中包含的“Examples/Basic iOS Example”查看完整的工作演示

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

      【讨论】:

        【解决方案5】:

        不久前我想要类似的行为,在@Mike_M 的帮助下,我能够弄明白。虽然有很多很多方法可以做到这一点,但这个特定的实现是创建一个自定义 UICollectionViewLayout。

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

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

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

        您可以使用[*youCollectionView indexPathsForVisibleItems] 获取collectionView 的可见单元格的indexPaths,然后获取这些indexPaths 的单元格。对于每个单元格,计算其中心到 yourCollectionView

        中心的距离

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

        现在设置一个规则,如果单元格的中心距离 x 远,则单元格的大小例如是“正常大小”,称之为 1。越靠近中心,它越接近达到正常尺寸 2 的两倍。

        那么你可以使用下面的 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);
         }
        

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

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

        【讨论】:

          【解决方案6】:

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

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

          希望这会有所帮助。

          【讨论】:

            【解决方案7】:

            嗯,昨天我让 UICollectionview 像这样移动。

            我可以和你分享我的代码:)

            这是我的故事板

            确保取消选中“启用分页”

            这是我的代码。

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

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

            1. scrollViewWillEndDragging

            2. insetForSectionAtIndex

            动画大小的关键代码是

            1. scrollviewDidScroll

            希望对你有帮助

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

            【讨论】:

            • 你应该在 viewDidLayoutSubviews 中设置 cellSize 而不是 viewDidAppear。
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-10-13
            • 2021-12-07
            • 1970-01-01
            • 2020-09-03
            • 2017-03-15
            • 1970-01-01
            相关资源
            最近更新 更多