UI 開源組件Flutter圖表范圍選擇器使用詳解
前言
最近有一個小需求:圖表支持局部顯示,如下底部的區域選擇器支持
- 左右拖動調節中間區域
- 拖拽中間區域,可以進行移動
- 圖表數據根據中間區域的占比進行顯示部分數據
這樣當圖表的數據量過大,不宜全部展示時,可選擇的局部展示就是個不錯的解決方案。由於一般的圖表庫沒有提供該功能,這裡自己通過繪制來實現以下,操作效果如下所示:
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); }
以上就是UI 開源組件Flutter圖表范圍選擇器使用詳解的詳細內容,更多關於Flutter圖表范圍選擇器的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- canvas 2d 環形統計圖手寫實現示例
- 一文帶你分清C++的定義,聲明和初始化
- 一篇文章搞定echarts地圖輪播高亮
- C語言數據結構算法基礎之循環隊列示例
- Python實戰基礎之繪制餅狀圖分析商品庫存