Swift 实现瀑布流布局(Masonry Layout)

阅读 762
收藏 26
2017-03-14
原文链接:ios.devdon.com

圖片類的應用我們常常會看到所謂的「瀑布流排版」,各種不同大小的圖片拼接擺放在畫面上,而也有人直接稱這種排版為Pinterest排版,
可能是因為Pinterest是早期經典的RWD設計網站之一。而正式一點的說法應該是Masonry Layout,Dynamic Grid Layout。

比如這張Pinterest官網的圖:


動手做「瀑布流排版」

上面是我們最終的實現效果,感覺不錯的話可以繼續往下看:D

排版的邏輯

  • 第一排橘色的部分,直接從左至右放下圖片。
  • 接下來不斷的將新的圖片,安插在最短的column上,從而實現瀑布流的排版方式,可以參考上面放置圖片的數字順序。

自定Layout

我們打算通過UICollectionViewFlowLayout來實現這個佈局,

prepare()是它的入口,在這裡可以做一些初始化的設定,比如基本的邊界、cell之間的距離等等。
因為demo中是提供了切換佈局的功能,而我們希望佈局在計算過後不用再重新計算,所以會先判斷是否已經算過。
如果沒有計算過則執行我們的computeAndStoreAttributesWithItemWidth方法來計算佈局信息。

    override func prepare() {
        super.prepare()
        
        minimumInteritemSpacing = 10
        minimumLineSpacing      = 10
        
        sectionInset.top        = 10
        sectionInset.left       = 10
        sectionInset.right      = 10
        
        // 如果之前沒有計算過Layout則計算並存入cache中
        if layoutAttributes[layoutType.keyName] == nil && collectionView != nil{
            // 根據想要的column數量來計算一個cell的寬度
            let contentWidth:CGFloat = collectionView!.bounds.size.width - sectionInset.left - sectionInset.right
            let itemWidth = (contentWidth - minimumInteritemSpacing * (CGFloat(layoutType.column)-1)) / CGFloat(layoutType.column)
            
            // 計算cell的佈局
            computeAndStoreAttributes(layoutType ,CGFloat(itemWidth))
            
        }
    }

「排版的本質是去計算每一個cell在scrollView中的位置」。
下面是計算每一個Cell的Attribute方法,其中包含了計算後存起來的動作,計算後會將結果存起來。

    // 計算cell的frame以及設定item size來提供系統計算UICollectionView的contentSize
    fileprivate func computeAndStoreAttributes(_ layoutType:LayoutType ,_ itemWidth:CGFloat) {
        
        // 以sectionInset.top作為最初始的高度,紀錄每一個column的高度
        var columnHeights = [CGFloat](repeating: sectionInset.top, count: layoutType.column)
        
        // 記錄每一個column的item個數
        var columnItemCount = [Int](repeating: 0, count: layoutType.column)
        
        // 紀錄每一個cell的attributes
        var attributes = [UICollectionViewLayoutAttributes]()
        
        var row = 0
        for item in items {
            // 建立一個attribute
            let indexPath = IndexPath.init(row: row, section: 0)
            let attribute = UICollectionViewLayoutAttributes(forCellWith: indexPath)
            
            // 找出最短的Column
            let minHeight = columnHeights.sorted().first!
            let minHeightColumn = columnHeights.index(of: minHeight)!
            
            // 新的照片放到最短Column上
            columnItemCount[minHeightColumn] += 1
            let itemX = (itemWidth + minimumInteritemSpacing) * CGFloat(minHeightColumn) + sectionInset.left
            let itemY = minHeight
            
            // 計算高度,按照原圖片大小等比例縮放
            let itemHeight = item.size.height * itemWidth / item.size.width
            
            // 設定Frame,加入到attributes中
            attribute.frame = CGRect(x: itemX, y: CGFloat(itemY), width: itemWidth, height: CGFloat(itemHeight))
            attributes.append(attribute)
            
            // 計算最短的column當前的高度
            columnHeights[minHeightColumn] += itemHeight + minimumLineSpacing
            
            row += 1
        }
        
        // 找出最高的Column
        let maxHeight = columnHeights.sorted().last!
        let column = columnHeights.index(of: maxHeight)
        
        // 用於系統計算collectionView的contentSize - 根據最高的Column來設置itemSize,使用總高度的平均值
        let itemHeight = (maxHeight - minimumLineSpacing * CGFloat(columnItemCount[column!])) / CGFloat(columnItemCount[column!])
        itemSize = CGSize(width: itemWidth, height: itemHeight)
        
        // 將計算後的結果存起來
        layoutAttributes[layoutType.keyName] = attributes
        layoutItemSize[layoutType.keyName] = itemSize
        
    }

下面是系統讀取attributes的方法,因為layoutAttributes中只有每一個cell的frame資訊,
所以要記得同時修改itemSize,否則因為itemSize的錯誤而導致UICollectionView的contentSize是錯的。

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        // 佈局變化時記得跟著改變itemSize
        if let size = layoutItemSize[layoutType.keyName] {
            itemSize = size
        }
        
        return layoutAttributes[layoutType.keyName]
    }

Demo中其他的效果

UICollectionView切換的動畫的方法,要記得先執行collectionViewLayout.invalidateLayout,然後再換成新的佈局。

        // 更換layout動畫
        UIView.animate(withDuration: 1, animations: {
            self.aCollectionView.collectionViewLayout.invalidateLayout()
            self.aCollectionView.performBatchUpdates({
                self.aCollectionView.setCollectionViewLayout(self.flowLayout, animated: true)
            }, completion: nil)
        })

推薦和參考

评论