Flutter - 仿Airbnb的价格区间筛选器。(一)

3,601 阅读4分钟

介绍

第一部分: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嵌套,也是因为删除了一些功能造成的,还请理解。 :) 我会尽快把下部分补上。 :)_

DEMO

github.com/bladeofgod/…