Swift 實現瀑布流排版(Masonry Layout)

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

比如這張Pinterest官網的圖:


動手做「瀑布流排版」

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

排版的邏輯

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

自定Layout

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

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

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

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


Demo中其他的效果

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


推薦和參考

6 thoughts on “Swift 實現瀑布流排版(Masonry Layout)

  1. 請問切換Segment的時候,FlowLayout 的方法 prepare -> layoutAttributesForElements 這個流程為什麽都會跑三次?

    1. Hi Bevis,
      collection view第一次出現的時候會調用一次,然後當它被invalidate的時候也會調用一次,而當view有變化時也會被調用一次。
      所以你看到三次的調用:

      • invalidate調用了一次。
      • view第一次出現調用了一次。
      • size被改變調用了一次。
      1. 謝謝回复,

        關於切Segment時collection view layout 變換的動畫,我將你的代碼刪刪減減變換順序得出來的效果,還是原本的最平滑,但是不太理解為什麼那樣的組合有那樣的效果。

        另外,我用instrument 的 leak 查了一下,似乎在變換layout時會產生一個memory leak,我原本以為應該是closure retain住了self、collection view 或是 layout ,但是改了一改leak還是沒消掉。

        1. 晚上好:D

          抱歉因為光想著實現瀑布流排版這件事情,而疏忽了一些細節。
          instrument中提示的memory leak是指HomeFlowLayout.swift文件中的,「fileprivate var layoutItemSize = [String:CGSize]()」方法導致的。

          當我從原本的方法改成layz初始化方法以後就消除了memory leak,至於為什麼…目前我還沒想到Orz,需要進一步了解一下。

          fileprivate lazy var layoutItemSize:[String:CGSize] = {
          return [String:CGSize]()
          }()

          1. fileprivate lazy var layoutItemSize:[String:CGSize] = {
            return [String:CGSize]()
            }
            這個寫法是 compute property 的寫法。(註:個人經驗應該可以省略最後的括號)
            與原本直接將 [String: CGSize] 給值的方法不同。

            原本 var layoutItemSize = [String:CGSize]() 這個寫法會在取值的時候重覆的 init 一個新的 [String: CGSize] 的物件。

            或許改寫成 var layoutItemSize: [String: CGSize] = [:] 就可以了,歡迎試試

發表迴響

你的電子郵件位址並不會被公開。 必要欄位標記為 *