UICollectionView 自定製佈局,例子是錢包界面

使用 UICollectionView 作錢包佈局界面,天然是自定製佈局,使用 UICollectionViewLayout 的子類git

咱們日常通常使用 UICollectionViewFlowLayout

指定大小 size, 間距 spacing, 每個格子的位置 frame 就出來了github

從頂部到底部,格子一個一個碼下來bash

如圖,作錢包佈局,須要自定製佈局 custom layout

自定製佈局 custom layout,就是手動指定每個格子的位置 frame

把格子放在咱們想要的位置上,即手動指定他的 frameide

自定製佈局很簡單,三步走:

* override public func prepare(), 重寫準備方法

通常能夠在這,把全部格子視圖的 frame 算出來佈局

frame 怎麼傳遞給對應的格子呢?ui

需建立對應的 UICollectionViewLayoutAttributes,spa

每每會有一些信息要記錄,狀態要保留,就使用 UICollectionViewLayoutAttributes 的子類。好來添加屬性code

* override public func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?, 重寫單個格子 item 的佈局方法

把上一步準備好的 UICollectionViewLayoutAttributes 子類,提供過去,供 UICollectionView 的管理引擎使用orm

* override public func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?, 重寫格子視圖 UICollection 的佈局方法

在這通常須要提供格子 item 、補充視圖 supplementary view 和裝飾視圖 decoration view 的佈局信息,cdn

本例子只涉及格子 item 的佈局, 把上一步準備好的 UICollectionViewLayoutAttributes 子類,提交給 UICollectionView 的管理引擎使用

錢包佈局的關鍵點:

這個佈局有兩個狀態,

* 初始狀態,前面的卡片層層疊疊,最後一張徹底展現

沒有卡片被選中的狀態至關於初始狀態

該狀態的佈局信息,計算簡單,

指定第一張卡片的位置 frame, 餘下卡片的位置,就都肯定了

就是縱座標有變化,y: titleHeight * CGFloat(index)

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
        }
    }
複製代碼

這裏使用了 UICollectionViewLayoutAttributes 的子類,

添加了一個卡片是否展開的屬性 isExpand

本例子中的 item , 添加了一個平移手勢,isExpand 用於手勢的交互

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
    }
}
複製代碼
* 選中狀態,選中的卡片展開,其他的卡片層層疊疊,最後一張狀態不變

這裏使用的計算方式是,把選中的卡片放在中間固定的位置,即肯定了選中卡片的 y 座標爲 collection.contentOffset.y + offsetSelected

這等於選中的編號 selectedIdx 的位置 frame, 肯定了

中間的位置肯定,而後計算兩邊

一邊從 selectedIdx - 1 到 0, 另外一邊從 selectedIdx + 1 到結尾,算出每個格子對應的 y 座標,就肯定了其 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
        
    }
複製代碼
* 選中了一個格子,就要刷新 UICollecionView 的自定製佈局

調用 invalidateLayout() ,就會把前面三個方法,從 prepare() 開始, 再走一遍

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
        }
    }
複製代碼

最後:

UICollectionView 的格子層層疊疊,下面的碼在上層的上方,經過 layer.zPosition 來保證

完整代碼見github 連接
相關文章
相關標籤/搜索