介紹
第一部分:Flutter - 仿Airbnb的價格區間篩選器。(一)
第二部分:Flutter - 仿Airbnb的價格區間篩選器。(二)
第三部分: Flutter-CustomPaint 繪製貝塞爾曲線圖表(三)
產品要求仿照airbnb的效果來一個,我看了一下,感覺這個互動挺棒。
分析
經過觀察,我將它分為圖表和底部滑塊兩部分。 圖表則進一步分為底層表和上層表,底層基本不用管就是背景,上層則需要根據滑塊進行變化。
上層圖表我想了三種實現方式:
1、(藉助MPchart)通過滑塊的位置,來重置圖表的Y值,以達到切割的效果。(實際效果發現如果圖表採用線性貝賽爾,0值會導致波谷溢位x軸,這是由於下層控制點造成的。而且上一個點很難跟滑塊對其導致切割線並不是垂直的)
2、自己繪製圖表,這個是最為靈活的,也是潛在的最完美的實現方案,但是非常耗時,由於工期較緊所以放棄了。(但是這個對於自定義widget的瞭解是非常有幫助的,後續我會把這裡的實現也補上)
3、(藉助MPchart)依然是上下兩張表,上層用ClipPath進行裁剪。(實際效果非常好,可以用先對短的時間達成相對完美的效果)
複製程式碼
實現
將價格滑塊widget分為左、右滑塊和中間的黑線三個widget
Container(
width: widget.rootWidth,
height: widget.rootHeight,
color: Colors.transparent,
child: Stack(
alignment: AlignmentDirectional.bottomStart,
overflow: Overflow.visible,
children: <Widget>[
///滑塊中間的黑線
Positioned(
bottom: 25,
child: _lineBlock(context, widget.rootWidth),
),
///左右滑塊
_leftImageBlock(context, widget.rootWidth),
_rightImageBlock(context, widget.rootWidth),
],
),
),
複製程式碼
通過leftBlackLineW、rightBlackLineW這兩個變數來控制水平padding,同時由滑塊的滑動來更新這兩個變數以達到黑線的動態變化。
Stack(
children: <Widget>[
Container(
color: Colors.transparent,
height: 5.0,
width: screenWidth,
alignment: Alignment.center,
//
padding: EdgeInsets.only(left: leftBlackLineW,right: rightBlackLineW),
child: Container(
color: Colors.black,
height: 3,
width: screenWidth ,
),
),
],
),
複製程式碼
滑塊的實現:
_imageItem(GlobalKey key){
//這裡要給一個key,後面要用來定位
return Container(
key: key,
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(6)
),
width: blockSize,
height: blockSize*0.7,
);
}
複製程式碼
左右滑塊的互動和手勢處理沒有本質的區別,所以這裡以左滑塊為例:(方便對程式碼的理解,我將介紹寫在註釋裡)
_leftImageBlock(BuildContext context, double screenWidth) {
return Positioned(
left: leftImageMargin,
//top: 0,
child: Stack(
alignment: AlignmentDirectional.bottomCenter,
overflow: Overflow.visible,
children: <Widget>[
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
//上方價格的顯示,當拖動時會顯示出來,避免手指拖動時擋住下方的價格而無法看到
Visibility(
visible: isLeftDragging,
child: Text(_leftPrice,style: TextStyle(fontSize: 12,color: Colors.black),),
),
//垂直佔位
SizedBox(
width: 1,
height: widget.rootHeight*0.7,
),
//左側滑塊
GestureDetector(
child: _imageItem(leftImageKey),
//水平方向移動 拖拽時
onHorizontalDragUpdate: (DragUpdateDetails details) {
/// details.delta.direction > 0 向左滑 、小於=0 向右滑動
isLeftDragging = true;
if(leftImageMargin < 0) {
//處理左邊邊界,避免滑塊溢位
leftImageMargin = 0;///確保不越界
leftBlackLineW = 2;
} else
//這裡進行兩滑塊相遇處理,如果小於等於5個步長,則不允許繼續向右滑動
//minimumDistance為最小間距,可以根據需要定製 預設是5個
if (details.delta.direction <= 0
&& ((screenWidth-(rightImageMargin+blockSize))-(leftImageMargin + blockSize))
<(singleW* minimumDistance)){
return ;
}
else {
//正常情況下的左側margin更新,以達到滑塊滑動的效果
leftImageMargin += details.delta.dx;
///確保線寬不溢位,這裡黑線的左側就會根據滑塊的變化而變化
leftBlackLineW = leftImageMargin+blockSize/2;
}
double _leftImageMarginFlag = leftImageMargin;
//重新整理上方的 price indicator
for(int i = 0; i< widget.list.length;i++){
if(_leftImageMarginFlag < singleW * (0.5 + i)){
///判斷滑塊位置區間 顯示對應價格
_leftPrice = widget.list[i].x;
//將所選的index傳出可以用作他用
leftImageCurrentIndex = i;
break;
}
}
setState(() {});// 重新整理UI
if(widget.leftSlidListener != null){
widget.leftSlidListener(true,leftImageCurrentIndex,leftImageKey);
}
},
///拖拽結束
onHorizontalDragEnd: (DragEndDetails details) {
//當拖拽結束時,我們需要對widget進行一次校準,避免出現影像異常
//同時,要求滑塊只能在每個價格區間的兩點上,也在這裡進行處理
isLeftDragging = false;
//確保快速短距離滑動時,滑塊超出最小間距的bug
if ( ((screenWidth-(rightImageMargin+blockSize))-(leftImageMargin+blockSize))<(singleW*5)){
setState(() {
});
return ;
}
double _leftImageMarginFlag = leftImageMargin;
///拖拽結束後,需要對滑塊進行校準,保證滑塊總是落在價格區間的端上上
for(int i = 0; i< widget.list.length;i++){
if(_leftImageMarginFlag < singleW * (0.5 + i)){
if(i == 0){
leftImageMargin = 0;
}else{
leftImageMargin = singleW * i;
}
_leftPrice = widget.list[i].x;
leftImageCurrentIndex = i;
break;
}
}
//解決快速滑動時,導致的橫線溢位問題
leftBlackLineW = leftImageMargin + blockSize;
setState(() {});// 重新整理UI
if(widget.leftSlidListener != null){
widget.leftSlidListener(false,leftImageCurrentIndex,leftImageKey);
}
},
),
//滑塊下方的價格文字,這裡偷個懶直接以白色代替透明,最好是用visiable或者offstage
Container(
padding: EdgeInsets.only(top: 6),
child: Text(_leftPrice,style: TextStyle(fontSize: 12,
color:!isLeftDragging ? Colors.black : Colors.white),),
)
],
),
],
),
);
}
複製程式碼
結語
至此整個價格滑塊widget就實現了,因為原專案是用provider來控制狀態的,DEMO並未使用,所以DEMO裡有些變數的傳遞看起來有點冗餘,其中還有一些莫名的widget巢狀,也是因為刪除了一些功能造成的,還請理解。 :) 我會盡快把下部分補上。 :)_