GIF效果图加载中【文件较大请稍等】
由于开发的需要实现一个RangeSlider【选择开始和结尾范围的Slider】。可惜pub.dev没有现成的第三方控件可用。所以就动手自己,实现了一个。
demo.gif
最终实现的交互效果【源码放在下一篇文章】
demo2.gif
新建文件,命名为【date_range_slider.dart】
import 'package:flutter/material.dart';
enum SelectedRange { leftA, rightB, none, translation }
enum AppearanceState { statePointerDown, statePointerUp }
typedef SliderValueChangedCallback = void Function(
num beginPercent, num endPercent);
class DateRangeSlider extends StatefulWidget {
final double sliderHandleWidth;
final double minimumRangePercentage;
final SliderValueChangedCallback? valueChangedCallback;
const DateRangeSlider(
{Key? key,
this.sliderHandleWidth = 25,
this.minimumRangePercentage = 0.2,
this.valueChangedCallback})
: super(key: key);
@override
_DateRangeSliderState createState() => _DateRangeSliderState();
}
class _DateRangeSliderState extends State<DateRangeSlider> {
Offset offsetA = Offset(0, 0);
Offset offsetB = Offset(250, 0);
SelectedRange selectedRange = SelectedRange.none;
AppearanceState appearanceState = AppearanceState.statePointerUp;
///
bool isFirstLoad = true;
Color darkBlue = Colors.indigo;
Color blueAccent = Colors.blueAccent;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
///最小间距
double minimumGap =
constraints.biggest.width * widget.minimumRangePercentage;
///第一次进入 默认 Range为 50% - 100%
if (isFirstLoad == true) {
isFirstLoad = false;
double halfWidth = constraints.biggest.width / 2;
this.offsetA = Offset(halfWidth - widget.sliderHandleWidth, 0);
this.offsetB = Offset(
constraints.biggest.width - widget.sliderHandleWidth / 2 - 2, 0);
///GetX 记录百分比
Future.delayed(Duration(milliseconds: 500), () {
calculatePercentageRange(constraints);
});
}
return Listener(
onPointerDown: (PointerDownEvent event) {
setState(() {
appearanceState = AppearanceState.statePointerDown;
});
},
onPointerMove: (PointerMoveEvent event) {
setState(() {
double buttonARightEdge =
offsetA.dx + event.delta.dx + widget.sliderHandleWidth / 2;
double buttonBRightEdge =
offsetB.dx + event.delta.dx + widget.sliderHandleWidth / 2;
///如果滑动 左边的按钮
if (selectedRange == SelectedRange.leftA) {
// print('6666666event.delta.dx ${event.delta.dx}');
if (buttonARightEdge >= 0 - widget.sliderHandleWidth / 2) {
///如果左边的按钮 往右边 滑,要保证两个按钮之间 最小间距
if (event.delta.dx > 0) {
if (buttonBRightEdge - buttonARightEdge > minimumGap) {
offsetA = Offset(offsetA.dx + event.delta.dx, 0);
}
} else {
double xSet = offsetA.dx + event.delta.dx;
if (xSet < -(widget.sliderHandleWidth / 2)) {
xSet = -(widget.sliderHandleWidth / 2);
}
offsetA = Offset(xSet, 0);
}
}
///如果滑动 右边的按钮
} else if (selectedRange == SelectedRange.rightB) {
if (buttonBRightEdge <= constraints.biggest.width) {
///如果右边的按钮 往左边滑,要保证两个按钮之间 最小间距
if (event.delta.dx < 0) {
if (buttonBRightEdge - buttonARightEdge > minimumGap) {
offsetB = Offset(offsetB.dx + event.delta.dx, 0);
}
} else {
double xSet = offsetB.dx + event.delta.dx;
if (xSet > (constraints.biggest.width)) {
xSet = (constraints.biggest.width);
}
offsetB = Offset(xSet, 0);
}
}
///如果是 处理平移 【range 】
} else if (selectedRange == SelectedRange.translation) {
if (buttonARightEdge >= 0 &&
buttonBRightEdge <= constraints.biggest.width) {
offsetA = Offset(offsetA.dx + event.delta.dx, 0);
offsetB = Offset(offsetB.dx + event.delta.dx, 0);
}
}
///GetX 记录百分比
calculatePercentageRange(constraints);
});
},
onPointerUp: (PointerUpEvent event) {
setState(() {
///恢复默认状态
selectedRange = SelectedRange.none;
appearanceState = AppearanceState.statePointerUp;
});
},
child: Container(
height: 55,
child: Stack(
children: [
Positioned(
top: 0,
left: offsetA.dx + widget.sliderHandleWidth / 2,
right: constraints.biggest.width -
offsetB.dx -
2 -
widget.sliderHandleWidth / 2,
height: 8,
child: GestureDetector(
onPanStart: (DragStartDetails details) {
selectedRange = SelectedRange.translation;
},
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
decoration: BoxDecoration(
color: appearanceState ==
AppearanceState.statePointerUp
? darkBlue.withOpacity(0.2)
: darkBlue.withOpacity(0.5),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(3),
topRight: Radius.circular(3))),
child: const Icon(
Icons.view_week_rounded,
color: Colors.white,
size: 8.0,
),
),
)),
Positioned(
top: 8,
left: 0,
right: 0,
bottom: 0,
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: darkBlue.withOpacity(0.2), width: 1.5),
borderRadius: BorderRadius.circular(5)),
height: 50,
child: Stack(
children: [
Positioned(
top: 0,
left: offsetA.dx + widget.sliderHandleWidth / 2,
right: constraints.biggest.width -
offsetB.dx -
3 -
widget.sliderHandleWidth / 2,
bottom: 0,
child: GestureDetector(
onPanStart: (DragStartDetails details) {
selectedRange = SelectedRange.translation;
},
child: Container(
color: darkBlue.withOpacity(0.15),
),
),
),
Positioned(
top: offsetA.dy,
left: offsetA.dx,
bottom: 0,
child: GestureDetector(
onPanStart: (DragStartDetails details) {
selectedRange = SelectedRange.leftA;
},
child: SliderHandleWidget(
width: widget.sliderHandleWidth,
),
)),
Positioned(
top: offsetB.dy,
left: offsetB.dx,
bottom: 0,
child: GestureDetector(
onPanStart: (DragStartDetails details) {
selectedRange = SelectedRange.rightB;
},
child: SliderHandleWidget(
width: widget.sliderHandleWidth,
),
)),
],
),
),
),
],
),
),
);
},
);
}
void calculatePercentageRange(BoxConstraints constraints) {
// print('_DateRangeSliderState.calculatePercentageRangeoffsetA.dx ${(offsetB.dx + chart_with_time_range.sliderHandleWidth / 2)} ${constraints.biggest.width}');
///GetX 记录百分比
double percentageA =
(offsetA.dx + widget.sliderHandleWidth / 2) / constraints.biggest.width;
double percentageB =
(offsetB.dx + widget.sliderHandleWidth / 2) / constraints.biggest.width;
if (percentageA > 1) {
percentageA = 1;
}
if (percentageA < 0) {
percentageA = 0;
}
if (percentageB > 1) {
percentageB = 1;
}
if (percentageB < 0) {
percentageB = 0;
}
if (widget.valueChangedCallback != null) {
widget.valueChangedCallback!(percentageA, percentageB);
}
}
}
class SliderHandleWidget extends StatefulWidget {
final double width;
const SliderHandleWidget({
this.width = 30,
Key? key,
}) : super(key: key);
@override
State<SliderHandleWidget> createState() => _SliderHandleWidgetState();
}
class _SliderHandleWidgetState extends State<SliderHandleWidget> {
Color unselectedColor = Colors.indigo.withOpacity(0.6);
Color selectedColor = Colors.blueAccent;
Color currentColor = Colors.white;
@override
void initState() {
// TODO: implement initState
super.initState();
currentColor = unselectedColor;
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Listener(
onPointerUp: (PointerUpEvent event) {
setState(() {
currentColor = unselectedColor;
});
},
onPointerDown: (PointerDownEvent event) {
setState(() {
currentColor = selectedColor;
});
},
child: AnimatedContainer(
duration: Duration(milliseconds: 500),
child: Column(
children: [
Container(
height: constraints.biggest.height * 0.15,
width: 1.5,
color: currentColor,
),
Container(
width: 10,
height: constraints.biggest.height * 0.7,
decoration: BoxDecoration(
border: Border.all(color: currentColor, width: 1.5),
borderRadius: BorderRadius.circular(3),
color: Colors.white),
),
Container(
height: constraints.biggest.height * 0.15,
width: 1.5,
color: currentColor,
)
],
),
color: Colors.transparent,
width: widget.width,
height: 50,
),
);
});
}
}
在测试页面调用 【DateRangeSlider】
import 'package:flutter/material.dart';
import 'date_range_slider.dart';
class TestPage extends StatefulWidget {
const TestPage({Key? key}) : super(key: key);
@override
State<TestPage> createState() => _TestPageState();
}
class _TestPageState extends State<TestPage> {
num beginPercentage = 0;
num endPercentage = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: DefaultTextStyle(
style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.bold, color: Colors.red),
child: Center(
child: SizedBox(
height: 120,
width: MediaQuery.of(context).size.width - 100,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Begin: " + beginPercentage.toStringAsFixed(2)),
Text("End: " + endPercentage.toStringAsFixed(2)),
DateRangeSlider(
valueChangedCallback: (a, b) {
setState(() {
beginPercentage = a * 100;
endPercentage = b * 100;
print('_TestPageState.build a:$a,b:$b');
});
},
),
],
),
),
),
));
}
}
网友评论