Flutter - 仿Airbnb的價格區間篩選器。(一)

吉哈達發表於2020-05-02

介紹

第一部分:Flutter - 仿Airbnb的價格區間篩選器。(一)

第二部分:Flutter - 仿Airbnb的價格區間篩選器。(二)

第三部分: Flutter-CustomPaint 繪製貝塞爾曲線圖表(三)

Flutter - 仿Airbnb的價格區間篩選器。(一)

產品要求仿照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巢狀,也是因為刪除了一些功能造成的,還請理解。 :) 我會盡快把下部分補上。 :)_

DEMO

github.com/bladeofgod/…

相關文章