本文做者:yanxin1563git
本文做者:github
zhanghaoweb
百度App在17年的版本中實現2個子view嵌套滾動,用於Feed落地頁(webview呈現文章詳情 + recycle呈現Native評論)。原理是在外層提供一個UI容器(咱們稱之爲」聯動容器」)處理WebView和Recyclerview連貫嵌套滾動。數組
當時的聯動容器對子view限制比較大,僅支持WebView和Recyclerview進行聯動滾動,數量也只支持2個子View。微信
隨着組件化進程的推動,爲方便各業務解耦,對聯動容器提出了更高的要求,須要支持任意類型、任意數量的子view進行聯動滾動,也就是本文要闡述的多子view嵌套滾動通用解決方案。app
先直觀感覺下聯動容器嵌套滾動的Demo效果:工具
同大多數自定義控件相似,聯動容器也須要處理子view的測量、佈局以及手勢處理。測量和佈局對聯動容器的場景來講很是簡單,手勢處理相對複雜些。組件化
從demo效果能夠看出,聯動容器須要處理好和子view嵌套滑動問題。嵌套滑動的處理方案有兩種佈局
百度App早期版本的聯動容器採用的方案2實現的,下圖爲方案2聯動容器手勢處理流程:學習
筆者對方案2聯動容器的實現代碼作了開源,感興趣的同窗能夠參考:https://github.com/baiduapp-tec/LinkageScrollLayout。
基於google的NestedScrolling實現多子view嵌套能節省很多開發量,故筆者對多子view嵌套的實現採用方案一。
Google在Android 5.0推出了一套NestedScrolling機制,這套機制滾動打破了對以前Android傳統的事件處理的認知,是按照逆向事件傳遞機制來處理嵌套滾動,事件傳遞可參考下圖:
網上有不少關於NestedScrolling的文章,若是沒接觸過NestedScrolling的同窗可參考下張鴻洋的這篇文章:https://blog.csdn.net/lmj623565791/article/details/52204039
爲了保證聯動容器中子view的任意性,聯動容器需提供完善的接口抽象供子view去實現。下圖爲聯動容器暴露的接口類圖:
ILinkageScroll是置於聯動容器中的子view必需要實現的接口,聯動容器在初始化時若是發現某個子view沒實現該接口,會拋出異常。
ILinkageScroll中又會涉及兩個接口:LinkageScrollHandler、ChildLinkageEvent。
LinkageScrollHandler接口中的方法聯動容器會在須要時主動調用,以通知子view完成一些功能,好比:獲取子view是否可滾動,獲取子view滾動條相關數據等。
ChildLinkageEvent接口定義了子view的一些事件信息,好比子view的內容滾動到頂部或底部。當發生這些事件後,子view主動調用對應方法,這樣聯動容器收到子view一些事件後會作出相應的反應,保證正常的聯動效果。
上面僅簡單說明了下接口功能,想更加深刻了解的同窗請參考:https://github.com/baiduapp-tec/ELinkageScroll
接下來咱們詳細分析下聯動容器對手勢處理細節,根據手勢類型,將嵌套滑動分爲兩種狀況來分析:1. scroll手勢;2. fling手勢;
先給出scroll手勢處理的核心代碼:
onNestedPreScroll()回調是google嵌套滑動機制NestedScrollingParent接口中的方法。當子view滾動時,會先經過此方法詢問父view是否消費這段滾動距離,父view根據自身狀況決定是否消費以及消費多少,並將消費的距離放入數組consumed中,子view再根據數組中的內容決定本身的滾動距離。
代碼註釋比較詳細,這裏總體再作個解釋:經過對子view的上邊沿閾值和聯動容器的scrollY進行比較,處理了3種case下的滾動狀況。
第10行,當scrollY == topEdge時,只要子view沒有滾動到頂或者底,都由子view正常消費滾動距離,不然由聯動容器消費滾動距離,並將消費的距離經過consumed變量通知子view,子view會根據consumed變量中的內容決定本身的滑動距離。
第17行,當scrollY > topEdge時,也就是說當觸摸的子view頭部已經滑出聯動容器,此時若是手指向上滑動,滑動距離所有由聯動容器消費,若是手指向下滑動,聯動容器會先消費部分距離,當聯動容器的scrollY達到topEdge後,剩餘的滑動距離由子view繼續消費。
第32行,當scrollY < topEdge這個和上一個第17行判斷相似,這裏不作過多解釋。scroll手勢處理流程圖以下:
聯動容器對fling手勢的處理大體思路以下:若是聯動容器的scrollY等於子view的top座標,則由子view自身處理fling手勢,不然由聯動容器處理fling手勢。
並且在一次完整的fling週期中,聯動容器和各子view將會交替去完成滑動行爲,直到速度降爲0,聯動容器須要處理好交替滑動時的速度銜接,保證整個fling的流暢行。接下來看下詳細實現:
onNestedPreFling()回調是google嵌套滑動機制NestedScrollingParent接口中的方法。當子view發生fling行爲時,會先經過此方法詢問父view是否要消費此次fling手勢,若是返回true,表示父view要消費此次fling手勢,反之不消費。
第6行根據velocityY正負值記錄本次的fling的方向;
第7行,當聯動容器scrollY值等於觸摸子view的top值,fling手勢由子view處理,同時聯動容器對本次fling手勢的速度進行追蹤,目的是當子view內容滾到頂或者底時,可以得到剩餘速度以讓聯動容器繼續fling;
第12行,由聯動容器消費本次fling手勢。下面看下聯動容器和子view交替fling的細節:
fling的速度傳遞分爲:
先看速度從聯動容器向子view傳遞。核心代碼在computeScroll()回調方法中。第9行,獲取聯動容器下一個滾動邊界值,若是達到下一個滾動邊界值,聯動容器須要將剩餘速度傳給下個子view,讓其繼續滾動。
第46行,getNextEdge()方法內部總體邏輯:遍歷全部子view,將聯動容器當前的scrollY與子view的top/bottom進行比較來獲取下一個滑動邊界。
第34行,當聯動容器檢測到滑動到下個邊界時,則調用ILinkageScroll.flingContent()讓子view根據剩餘速度繼續滾動。
再看速度從子view向聯動容器傳遞,核心代碼在第76行。當子view內容滾動到頂或者底,會回調onContentScrollToTop()方法或者onContentScrollToBottom()方法,聯動容器收到回調後,在第86行和第98行,繼續執行後續滾動。fling手勢處理流程圖以下:
對於內容可滾動的頁面,ScrollBar則是一個不可或缺的UI組件,因此,ScrollBar也是聯動容器必需要實現的功能。
好在Android系統對滾動條的抽象很是友好,自定義控件只須要重寫View中的幾個方法,Android系統就能幫助你正確繪製出滾動條。咱們先看下View中的相關方法:
對於垂直Scrollbar,咱們只須要重寫computeVerticalScrollOffset(),computeVerticalScrollExtent(),computeVerticalScrollRange()這三個方法便可。Android對這三個方法註釋已經很是詳細了,這裏再簡單解釋下:
computeVerticalScrollOffset()表示當前頁面內容滾動的偏移值,這個值是用來控制Scrollbar的位置。缺省值爲當前頁面Y方向上的滾動值。
computeVerticalScrollExtent()表示滾動條的範圍,也就是滾動條在垂直方向上所能觸及的最大界限,這個值也會被系統用來計算滾動條的長度。缺省值是View的實際高度。
computeVerticalScrollRange()表示整個頁面內容可滾動的數值範圍,缺省值爲View的實際高度。
須要注意的是:offset,extent,range三個值在單位上必須保持一致。
聯動容器是由系統中可滾動的子view組成的,這些子view(ListView、RecyclerView、WebView)確定都實現了ScrollBar功能,那麼聯動容器實現ScrollBar就很是簡單了,聯動容器只需拿到全部子view的offset,extent,range值,而後再根據聯動容器的滑動邏輯把全部子view的這些值轉換成聯動容器對應的offset,extent,range便可。接口設計以下:
LinkageScrollHandler接口在3.2小節解釋過,這裏不在贅述。這裏面三個方法由子view去實現,聯動容器會經過這三個方法獲取子view與滾動條相關的值。下面看下聯動容器中關於ScrollBar的詳細邏輯:
以上就是聯動容器實現ScrollBar的核心代碼,註釋也很是詳細,這裏再重點強調幾點:
系統爲了提升效率,ViewGroup默認不調用onDraw()方法,這樣就不會走ScrollBar的繪製邏輯。因此在第6行,須要調用setWillNotDraw(false)打開ViewGroup繪製流程;
第16行,收到子view的滾動回調,調用awakenScrollBars()觸發滾動條的繪製;
對於extent,直接使用缺省的extent,即聯動容器的高度;
對於range,對全部子view的range進行求和,最後獲得值即爲聯動容器的range;
對於offset,一樣先對全部子view的offset進行求和,以後還須要加上聯動容器自身的scrollY值,最終獲得的值即爲聯動容器的offset。
你們能夠返回到文章開頭,再看下Demo中滾動條的效果,相比於市面上其它使用相似聯動技術的App,本文對滾動條的實現很是接近原生了。
聯動容器執行fling操做時,藉助OverScroller工具類完成的。代碼以下:
藉助OverScroller.fling()方法完成聯動容器的fling行爲,這段代碼在小米手機上運行聯動會出現問題,mScroller.getCurrVelocity()一直是0。
緣由是小米手機Rom重寫了OverScroller,當fling()方法第三個參數傳0時,OverScroller.mCurrVelocity一直爲NaN,致使沒法計算出正確剩餘速度。
爲了解決小米手機的問題,咱們須要將第三個參數傳個非0值,這裏給1便可。
多子view嵌套實現原理並不複雜,對手勢處理的邊界條件比較瑣碎,須要來回調試完善,歡迎業內的朋友一塊兒交流學習。
Sample地址: https://github.com/baiduapp-tec/ELinkageScroll
---------------------------------
在微信-搜索頁面中輸入「百度App技術」,便可關注微信官方帳號。