場景
在社交類軟件中,經(jīng)常都會有類似抖音這種顯示彈出框的場景,用來顯示一段評論列表夺艰∮罂蓿↓
底部評論彈框.png
在flutter里面,有一個交互效果非常不錯的原生組件:BottomSheet,自帶入場出場動畫以及下拉退出動畫效果郁副,所以毫無疑問使用這個控件是非常好的選擇减牺,可是當(dāng)使用起來會發(fā)現(xiàn),如果BoottomSheet中的子組件是可滾動類型的child時存谎,會因為滑動沖突問題導(dǎo)致下拉退出功能失效拔疚,一旦無法實現(xiàn)下拉退出效果,在用戶體驗上是非常不好的既荚,這個組件也顯得比較雞肋稚失,那么該如何在它的基礎(chǔ)上實現(xiàn)下拉效果呢?
思路
我們從BottomSheet的源碼看恰聘,內(nèi)部其實是通過獲取用戶的滑動距離配合AnimationBuilder實現(xiàn)的動態(tài)改變彈框高度句各,從而實現(xiàn)的上拉下拉的動畫效果吸占。
image.png
而當(dāng)包含Listview時,由于Listview的本身滑動機(jī)制使得BottomSheet內(nèi)部的觸摸事件無法傳遞所致凿宾,所以從這里入手我們直接自己來處理整個用戶的觸摸手勢矾屯,當(dāng)listview在頂部時手勢響應(yīng)下拉動畫,當(dāng)listview不在頂部時則繼續(xù)響應(yīng)listview本身的滑動效果即可初厚。
所以這里會涉及到如下組件:
- Listener:用于監(jiān)聽用戶的手勢件蚕,獲取手指按下的位置、滑動的距離产禾、離開屏幕的時機(jī)排作。
- AnimationContainer:動態(tài)的改變彈框高度,從而達(dá)到高度隨著手勢變化的下拉動畫效果亚情。
- StreamBuilder:通過不斷的接收手勢事件來改變彈框的高度纽绍。
實現(xiàn)
部分代碼如下,完整代碼在文末
/// 用來發(fā)送事件 改變彈框高度的stream
StreamController<double> _streamController = StreamController<double>.broadcast();
/// 列表彈起的正常高度
double _totalHeight = 400;
/// 記錄手指按下的位置
double _pointerDy = 0;
AnimatedContainer(
duration: Duration(milliseconds: 30),
height: currentHeight,
child: Listener(
onPointerMove: (event){
// 觸摸事件過程 手指一直在屏幕上且發(fā)生距離滑動
if(_scrollController.offset != 0){
// 只有列表滾動到頂部時才觸發(fā)下拉動畫效果
print("onPointerMove:${_scrollController.offset}");
return;
}
double distance = event.position.dy - _pointerDy;
if (distance.abs() > 0) {
// 獲取手指滑動的距離势似,計算彈框?qū)崟r高度拌夏,并發(fā)送事件
double _currentHeight = _totalHeight - distance;
if(_currentHeight > _totalHeight){
return;
}
_streamController.sink.add(_currentHeight);
}
},
onPointerUp: (event){
// 觸摸事件結(jié)束 手指離開屏幕
// 這里認(rèn)為滑動超過一半就認(rèn)為用戶要退出了,值可以根據(jù)實際體驗修改
if(currentHeight < (_totalHeight * 0.5)){
Navigator.pop(context);
}else{
_streamController.sink.add(_totalHeight);
}
},
onPointerDown: (event){
// 觸摸事件開始 手指開始接觸屏幕
_pointerDy = event.position.dy + _scrollController.offset;
},
child: ListView(
controller: _scrollController,
physics: currentHeight != _totalHeight ? NeverScrollableScrollPhysics() : ClampingScrollPhysics(),
children: [
],
),
),
)
關(guān)鍵點(diǎn)
- 當(dāng)listview本身沒有滑動到頂部時還要繼續(xù)響應(yīng)listview的滑動效果履因,是否在頂部的判斷條件是_scrollController.offset != 0
// 觸摸事件過程 手指一直在屏幕上且發(fā)生距離滑動
if(_scrollController.offset != 0){
// 只有列表滾動到頂部時才觸發(fā)下拉動畫效果
print("onPointerMove:${_scrollController.offset}");
return;
}
- 獲取手指滑動的縱向距離障簿,彈框的動態(tài)高度=彈框的最大高度-手指滑動的距離
double distance = event.position.dy - _pointerDy;
if (distance.abs() > 0) {
// 獲取手指滑動的距離,計算彈框?qū)崟r高度栅迄,并發(fā)送事件
double _currentHeight = _totalHeight - distance;
if(_currentHeight > _totalHeight){
return;
}
_streamController.sink.add(_currentHeight);
}
- 在獲取手指離開屏幕的方法中站故,有兩種情況,一種是滑動距離不夠毅舆,彈框要回到最大高度西篓,一種是滑動距離足夠,這時將彈框pop掉即可憋活,注意這里pop掉是會響應(yīng)BottomSheet的退出動畫岂津,所以無需在做特殊處理。
onPointerUp: (event){
// 觸摸事件結(jié)束 手指離開屏幕
// 這里認(rèn)為滑動超過一半就認(rèn)為用戶要退出了悦即,值可以根據(jù)實際體驗修改
if(currentHeight < (_totalHeight * 0.5)){
Navigator.pop(context);
}else{
_streamController.sink.add(_totalHeight);
}
},
- 當(dāng)動畫效果發(fā)生時吮成,也就是彈框的高度不是最大高度時,這里要將listview的滑動效果禁止掉辜梳,否則在手指上下滑動彈框整體時會有l(wèi)istview的鬼畜響應(yīng)效果粱甫,彈框高度恢復(fù)時也同步恢復(fù)listview的滑動響應(yīng)。
physics: currentHeight != _totalHeight ? NeverScrollableScrollPhysics() : ClampingScrollPhysics(),
效果
bottom_sheet.gif
總結(jié)
- 通過這個問題還是意識到一定要多看源碼作瞄、多看源碼茶宵、多看源碼,往往看似比較困難的問題通常在懂得原理后也就迎刃而解宗挥。
- 頻繁的通過stream發(fā)送事件不是此方法的最優(yōu)解乌庶,只要思路正確叶摄,配合上Flutter本身的多種多樣的組件其實是有很多解決方案的“材猓或許你有更好的思路也不妨交流下~