BottomSheetBehavior
- 追蹤 BottomSheet
- 系統默認實現效果
- 準備要實現的功能點:
- 定義三段式狀態:BottomSheetBehavoir
- 閥值定義
- 1. 未達到滾動閥值,恢復狀態
- 2. 達到滾動閥值,更新狀態
前面倒是有講過Android原生的BottomSheetBehavior,使用場景還是蠻多的,最近在用Flutter做一款地圖App,有用到BottomSheet的功能,但是 Flutter 自帶的BottomSheet有點拉,只能顯示和隱藏銷毀,不支持折疊為最小高度狀態也不支持三段式拖動,那就自己擼一個吧:
追蹤 BottomSheet
既然是基于系統的BottomSheet ,不妨來看看sdk的實現方式,正常來講,顯示一個BottomSheet,可以通過showBottomSheet 來觸發,或者給Scaffold配置bottomSheet屬性,查看源碼可以看到Scaffold.of(context).showBottomSheet,內部是創建了一個_StandardBottomSheet,繼續追蹤發現Widget其實是通過AnimatedBuilder來實現內容高度的擴展,其內部維護了一個BottomSheet。
簡單閱讀下BottomSheet源碼,重點就在于 GestureDetector 的垂直方向上的手勢回調 onVerticalDragUpdate 、以及onVerticalDragEnd,拖動位置更新、慣性滑動以及銷毀,核心都在這了。
系統默認實現效果
- 拖拽速度大于某一個像素閥值時,銷毀。
- 拖拽位置小于總高度的一半時,銷毀。
保留這一份默認效果,對于想使用默認效果的同學,不做任何額外配置即可。
準備要實現的功能點:
- 三段式: 基于SDK的BottomSheet ,可擴展為完全展開、中間狀態、折疊狀態;
- 阻尼、慣性滑動: 支持配置最小滑動偏移量;
- 保持狀態,支持Peek狀態: 以最小高度顯示BottomSheet;
- 打破 showBottomSheet 限制: 兼容系統默認的彈出方式,亦可當作正常的Widget使用,脫離showBottomSheet。
定義三段式狀態:BottomSheetBehavoir
- EXPANDED 完全展開
- HALF 中間狀態,介于EXPANDED與PEEK之間
- PEEK 以一個最小高度展示
- HIDDEN 完全隱藏,即銷毀,系統默認效果
開啟三段式,我們還需要配置一個約束條件,即BottomSheet的最大高度和最小高度 BoxConstraints:
- 最小高度
HALF模式下
如果提供的 Constraints minHeight 小于最大高度的一半,則取后者,防止位置錯亂!
var peekThreshold = enableHalf? min(_childHeight / 2, constraints.minHeight) / _childHeight: constraints.minHeight / _childHeight;
閥值定義
- 拖拽滾動閥值,大于此值,才允許滑動
const double _offsetThreshold = 32.0; - 展開時最大高度 閥值
const double _maxThreshold = 1.0; - 中間狀態閥值
const double _halfThreshold = 0.5;
當拖拽結束時,如果拖拽偏移量小于此閥值,則恢復狀態,這里有個麻煩的點是需要根據用戶拖拽方向來判斷,是向上還是向下拖動。
方向判斷可以在 _handleDragStart 回調時記錄初始偏移量startY,_handleDragEnd 時計算開始和結束的差值
/// 偏移量
var offset = updateY-startY ;
/// 當前動畫值var value = widget.animationController!.value;late double toValue;late BottomSheetBehavior mode;
offset<0 為向上滑動,反之 向下滑動。接下來需要根據滾動閥值來更新BottomSheet狀態。
1. 未達到滾動閥值,恢復狀態
- 向上滑動,恢復BottomSheet狀態: Expanded / Half / Peek
if (value >= _maxThreshold) {// 處于Expand狀態,恢復toValue = _maxThreshold;mode = BottomSheetBehavior.EXPANDED;} else if (value > _halfThreshold && enableHalf) {// 處于Half,恢復toValue = _halfThreshold;mode = BottomSheetBehavior.HALF;} else {toValue = peekThreshold;mode = BottomSheetBehavior.PEEK;}
- 向下滑動,恢復BottomSheet狀態: Expanded / Half / Peek
if (value > _halfThreshold) {// 處于Expand狀態,恢復toValue = _maxThreshold;mode = BottomSheetBehavior.EXPANDED;} else if (value > peekThreshold && enableHalf) {// 處于Half,恢復toValue = _halfThreshold;mode = BottomSheetBehavior.HALF;} else {toValue = peekThreshold;mode = BottomSheetBehavior.PEEK;}
2. 達到滾動閥值,更新狀態
- 向上滑動,更新BottomSheet狀態: Expanded / Half / Peek
if (value > _halfThreshold) {toValue = _maxThreshold;mode = BottomSheetBehavior.EXPANDED;} else if (value > peekThreshold) {toValue = enableHalf ? _halfThreshold : _maxThreshold;mode = enableHalf ? BottomSheetBehavior.HALF : BottomSheetBehavior.EXPANDED;} else {toValue = peekThreshold;mode = BottomSheetBehavior.PEEK;}
- 向下滑動,更新BottomSheet狀態: Half / Peek
if (value > _halfThreshold) {toValue = enableHalf ? _halfThreshold : peekThreshold;mode = enableHalf ? BottomSheetBehavior.HALF : BottomSheetBehavior.PEEK;
} else {toValue = peekThreshold;mode = BottomSheetBehavior.PEEK;
}
以上,我們獲取到了開始講到的AnimatedBuilder的 動畫值以及變化量,在**_handleDragEnd**中可以通過animateTo平滑的過渡BottomSheet狀態
/// 以動畫的形式fly
void animateTo(double to) {widget.animationController!.animateTo(to,curve: Curves.linearToEaseOut,duration: animateDuration,);
}
- 另外,至于BottomSheet的最新的狀態回調,最好是在動畫結束后再通知給調用者,以免更新狀態期間build重繪!
Future.delayed(animateDuration, () => widget.onBehaviorChanged?.call(mode));
至此,既保留了flutter默認的BottomSheet效果,又擴展了三段式,當然,調用方式和系統BottomSheet一模一樣,另外還可以像普通Widget一樣來使用哦,來看看最終的效果吧
項目效果
Demo