How archive like this CollectionView stacked Cell for Overlapping Cell Like Wallet

Viewed 784

I want exactly when tap particular cell it will show expand

enter image description here

let layout = UICollectionViewFlowLayout()
layout.minimumLineSpacing = -UIScreen.main.bounds.width/2.08

i want expand cell?

Friends any suggestion and needs helpful for us Thanks

4 Answers

Usually we use UICollectionViewFlowLayout, once the item's size and spacing is given, the item's frame is settled.

To do

CollectionView stacked Cell for Overlapping Cell Like Wallet

You need to give the frame you want for every item in UICollectionView.

Finishing it by customizing UICollectionViewLayout, and use your own UICollectionViewLayout subclass.

The final effect is as the following image

11

The basic thing:

To do custom layout is easy:

  • override public func prepare() ,

to calculate the frames for every item,

And put the frame for item in its container , your custom UICollectionViewLayoutAttributes

  • override public func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? ,

Retrieves layout information for an item at the specified index path with a corresponding cell.

to assign your prepared custom UICollectionViewLayoutAttributes for item layout

  • override public func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? ,

Retrieves the layout attributes for all of the cells and views in the specified rectangle.

There is no supplementary views, and decoration views in this situation, just to handle items.

to assign your prepared custom UICollectionViewLayoutAttributes for UICollectionView layout

Key Point:

You have two kinds of layout state.

  • the initial state, and the unselected state is the same, when no card is selected.

222

The front card are partly hidden, and the last card is full shown.

The custom UICollectionViewLayoutAttributes, with isExpand to record if there is cell selected.

isExpand is used for an cell added UIPanGestureRecognizer

class CardLayoutAttributes: UICollectionViewLayoutAttributes {
    var isExpand = false

    override func copy(with zone: NSZone? = nil) -> Any {
        let attribute = super.copy(with: zone) as! CardLayoutAttributes
        attribute.isExpand = isExpand
        return attribute
    }
}

The frame calculation of state unselected is easy.

Set the frame of the first item,

then the second ...

y: titleHeight * CGFloat(index) , all OK

 fileprivate func setNoSelect(attribute:CardLayoutAttributes) {
        guard let collection = collectionView else {
            return
        }
        let noneIdx = Int(collection.contentOffset.y/titleHeight)
        if noneIdx < 0 {
            return
        }
        attribute.isExpand = false
        let index = attribute.zIndex
        var currentFrame = CGRect(x: collection.frame.origin.x, y: titleHeight * CGFloat(index), width: cellSize.width, height: cellSize.height)
        if index == noneIdx{
            attribute.frame = CGRect(x: currentFrame.origin.x, y: collection.contentOffset.y, width: cellSize.width, height: cellSize.height)
        }
        else if index <= noneIdx, currentFrame.maxY > collection.contentOffset.y{
            currentFrame.origin.y -= (currentFrame.maxY - collection.contentOffset.y )
            attribute.frame = currentFrame
        }
        else {
            attribute.frame = currentFrame
        }
    }
  • the selected state, to select an item, the item is expanded , others will make room for it.

The logic is to put the selected item in the middle, the y position matters, collection.contentOffset.y + offsetSelected,

The center item's frame is known , then calculate the two sides.

One side is items from (selectedIdx-1) to 0, calculate the frame of item.

The Other side is items from (selectedIdx+1) to the final index , also calculate item's frame.

    fileprivate func calculate(for attributes: [CardLayoutAttributes],  choose selectedIP: IndexPath) -> [CGRect]{
        
        guard let collection = collectionView else {
            return []
        }
        let noneIdx = Int(collection.contentOffset.y / titleHeight)
        if noneIdx < 0 {
            return []
        }
        let x = collection.frame.origin.x
        
        var selectedIdx = 0
        for attr in attributes{
            if attr.indexPath == selectedIP{
                break
            }
            selectedIdx += 1
        }
        
        var frames = [CGRect](repeating: .zero, count: attributes.count)
        
        // Edit here
        let offsetSelected: CGFloat = 100
        let marginBottomSelected: CGFloat = 10
        frames[selectedIdx] = CGRect(x: x, y: collection.contentOffset.y + offsetSelected, width: cellSize.width, height: cellSize.height)
        if selectedIdx > 0{
            for i in 0...(selectedIdx-1){
                frames[selectedIdx - i - 1] = CGRect(x: x, y: frames[selectedIdx].origin.y - titleHeight * CGFloat(i + 1), width: cellSize.width, height: cellSize.height)
            }
        }
        if selectedIdx < (attributes.count - 1){
            for i in (selectedIdx + 1)...(attributes.count - 1){
                frames[i] = CGRect(x: x, y: frames[selectedIdx].origin.y + marginBottomSelected + titleHeight * CGFloat(i - selectedIdx - 1) + cellSize.height, width: cellSize.width, height: cellSize.height)
            }
        }
        
        
        return frames
        
    }

9999

  • When there is a item selected, you should refresh the custom layout.

To call invalidateLayout(),

Invalidates the current layout and triggers a layout update.

    fileprivate var _selectPath: IndexPath? {
        didSet {
            self.collectionView!.isScrollEnabled = (_selectPath == nil)
        }
    }
    
    
    
    public var selectPath: IndexPath? {
        set {
            _selectPath = (_selectPath == newValue) ? nil : newValue
            self.collectionView?.performBatchUpdates({
                self.invalidateLayout()
            }, completion: nil)
        } get {
            return _selectPath
        }
    }

one more thing, the sample demo in github

Here is the overlapping the layout, minimumLineSpacing negative number

if click the card it should be expanded and scroll down and up card should not disclose anymoew like wallet

