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

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


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

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

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

計(jì)算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. 計(jì)算 halfExpandedOffset

3. 計(jì)算 expandedOffset

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

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


onInterceptTouchEvent的攔截邏輯如下

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

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

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

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

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

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

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

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

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

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