一文搞懂 BottomSheetBehavior
BottomSheetBehavior能實現(xiàn)怎樣的效果,一圖勝千言。
如果僅僅是實現(xiàn)上下拖動和隱藏的功能。拋開BottomSheetBehavior自己實現(xiàn)也不難,在沒有CoordinatorLayout的年代,這種效果往往是純手工打造。既然如此為何Google要專門設(shè)計BottomSheetBehavior呢?為了搞清楚這個問題,我查閱源碼探究了一番,確實發(fā)現(xiàn)了一些隱秘的角落。我將從以下幾個方面講解BottomSheetBehavior的設(shè)計思路
- 講解BottomSheetBehavior的幾種狀態(tài)
- 講解BottomSheetBehavior的事件分發(fā)
- 講解BottomSheetBehavior如何處理嵌套滑動
- 實現(xiàn)高德地圖首頁效果,歡迎關(guān)注字節(jié)小站微信公眾號號
BottomSheetBehavior一共有6種狀態(tài)
- STATE_EXPANDED ?全部展開狀態(tài)
- STATE_COLLAPSED 收起狀態(tài)
- STATE_DRAGGING ?拖動狀態(tài)
- STATE_SETTLING
- STATE_HIDDEN ? ?隱藏狀態(tài)
- STATE_HALF_EXPANDED 半展開狀態(tài)

系統(tǒng)通過哪種方式實現(xiàn)每種狀態(tài)不同的偏移量呢?
- layout階段通過ViewCompat.offsetTopAndBottom(child, offset)實現(xiàn)偏移量
- 用戶觸摸交互階段通過ViewDragHelper.dragTo(left,top,dx,dy)實現(xiàn)偏移量
2.1 Layout階段
BottomSheetBehavior#onLayoutChildLayout階段最后會通過findScrollingChild方法,尋找開啟了嵌套滑動的后代View。其實這就是Google單獨研發(fā)出BottomSheetBehavior的主要考量。滿足支持嵌套滑動的BottomSheet效果。
2.2 用戶觸摸交互階段


2.3 狀態(tài)對應(yīng)的偏移量
| 狀態(tài) | 偏移量 |
|---|---|
| STATE_COLLAPSED | collapsedOffset |
| STATE_EXPANDED | getExpandedOffset() |
| STATE_HALF_EXPANDED | halfExpandedOffset |
| STATE_HIDDEN | parentHeight |
1. 計算 collapsedOffset

| 變量名 | 默認值 |
|---|---|
| PEEK_HEIGHT_AUTO | 常量值-1 |
| peekHeightMin | 默認值64dp,用戶不可修改 |
| peekHeightAuto | 默認值true,用戶可設(shè)置 |
| peekHeight | 默認值0,如果設(shè)置為PEEK_HEIGHT_AUTO peekHeightAuto為true否則為false,如果設(shè)置小于-1則為0 |
| fitToContents | 默認值true,用戶可設(shè)置 |
| fitToContentOffset | Math.max(0, parentHeight - child.getHeight()) |
peekHeight默認值為0。設(shè)置邏輯如下

- height為-1,則peekHeightAuto設(shè)置為true。
- 否則peekHeightAuto為false,而且peekHeight最小值為0。

計算collapsedOffset值有四種情況
| Case | peekHeightAuto | fitToContents |
|---|---|---|
| case1 | true | true |
| case2 | true | false |
| case3 | false | true |
| case4 | false | false |
返回值
| Case | 返回值 |
|---|---|
| Case1 | Math.max(parentHeight - Math.max(peekHeightMin, parentHeight - parentWidth * 9 / 16), fitToContentsOffset) |
| Case2 | parentHeight-Math.max(peekHeightMin, parentHeight - parentWidth * 9 / 16) |
| Case3 | Math.max(parentHeight - peekHeight, fitToContentsOffset) |
| Case4 | parentHeight - peekHeight |
2. 計算 halfExpandedOffset

3. 計算 expandedOffset

4.如何固定BottomSheetBehavior的高度?
了解這些值的計算有什么好處。假設(shè)我想讓BottomSheetBehavior,固定高度,不能向上滑也不能向下滑。那我們則需要將collapsedOffset和expandedOffset設(shè)置為一樣的值才行。
代碼如下
為了良好的閱讀體驗沒有使用代碼塊呈現(xiàn)代碼,如果你想獲取代碼請訪問github代碼庫
3. 講解BottomSheetBehavior的事件分發(fā)學(xué)習Android事件分發(fā)是有方法的。我總結(jié)為"三板斧"分析法
- 源碼分析
- 場景化
- 樹形圖分析
3.1 三板斧之源碼分析

