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