【问题标题】:UICollectionView Cell OverlapUICollectionView 单元格重叠
【发布时间】:2013-04-10 16:21:04
【问题描述】:

我有一个水平的 UICollectionView。我想让单元格相互重叠一定数量的 60 像素,以便第二个单元格与第一个单元格重叠 60 个像素,第三个单元格与第二个单元格重叠相同的数量,依此类推。

我尝试对 UICollectionViewFlowLayout 进行子类化。但我无法将 UICollectionviewCell 放在另一个之上。

【问题讨论】:

    标签: iphone ios uicollectionview uicollectionviewcell uicollectionviewlayout


    【解决方案1】:

    从本教程开始:https://web-beta.archive.org/web/20151116053251/http://blog.tobiaswiedenmann.com/post/35135290759/stacklayout

    我专门为水平 UICollectionView 修改了 stacklayout

    UICollectionViewStackLayout.h

    #import <UIKit/UIKit.h>
    
    @interface UICollectionViewStackLayout : UICollectionViewFlowLayout
    
    #define STACK_OVERLAP 60 //this corresponds to 60 points which is probably what you want, not pixels
    #define ITEM_SIZE CGSizeMake(190,210)
    
    @end
    

    UICollectionViewStackLayout.m

    #import "UICollectionViewStackLayout.h"
    
    @implementation UICollectionViewStackLayout
    
    -(id)init{
    
        self = [super init];
    
        if (self) {
            [self commonInit];
        }
    
        return self;
    }
    
    -(id)initWithCoder:(NSCoder *)aDecoder
    {
        if (self = [super init]) {
            [self commonInit];
        }
    
        return self;
    }
    
    -(void)commonInit
    {
        self.itemSize = ITEM_SIZE;
    
        //set minimum layout requirements
        self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
        self.minimumInteritemSpacing = 0;
    }
    
    -(CGSize)collectionViewContentSize
    {
        NSInteger xSize = [self.collectionView numberOfItemsInSection:0]
        * self.itemSize.width;
        NSInteger ySize = [self.collectionView numberOfSections]
        * (self.itemSize.height);
    
        CGSize contentSize = CGSizeMake(xSize, ySize);
    
        if (self.collectionView.bounds.size.width > contentSize.width)
            contentSize.width = self.collectionView.bounds.size.width;
    
        if (self.collectionView.bounds.size.height > contentSize.height)
            contentSize.height = self.collectionView.bounds.size.height;
    
        return contentSize;
    }
    
    -(NSArray*)layoutAttributesForElementsInRect:(CGRect)rect
    {
        NSArray* attributesArray = [super layoutAttributesForElementsInRect:rect];
        int numberOfItems = [self.collectionView numberOfItemsInSection:0];
    
        for (UICollectionViewLayoutAttributes *attributes in attributesArray) {
            CGFloat xPosition = attributes.center.x;
            CGFloat yPosition = attributes.center.y;
    
            if (attributes.indexPath.row == 0) {
                attributes.zIndex = INT_MAX; // Put the first cell on top of the stack
            } else {
                xPosition -= STACK_OVERLAP * attributes.indexPath.row;
                attributes.zIndex = numberOfItems - attributes.indexPath.row; //Other cells below the first one
            }
    
            attributes.center = CGPointMake(xPosition, yPosition);
        }
    
        return attributesArray;
    }
    
    - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)path {
        UICollectionViewLayoutAttributes* attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:path];
        return attributes;
    }
    
    @end
    

    【讨论】:

    • 我在 Apple tv 应用中实现了您的答案。但我面临的问题是第一个单元格是完美的,但是当我移动焦点下一个单元格时其他所有单元格重叠,如果我移动焦点结果应该与第一个单元格相同,请给我一个完美的解决方案。
    【解决方案2】:

    我使用了 mdewitt 的答案,将其转换为 swift 3,效果很好! 对于任何有兴趣的人:

    class CollectionViewOverlappingLayout: UICollectionViewFlowLayout {
    
    var overlap: CGFloat = 30
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.scrollDirection = .horizontal
        self.minimumInteritemSpacing = 0
    }
    
    override var collectionViewContentSize: CGSize{
        let xSize = CGFloat(self.collectionView!.numberOfItems(inSection: 0)) * self.itemSize.width
        let ySize = CGFloat(self.collectionView!.numberOfSections) * self.itemSize.height
    
        var contentSize = CGSize(width: xSize, height: ySize)
    
        if self.collectionView!.bounds.size.width > contentSize.width {
            contentSize.width = self.collectionView!.bounds.size.width
        }
    
        if self.collectionView!.bounds.size.height > contentSize.height {
            contentSize.height = self.collectionView!.bounds.size.height
        }
    
        return contentSize
    }
    
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    
        let attributesArray = super.layoutAttributesForElements(in: rect)
        let numberOfItems = self.collectionView!.numberOfItems(inSection: 0)
    
        for attributes in attributesArray! {
            var xPosition = attributes.center.x
            let yPosition = attributes.center.y
    
            if attributes.indexPath.row == 0 {
                attributes.zIndex = Int(INT_MAX) // Put the first cell on top of the stack
            } else {
                xPosition -= self.overlap * CGFloat(attributes.indexPath.row)
                attributes.zIndex = numberOfItems - attributes.indexPath.row //Other cells below the first one
            }
    
            attributes.center = CGPoint(x: xPosition, y: yPosition)
        }
    
        return attributesArray
    }
    
    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        return UICollectionViewLayoutAttributes(forCellWith: indexPath)
    }
    }
    

    【讨论】:

    • 滚动时的错误。集合末尾的空白单元格和一大片空白区域。
    【解决方案3】:

    基于 Maik Fruhner 的回答。使用方法:

    let someCollectionView = UICollectionView(frame: <frame you want>,
                                collectionViewLayout: CollectionViewOverlappingLayout())
    

    自定义布局代码:

    class CollectionViewOverlappingLayout: UICollectionViewFlowLayout {
    
      var overlap: CGFloat = 14
    
    
      override init() {
        super.init()
        self.sharedInit()
      }
      required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.sharedInit()
      }
      func sharedInit(){
        self.scrollDirection = .horizontal
        self.minimumInteritemSpacing = 0
      }
    
      override var collectionViewContentSize: CGSize{
        let xSize = CGFloat(self.collectionView!.numberOfItems(inSection: 0)) * self.itemSize.width
        let ySize = CGFloat(self.collectionView!.numberOfSections) * self.itemSize.height
    
        var contentSize = CGSize(width: xSize, height: ySize)
    
        if self.collectionView!.bounds.size.width > contentSize.width {
          contentSize.width = self.collectionView!.bounds.size.width
        }
    
        if self.collectionView!.bounds.size.height > contentSize.height {
          contentSize.height = self.collectionView!.bounds.size.height
        }
    
        return contentSize
      }
    
      override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    
        let attributesArray = super.layoutAttributesForElements(in: rect)
        let numberOfItems = self.collectionView!.numberOfItems(inSection: 0)
    
        for attributes in attributesArray! {
          var xPosition = attributes.center.x
          let yPosition = attributes.center.y
    
          if attributes.indexPath.row == 0 {
            attributes.zIndex = Int(INT_MAX) // Put the first cell on top of the stack
          } else {
            xPosition -= self.overlap * CGFloat(attributes.indexPath.row)
            attributes.zIndex = numberOfItems - attributes.indexPath.row //Other cells below the first one
          }
    
          attributes.center = CGPoint(x: xPosition, y: yPosition)
        }
    
        return attributesArray
      }
    
      override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        return UICollectionViewLayoutAttributes(forCellWith: indexPath)
      }
    }
    

    适用于 swift 5(可能还有 4.x)。

    【讨论】:

    • 它与 diffable 数据源一起爆炸 - 它似乎想在 dataSource.apply 上使用 self.collectionView!.numberOfItems
    【解决方案4】:

    我使用上面的两个答案为 Xamarin.iOS 创建了一个示例:

    public class CustomFlowLayout : UICollectionViewFlowLayout
    {
        public CustomFlowLayout(nfloat width, nfloat height)
        {
            ItemSize = new CGSize(width, height);
            ScrollDirection = UICollectionViewScrollDirection.Horizontal;
        } 
    
        public override CGSize CollectionViewContentSize
        {
            get
            {
                var xSize = CollectionView.NumberOfItemsInSection(0) * ItemSize.Width;
                var ySize = CollectionView.NumberOfSections() * ItemSize.Height;
    
                var contentSize = new CGSize(xSize, ySize);
    
                if (CollectionView.Bounds.Size.Width > contentSize.Width)
                    contentSize.Width = CollectionView.Bounds.Size.Width;
    
                if (CollectionView.Bounds.Size.Height > contentSize.Height)
                    contentSize.Height = CollectionView.Bounds.Size.Height;
    
                return contentSize;
            }
        }
    
        public override UICollectionViewLayoutAttributes[] LayoutAttributesForElementsInRect(CGRect rect)
        {
            var attributesArray = base.LayoutAttributesForElementsInRect(rect);
            var numberOfItems = CollectionView.NumberOfItemsInSection(0);
    
            foreach (var attributes in attributesArray)
            {
                var xPosition = attributes.Center.X;
                var yPosition = attributes.Center.Y;
    
                if (attributes.IndexPath.Row == 0)
                    attributes.ZIndex = int.MaxValue; // Put the first cell on top of the stack
                else
                {
                    xPosition -= 20 * attributes.IndexPath.Row; //20 - value based on your cell width
                    attributes.ZIndex = numberOfItems - attributes.IndexPath.Row; //Other cells below the first one
                }
    
                attributes.Center = new CGPoint(xPosition, yPosition);
            }
    
            return attributesArray;
        }
    }
    

    【讨论】:

    • 你在效果器中使用过这个吗?你也可以分享那个代码吗?
    • 现在不在我的笔记本电脑附近,但如果我没记错的话,它看起来像 uicollectionview colview = new uicollectionview(); colview.ViewFlowLayout = new CustomFlowLayout(number, number) 其中数字通常与屏幕宽度和高度成正比。它对你有帮助吗?明天我可以发布实际代码。你遇到了什么问题?
    • 那太好了。当我使用此代码为 UICollectionView.SetCollectionViewLayout(UICollectionViewLayout, bool) 方法创建实例时,我的应用程序会静默崩溃。
    • 显然只有你可以编辑它,所以请这样做 - 这样其他人就不必像我一样想出同样的事情了。
    • @iSpain17 感谢您的提醒。我在实际实现中已将其删除,但忘记在此处更新。
    【解决方案5】:

    在调用 numberOfItemsInSection 时使用 UICollectionViewDiffableDataSource 不会崩溃)

    (注意,其他解决方案修复了 collectionViewContentSize / 我欢迎编辑以解决此问题,而无需调用 numberOfItemsInSection。

        var cellLayout = OverlapLayout()
        cellLayout.scrollDirection = .horizontal
        cellLayout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
        cellLayout.itemSize = CGSize(width: 44, height: 44)
    
    
    class OverlapLayout: UICollectionViewFlowLayout {
    
        var overlap: CGFloat = 20
        override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
            let arr = super.layoutAttributesForElements(in: rect)!
            return arr.map { atts in
                var atts = atts
                if atts.representedElementCategory == .cell {
                    let ip = atts.indexPath
                    atts = self.layoutAttributesForItem(at: ip)!
                }
                return atts
            }
        }
    
        override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
            let atts = super.layoutAttributesForItem(at: indexPath)!
            var xPosition = atts.center.x
            if indexPath.item == 0 {
                return atts
            }else{
               xPosition -= self.overlap * CGFloat(atts.indexPath.row)
               atts.center = CGPoint(x: xPosition, y: atts.center.y)
               return atts
            }
    
        }
    
    }
    

    // Requires Snapkit
    class BubbleCell: UICollectionViewCell {
        static let ID = "BubbleCell"
        var magicCornerRadius = 22 // make this half the collectionview height
        lazy var bubbleImageView: UIImageView = UIImageView(frame: .zero)
    
        override init(frame: CGRect) {
            super.init(frame: frame)
    
            setupViews()
    
        }
    
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
        private func setupViews() {
    
            self.backgroundColor =  .clear
            contentView.backgroundColor = .clear
    
            // Hero Image
            contentView.addSubview(bubbleImageView)
            bubbleImageView.snp.remakeConstraints { (make) in
                make.edges.equalToSuperview()
            }
            bubbleImageView.layer.cornerRadius = magicCornerRadius
            bubbleImageView.layer.borderColor = UIColor.white.cgColor
            bubbleImageView.layer.borderWidth = 2
            bubbleImageView.clipsToBounds = true
            bubbleImageView.image = kPlaceholderImage
            bubbleImageView.contentMode = .scaleAspectFill
    
        }
    
        func configureCell(imageURL: URL) {
          // use third party library to fetch image here / that caches eg. SDWebImage
    
        }
    
    }
    

    【讨论】:

    • 我相信这个解决方案还没有完成。如果您有许多项目 (>100) 并前后滚动,重叠将改变其方向并有视觉错误。另外,滚动距离会比内容大很多。
    • 根据 cmets / 这是一个 hack - 需要更多的工作。对不起。