從onInterceptTouchEvent的代碼中,可以看到viewDragHelper.shouldInterceptTouchEvent(event),說明攔截方法會讓ViewDragHelper方法處理。
ViewDragHelper的初始化,會傳入ViewDragHelper.Callback dragCallback對象,該對象的boolean tryCaptureView(View child, int pointerId)方法決定viewDragHelper.shouldInterceptTouchEvent的返回值。


onInterceptTouchEvent的攔截邏輯如下

onTouchEvent主要交由ViewDragHelper#processTouchEvent處理,如果是Move事件最終會調(diào)用dragTo方法進行移動

3.2 三板斧之場景化和樹形圖分析
假設(shè)有場景如下,用戶可以在HeadLayout、NestedScrollingChild,TopMostLayout區(qū)域內(nèi)上下滑動。這三個case,事件的處理路徑如何呢?
轉(zhuǎn)化成樹形圖如下
設(shè)置BottomSheetBehavior為LinearLayout的Behavior
3.2.1. 在HeadLayout區(qū)域內(nèi)上下滑動
- Down事件處理,初始狀態(tài),在ViewDragHelper的shouldInterceptTouchEvent方法中不會調(diào)用tryCaptureViewForDrag方法,該方法返回false。在BottomSheetBehavior onInterceptTouchEvent中完整事件路徑如下,紅線表示事件的分發(fā)路徑

結(jié)合樹形圖分析。由于BottomSheetBehavior不攔截事件。Down事件分發(fā)流程如下

最終會調(diào)用到BottomSheetBehavior的onTouchEvent方法,會調(diào)用到ViewDragHelper的processTouchEvent方法

最終會將ViewDragHelper的dragState設(shè)置為STATE_DRAGGING
- MOVE事件在BottomSheetBehavior onInterceptTouchEvent分發(fā)流程如下

接下來直接調(diào)用 BottomSheetBehavior 的onTouchEvent方法。同樣調(diào)用到ViewDragHelper的processTouchEvent方法
3.2.2. 在NestedScrollingView區(qū)域內(nèi)上下滑動
1.Down事件分發(fā)到BottomSheetBehavior的onInterceptTouchEvent分發(fā)流程如下
由于不攔截。Down事件分發(fā)給NestedScrollingChild,NestedScrollingChild會啟動嵌套滑動,與BottomSheetBehavior配合完成嵌套滑動
2.Move事件分發(fā)流程比較復(fù)雜,當在NSC中Move的距離沒達到閾值時,MOVE會繼續(xù)分發(fā)到BottomSheetBehavior的onInterceptTouchEvent中,當在NSC中MOVE距離達到閾值時,會調(diào)用parent.requestDisallowInterceptTouchEvent(true)從此直達NSC,就是純粹的嵌套滑動了。

接下來事件交由NSC分發(fā),當MOVE距離大于閾值時,事件直接交由NSC處理。

3.2.2. 在TopMostLayout區(qū)域內(nèi)上下滑動,該區(qū)域與NSC區(qū)域沒有交集
Down事件同上
MOVE事件,當距離>ViewDragHelper閾值時

由于MOVE事件攔截了,會交由BottomSheetBehavior onTouchEvent處理,如下圖
至此已基本講解完BottomSheetBehavior的事件分發(fā)機制。具體細節(jié)未能盡善盡美。紙上得來終覺淺,希望讀者可以結(jié)合文章去探索源碼。下次我將用BottomSheetBehavior來實現(xiàn)高德地圖首頁效果。歡迎持續(xù)關(guān)注

—————END—————
我是南塵,只做比心的公眾號,歡迎關(guān)注我。
推薦閱讀:
歡迎關(guān)注南塵的公眾號:nanchen
做不完的開源,寫不完的矯情,只做比心的公眾號,如果你喜歡,你可以選擇分享給大家。如果你有好的文章,歡迎投稿,讓我們一起來分享。? ? ??? ??長按上方二維碼關(guān)注? ? ? ? 做不完的開源,寫不完的矯情? ? ? ? 一起來看 nanchen 同學(xué)的成長筆記
