美文网首页
【开源 UI 组件】Flutter 图表范围选择器

【开源 UI 组件】Flutter 图表范围选择器

作者: super可乐 | 来源:发表于2022-08-25 11:45 被阅读0次

    前言

    最近有一个小需求:图表支持局部显示,如下底部的区域选择器支持

    • 左右拖动调节中间区域
    • 拖拽中间区域,可以进行移动
    • 图表数据根据中间区域的占比进行显示部分数据

    这样当图表的数据量过大,不宜全部展示时,可选择的局部展示就是个不错的解决方案。由于一般的图表库没有提供该功能,这里自己通过绘制来实现以下,操作效果如下所示:


    1. 使用 chart_range_selector

    目前这个范围选择器已经发布到 pub 上了,名字是 chart_range_selector。大家可以通过依赖进行添加

    dependencies:
      chart_range_selector: ^1.0.0
    

    这个库本身是作为独立 UI 组件存在的,在拖拽过程中改变区域范围时,会触发回调。使用者可以通过监听来获取当前区域的范围。这里的区域起止是以分率的形式给出的,也就是最左侧是 0 最右侧是 1 。如下的区域范围是 0.26 ~ 0.72

    ChartRangeSelector(
      height: 30,
      initStart: 0.4,
      initEnd: 0.6,
      onChartRangeChange: _onChartRangeChange,
    ),
    
    void _onChartRangeChange(double start, double end) {
      print("start:$start, end:$end");
    }
    

    封装的组件名为: ChartRangeSelector ,提供了如下的一些配置参数:

    配置项 类型 简述
    initStart double 范围启始值 0~1
    initEnd double 范围终止值 0~1
    height double 高度值
    onChartRangeChange OnChartRangeChange 范围变化回调
    bgStorkColor Color 背景线条颜色
    bgFillColor Color 背景填充颜色
    rangeColor Color 区域颜色
    rangeActiveColor Color 区域激活颜色
    dragBoxColor Color 左右拖拽块颜色
    dragBoxActiveColor Color 左右拖拽块激活颜色

    2. ChartRangeSelector 实现思路分析

    这个组件整体上是通过 ChartRangeSelectorPainter 绘制出来的,其实这些图形都是挺规整的,绘制来说并不是什么难事。重点在于事件的处理,拖拽不同的部位需要处理不同的逻辑,还涉及对拖拽部位的校验、高亮示意,对这块的整合还是需要一定的功力的。

    代码中通过 RangeData 可监听对象为绘制提供必要的数据,其中 minGap 用于控制范围的最小值,保证范围不会过小。另外定义了 OperationType 枚举表示操作,其中有四个元素,none 表示没有拖拽的普通状态;dragHead 表示拖动起始块,dragTail 表示拖动终止块,dragZone 表示拖动范围区域。

    enum OperationType{
      none,
      dragHead,
      dragTail,
      dragZone
    }
    
    class RangeData extends ChangeNotifier {
      double start;
      double end;
      double minGap;
      OperationType operationType=OperationType.none;
    
      RangeData({this.start = 0, this.end = 1,this.minGap=0.1});
    
      //暂略相关方法...
    }
    

    在组件构建中,通过 LayoutBuilder 获取组件的约束信息,从而获得约束区域宽度最大值,也就是说组件区域的宽度值由使用者自行约束,该组件并不强制指定。使用 SizedBox 限定画板的高度,通过 CustomPaint 组件使用 ChartRangeSelectorPainter 进行绘制。使用 GestureDetector 组件进行手势交互监听,这就是该组件整体上实现的思路。

    3.核心代码实现分析

    可以看出,这个组件的核心就是 绘制 + 手势交互 。其中绘制比较简单,就是根据 RangeData 数据和颜色配置画些方块而已,稍微困难一点的是对左右控制柄位置的计算。另外,三个可拖拽物的激活状态是通过 RangeData#operationType 进行判断的。

    也就是说所有问题的焦点都集中在 手势交互 中对 RangeData 数据的更新。如下是处理按下的逻辑,当触电横坐标左右 10 逻辑像素之内,表示激活头部。如下 tag1 处通过 dragHead 方法更新 operationType 并触发通知,这样画板绘制时就会激活头部块,右侧和中间的激活同理。

    ---->[RangeData#dragHead]----
    void dragHead(){
      operationType=OperationType.dragHead;
      notifyListeners();
    }
    
    void _onPanDown(DragDownDetails details, double width) {
      double start = width * rangeData.start;
      double x = details.localPosition.dx;
      double end = width * rangeData.end;
      if (x >= start - 10 && x <= end + 10) {
        if ((start - details.localPosition.dx).abs() < 10) {
          rangeData.dragHead(); // tag1
          return;
        }
        if ((end - details.localPosition.dx).abs() < 10) {
          rangeData.dragTail();
          return;
        }
        rangeData.dragZone();
      }
    }
    

    对于拖手势的处理,是比较复杂的。如下根据 operationType 进行不同的逻辑处理,比如当 dragHead 时,触发 RangeData#moveHead 方法移动 start 值。这里将具体地逻辑封装在 RangeData 类中。可以使代码更加简洁明了,每个操作都有 bool 返回值用于校验区域也没有发生变化,比如拖拽到 0 时,继续拖拽是会触发事件的,此时返回 false,避免无意义的 onChartRangeChange 回调触发。

    void _onUpdate(DragUpdateDetails details, double width) {
      bool changed = false;
      if (rangeData.operationType == OperationType.dragHead) {
        changed = rangeData.moveHead(details.delta.dx / width);
      }
      if (rangeData.operationType == OperationType.dragTail) {
        changed = rangeData.moveTail(details.delta.dx / width);
      }
      if (rangeData.operationType == OperationType.dragZone) {
        changed = rangeData.move(details.delta.dx / width);
      }
      if (changed) widget.onChartRangeChange.call(rangeData.start, rangeData.end);
    }
    

    如下是 RangeData#moveHead 的处理逻辑,_recordStart 用于记录起始值,如果移动后未改变,返回 false。表示不执行通知和触发回调。

    ---->[RangeData#moveHead]----
    bool moveHead(double ds) {
      start += ds;
      start = start.clamp(0, end - minGap);
      if (start == _recordStart) return false;
      _recordStart = start;
      notifyListeners();
      return true;
    }
    

    4. 结合图表使用

    下面是结合 charts_flutter 图标库实现的范围显示案例。其中核心点是 domainAxis 可以通过 NumericAxisSpec 来显示某个范围的数据,而 ChartRangeSelector 提供拽的交互操作来更新这个范围,可谓相辅相成。

    class RangeChartDemo extends StatefulWidget {
      const RangeChartDemo({Key? key}) : super(key: key);
    
      @override
      State<RangeChartDemo> createState() => _RangeChartDemoState();
    }
    
    class _RangeChartDemoState extends State<RangeChartDemo> {
      List<ChartData> data = [];
    
      int start = 0;
      int end = 0;
    
      @override
      void initState() {
        super.initState();
        data = randomDayData(count: 96);
        start = 0;
        end = (0.8 * data.length).toInt();
      }
    
      Random random = Random();
    
      List<ChartData> randomDayData({int count = 1440}) {
        return List.generate(count, (index) {
          int value = 50 + random.nextInt(200);
          return ChartData(index, value);
        });
      }
    
      @override
      Widget build(BuildContext context) {
    
        List<charts.Series<ChartData, int>> seriesList = [
          charts.Series<ChartData, int>(
            id: 'something',
            colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault,
            domainFn: (ChartData sales, _) => sales.index,
            measureFn: (ChartData sales, _) => sales.value,
            data: data,
          )
        ];
    
        return Column(
          children: [
            Expanded(
              child: charts.LineChart(seriesList,
                  animate: false,
                  primaryMeasureAxis: const charts.NumericAxisSpec(
                      tickProviderSpec: charts.BasicNumericTickProviderSpec(desiredTickCount: 5),),
                  domainAxis: charts.NumericAxisSpec(
                    viewport: charts.NumericExtents(start, end),
                  )),
            ),
            const SizedBox(
              height: 10,
            ),
            SizedBox(
              width: 400,
              child: ChartRangeSelector(
                  height: 30,
                  initEnd: 0.5,
                  initStart: 0.3,
                  onChartRangeChange: (start, end) {
                    this.start = (start * data.length).toInt();
                    this.end = (end * data.length).toInt();
                    setState(() {});
                  }),
            ),
          ],
        );
      }
    }
    
    class ChartData {
      final int index;
      final int value;
    
      ChartData(this.index, this.value);
    }
    

    本文就介绍到这里,更多的实现细节感兴趣的可以研究一下源码。谢谢观看 ~

    作者:张风捷特烈
    链接:https://juejin.cn/post/7134885139980484615

    相关文章

      网友评论

          本文标题:【开源 UI 组件】Flutter 图表范围选择器

          本文链接:https://www.haomeiwen.com/subject/xrwagrtx.html