UI 开源组件Flutter图表范围选择器使用详解

前言

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

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

UI 开源组件Flutter图表范围选择器使用详解_第1张图片

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

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 ,提供了如下的一些配置参数:

UI 开源组件Flutter图表范围选择器使用详解_第2张图片

配置项 类型 简述
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 组件进行手势交互监听,这就是该组件整体上实现的思路。

UI 开源组件Flutter图表范围选择器使用详解_第3张图片

3.核心代码实现分析

可以看出,这个组件的核心就是 绘制 + 手势交互 。其中绘制比较简单,就是根据 RangeData 数据和颜色配置画些方块而已,稍微困难一点的是对左右控制柄位置的计算。

另外,三个可拖拽物的激活状态是通过 RangeData#operationType 进行判断的。

UI 开源组件Flutter图表范围选择器使用详解_第4张图片

也就是说所有问题的焦点都集中在 手势交互 中对 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 createState() => _RangeChartDemoState();
}
class _RangeChartDemoState extends State {
  List 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 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> seriesList = [
      charts.Series(
        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);
}

以上就是UI 开源组件Flutter图表范围选择器使用详解的详细内容,更多关于Flutter图表范围选择器的资料请关注脚本之家其它相关文章!

你可能感兴趣的:(UI 开源组件Flutter图表范围选择器使用详解)