如何用Flutter 做一个RangeSlider范围选择选择器1

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 {
  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 createState() => _SliderHandleWidgetState();
}

class _SliderHandleWidgetState extends State {
  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 createState() => _TestPageState();
}

class _TestPageState extends State {
  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');
                  });
                },
              ),
            ],
          ),
        ),
      ),
    ));
  }
}

你可能感兴趣的:(如何用Flutter 做一个RangeSlider范围选择选择器1)