class OverlappedCustomFlowLayout: UICollectionViewFlowLayout {
    override func prepare() {
        super.prepare()
        // This allows us to make intersection and overlapping
        // A negative number implies overlapping whereas positive implies space between the adjacent edges of two cells.
        minimumLineSpacing = -100
    }
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let layoutAttributes = super.layoutAttributesForElements(in: rect)
        for currentLayoutAttributes: UICollectionViewLayoutAttributes in layoutAttributes! {
            // zIndex - Specifies the item’s position on the z-axis.
            // Unlike a layer's zPosition, changing zIndex allows us to change not only layer position,
            // but tapping/UI interaction logic too as it moves the whole item.
            currentLayoutAttributes.zIndex = currentLayoutAttributes.indexPath.row + 1
        }
        return layoutAttributes
    }
}

you can try the following idea

The layout part:

var selectedIdx: Int?



override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let layoutAttributes = super.layoutAttributesForElements(in: rect)
        var originY: CGFloat = 0
        let defaultOffsetY: CGFloat = 80
        if let idx = selectedIdx{
            // expanded layout
            for i in 0..<cnt{
                 // frame the attribute
                 if i == idx + 1{
                       // to expand is to make room for the rest items
                       originY += 400
                 }
                 else{
                       originY += defaultOffsetY
                 }
            }
        }
        else{
            // default layout 
            for i in 0..<cnt{ 
                 // frame the attribute
                 originY += defaultOffsetY
            }
        }
        return layoutAttributes
    }

trigger the expand action

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        // edit the state
        layout.selectedIdx = indexPath.item
        // trigger refreshing the collectionView's layout
        collectionView.reloadData()
 }

I have tried it's working fine, thanks to everyone

class CardLayout: UICollectionViewLayout {
    var contentHeight: CGFloat = 0.0
    var cachedAttributes = [UICollectionViewLayoutAttributes]()
    var nextIndexPath: Int?
    override init() {
        super.init()
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override var collectionViewContentSize: CGSize {
        var size = super.collectionViewContentSize
        let collection = collectionView!
        size.width = collection.bounds.size.width
        if let _ = FlowLayoutAttributes.shared.cellIndex{
            size.height = contentHeight+UIScreen.main.bounds.width/2+38
        }else{
            size.height = contentHeight
        }
        print("Contend",contentHeight)
        return size
    }
    
    
    func reloadData(){
        self.cachedAttributes = [UICollectionViewLayoutAttributes]()
    }
    
    override func prepare() {
        cachedAttributes.removeAll()
        guard let numberOfItems = collectionView?.numberOfItems(inSection: 0) else {
            return
        }
        for index in 0..<numberOfItems {
            let layout = UICollectionViewLayoutAttributes(forCellWith: IndexPath(row: index, section: 0))
            layout.frame = frameFor(index: index)
            
            if let indexExpand = FlowLayoutAttributes.shared.cellIndex, indexExpand ==  index {
                self.nextIndexPath = index+1
                contentHeight = CGFloat(CGFloat(numberOfItems)*getCardSize())+UIScreen.main.bounds.width/2+38*2
            }else{
                contentHeight = CGFloat(CGFloat(numberOfItems)*getCardSize())+UIScreen.main.bounds.width/2+38
            }
            layout.zIndex = index
            layout.isHidden = false
            
            cachedAttributes.append(layout)
        }
    }
    
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        
        var layoutAttributes = [UICollectionViewLayoutAttributes]()
        
        for attributes in cachedAttributes {
            if attributes.frame.intersects(rect) {
                layoutAttributes.append(cachedAttributes[attributes.indexPath.item])
            }
            
        }
        return layoutAttributes
    }
    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        return true
    }
    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        return cachedAttributes[indexPath.item]
    }
    func frameFor(index: Int) -> CGRect {
        var frame = CGRect(origin: CGPoint(x: CGFloat(8), y:0), size: CGSize(width: UIScreen.main.bounds.width - CGFloat(8 + 8), height: CGFloat(UIScreen.main.bounds.width/2+38)))
        var frameOrigin = frame.origin
        if let indexExpand = FlowLayoutAttributes.shared.cellIndex{
            if index > 0 {
                if indexExpand < index {
                    let spacesHeight = CGFloat((getCardSize() * CGFloat(index)))+UIScreen.main.bounds.width/2+38-getCardSize()/2
                    frameOrigin.y = spacesHeight
                }else{
                    frameOrigin.y = CGFloat((getCardSize() * CGFloat(index)))
                }
            }
        }else{
            if index > 0 {
                frameOrigin.y = CGFloat((getCardSize() * CGFloat(index)))
            }
        }
        frame.origin = frameOrigin
        return frame
    }
    func getCardSize()-> CGFloat{
        if UIDevice().userInterfaceIdiom == .phone {
            switch UIScreen.main.nativeBounds.height {
            case 1136:
                print("iPhone 5 or 5S or 5C")
                return  45.25
            case 1334:
                print("iPhone 6/6S/7/8")
                return  45.25
            case 1920, 2208:
                print("iPhone 6+/6S+/7+/8+")
                return  46
            case 2436:
                print("iPhone X/XS/11 Pro")
                return  45.25
            case 2688:
                print("iPhone XS Max/11 Pro Max")
                return  46
            case 1792:
                print("iPhone XR/ 11 ")
                return  46
            case 2532:
                print("iPhone 12/ iPhone 12 Pro")
                return  45.50
            case 2778:
                print("iPhone 12 Pro Max")
                return  46.2
            default:
                return  46.2
            }
        }else{
            return CGFloat(46.2)
        }
    }
    
}
Related