巢狀滾動效果實現討論

Bawn發表於2019-03-06

本文要討論的是類似於即刻淘票票首頁,抖音簡書個人主頁這樣的巢狀滾動效果,事實上網上已經有很多的相關的文章,比如:

而且絕大多數的文章都是從如何解決手勢衝突出發給出相應的解決方案,原因是他們大多數都採用了三級 Scrollview 的解決方案,如下圖

image1

  • 藍色檢視:一級 ScrollView

  • 紅色檢視:HeaderView

  • 綠色檢視:MenuView

  • 橘色檢視:二級 ScrollView

  • 黑色、深黑、淺黑:三級 ScrollView

可以看到三級 ScrollView 和 一級 ScrollView都需要在縱向滾動,所以重點要解決的就是這裡的滾動衝突,具體的細節我就不再贅述,大家還可以參考HGPersonalCenter這個專案,裡面有詳細的註釋。

之所以在前面給出了四個例子,是因為淘票票和簡書採用的是上面提到的方案,而抖音和即刻兩個則不是,並且即刻在體驗上更完美,這個後面會講到。簡單粗暴的用越獄手機+Reveal驗證下淘票票

image2

  • 上層的 MVNestTableView:一級 ScrollView

  • 中間的 UIScrollView:二級 ScrollView

  • 下層的 MVNestTableView:三級 ScrollView

當然通過點選狀態列看也可以粗略判斷實現方式,比如淘票票在點選狀態列後檢視只會滾動到子 ScrollView 的頂部而不是最外面 ScrollView 的,簡書雖然滾動到最外層的頂部但效果明顯不夠自然,原因就是三級 ScrollView 在縱向沒有延伸到頂部。

抖音和即刻在點選狀態列返回到頂部的效果非常自然,所以有理由相信它們在實現上不同於上述方案,那麼抖音和即刻的實現方式具體有什麼不同?同樣的用Reveal看下即刻的檢視結構

image3

從整體結構上來看即刻只有二級 ScrollView,所以在縱向上 ChildScrollView 會完全接管手勢,橫向滾動時又由 MainScrollView 控制,這樣子帶來的好處在於無需關心手勢衝突問題,但要實現前面提到的效果還必須處理是以下問題:

  • HeaderView 和 MenuView 的位置需要根據 ChildScrollView 的滾動而改變

  • 在切換的 Tab 的時候需要同步下一個 ChildScrollView 的 offset

  • ChildScrollView 必須在頂部留出 HeaderView 和 MenuView 高度總和的空白區域

  • HeaderView 不能攔截滾動手勢

在這裡就不給出具體的實現細節,文章後面最後有通過兩種方案實現的開源庫,歡迎 Star。

前面提到的即刻和抖音採用的都是這種二級 ScrollView 的方案,但即刻在體驗上更好,比如抖音的個人主頁如果手指開始滾動的地方有可互動的控制元件(Tab欄),那麼這時候滑動是會失效的,還有在切換Tab後將檢視下拉滾動到頂部然後返回到之前的Tab頁,抖音是直接返回到了原始的位置而即刻還是能保留之前進度。

頭部滾動失效解決方案

即刻為了達到完美的效果,在每個 ChildScrollView 頂部都新增了 HeaderView 和 MenuView,這樣子作為一個整體,即使開始觸控的地方有可互動控制元件也可以上下滾動。然後在左右滑動的時又讓ChildScrollView 內的 HeaderView 和 MenuView 隱藏,當停止滾動的時讓原本在外層 ScrollView 內的 HeaderView 和 MenuView 顯示。

保留進度解決方案

關於保留進度首先要做的就是判斷當前 ChildScrollView 是不是處於一種特殊狀態,這種狀態就是 offset.y的值是否大於 HeaderView 的偏移量,然後再通過判斷 ChildScrollView 當前的滾動方向,來決定是否要調整 HeaderView 和 MenuView 的位置。

對比兩個方案最終的實現各有優缺點

方案一

優點:

  • 無障礙配合使用第三方下拉重新整理庫

  • ChildViewController 無需額外設定

缺點:

  • 實現較複雜
  • 滾動有細微的停頓感

  • 切換Tab不能保留進度

  • 點選狀態列不能返回到頂部

方案二

優點:

  • 實現簡單

  • 滾動無停頓感

  • 切換Tab可保留進度

  • 點選狀態列可返回到頂部

缺點

  • ChildViewController 需要額外的設定(ChildScrollView 必須在頂部留出 HeaderView 和 MenuView 高度)

  • 下拉重新整理只能在 ChildViewController 內實現

這裡要提的是,由於方案二中 MainScrollView 並不會在縱向有滾動,所以下拉重新整理必須放在 ChildViewController 內實現,但又因為 HeaderView 和 MenuView 需要根據 ChildScrollView 的偏移而移動,在配合MJRefresh時它們的偏移有明顯的Bug(在本文釋出前我並沒深究解決方案),或許即刻也是因為這個原因而採用上面提到的解決辦法。

方案一開源庫:Aquaman

方案二開源庫:Shazam

上面兩個解決方案中的 MenuView 都設計成了交由開發者實現,因為即使整合各種樣式的也難滿足設計上的千奇百怪的要求,參考我的Demo就能很快實現一個自己想要的效果。

相關文章