UI 開源組件Flutter圖表范圍選擇器使用詳解
前言
最近有一個(gè)小需求:圖表支持局部顯示,如下底部的區(qū)域選擇器支持
- 左右拖動(dòng)調(diào)節(jié)中間區(qū)域
- 拖拽中間區(qū)域,可以進(jìn)行移動(dòng)
- 圖表數(shù)據(jù)根據(jù)中間區(qū)域的占比進(jìn)行顯示部分?jǐn)?shù)據(jù)
這樣當(dāng)圖表的數(shù)據(jù)量過(guò)大,不宜全部展示時(shí),可選擇的局部展示就是個(gè)不錯(cuò)的解決方案。由于一般的圖表庫(kù)沒有提供該功能,這里自己通過(guò)繪制來(lái)實(shí)現(xiàn)以下,操作效果如下所示:
1. 使用 chart_range_selector
目前這個(gè)范圍選擇器已經(jīng)發(fā)布到 pub
上了,名字是 chart_range_selector。大家可以通過(guò)依賴進(jìn)行添加
dependencies: chart_range_selector: ^1.0.0
這個(gè)庫(kù)本身是作為獨(dú)立 UI
組件存在的,在拖拽過(guò)程中改變區(qū)域范圍時(shí),會(huì)觸發(fā)回調(diào)。使用者可以通過(guò)監(jiān)聽來(lái)獲取當(dāng)前區(qū)域的范圍。這里的區(qū)域起止是以分率的形式給出的,也就是最左側(cè)是 0
最右側(cè)是 1
。如下的區(qū)域范圍是 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
,提供了如下的一些配置參數(shù):
配置項(xiàng) | 類型 | 簡(jiǎn)述 |
---|---|---|
initStart | double | 范圍啟始值 0~1 |
initEnd | double | 范圍終止值 0~1 |
height | double | 高度值 |
onChartRangeChange | OnChartRangeChange | 范圍變化回調(diào) |
bgStorkColor | Color | 背景線條顏色 |
bgFillColor | Color | 背景填充顏色 |
rangeColor | Color | 區(qū)域顏色 |
rangeActiveColor | Color | 區(qū)域激活顏色 |
dragBoxColor | Color | 左右拖拽塊顏色 |
dragBoxActiveColor | Color | 左右拖拽塊激活顏色 |
2. ChartRangeSelector 實(shí)現(xiàn)思路分析
這個(gè)組件整體上是通過(guò) ChartRangeSelectorPainter
繪制出來(lái)的,其實(shí)這些圖形都是挺規(guī)整的,繪制來(lái)說(shuō)并不是什么難事。
重點(diǎn)在于事件的處理,拖拽不同的部位需要處理不同的邏輯,還涉及對(duì)拖拽部位的校驗(yàn)、高亮示意,對(duì)這塊的整合還是需要一定的功力的。
代碼中通過(guò) RangeData
可監(jiān)聽對(duì)象為繪制提供必要的數(shù)據(jù),其中 minGap
用于控制范圍的最小值,保證范圍不會(huì)過(guò)小。
另外定義了 OperationType
枚舉表示操作,其中有四個(gè)元素,none
表示沒有拖拽的普通狀態(tài);
dragHead
表示拖動(dòng)起始?jí)K,dragTail
表示拖動(dòng)終止塊,dragZone
表示拖動(dòng)范圍區(qū)域。
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}); //暫略相關(guān)方法... }
在組件構(gòu)建中,通過(guò) LayoutBuilder
獲取組件的約束信息,從而獲得約束區(qū)域?qū)挾茸畲笾?,也就是說(shuō)組件區(qū)域的寬度值由使用者自行約束,該組件并不強(qiáng)制指定。
使用 SizedBox
限定畫板的高度,通過(guò) CustomPaint
組件使用 ChartRangeSelectorPainter
進(jìn)行繪制。
使用 GestureDetector
組件進(jìn)行手勢(shì)交互監(jiān)聽,這就是該組件整體上實(shí)現(xiàn)的思路。
3.核心代碼實(shí)現(xiàn)分析
可以看出,這個(gè)組件的核心就是 繪制
+ 手勢(shì)交互
。其中繪制比較簡(jiǎn)單,就是根據(jù) RangeData
數(shù)據(jù)和顏色配置畫些方塊而已,稍微困難一點(diǎn)的是對(duì)左右控制柄位置的計(jì)算。
另外,三個(gè)可拖拽物的激活狀態(tài)是通過(guò) RangeData#operationType
進(jìn)行判斷的。
也就是說(shuō)所有問(wèn)題的焦點(diǎn)都集中在 手勢(shì)交互
中對(duì) RangeData
數(shù)據(jù)的更新。如下是處理按下的邏輯,當(dāng)觸電橫坐標(biāo)左右 10
邏輯像素之內(nèi),表示激活頭部。
如下 tag1
處通過(guò) dragHead
方法更新 operationType
并觸發(fā)通知,這樣畫板繪制時(shí)就會(huì)激活頭部塊,右側(cè)和中間的激活同理。
---->[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(); } }
對(duì)于拖手勢(shì)的處理,是比較復(fù)雜的。如下根據(jù) operationType
進(jìn)行不同的邏輯處理,比如當(dāng) dragHead
時(shí),觸發(fā) RangeData#moveHead
方法移動(dòng) start
值。這里將具體地邏輯封裝在 RangeData
類中。
可以使代碼更加簡(jiǎn)潔明了,每個(gè)操作都有 bool
返回值用于校驗(yàn)區(qū)域也沒有發(fā)生變化,比如拖拽到 0
時(shí),繼續(xù)拖拽是會(huì)觸發(fā)事件的,此時(shí)返回 false
,避免無(wú)意義的 onChartRangeChange
回調(diào)觸發(fā)。
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
用于記錄起始值,如果移動(dòng)后未改變,返回 false
。表示不執(zhí)行通知和觸發(fā)回調(diào)。
---->[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. 結(jié)合圖表使用
下面是結(jié)合 charts_flutter
圖標(biāo)庫(kù)實(shí)現(xiàn)的范圍顯示案例。其中核心點(diǎn)是 domainAxis
可以通過(guò) NumericAxisSpec
來(lái)顯示某個(gè)范圍的數(shù)據(jù),而 ChartRangeSelector
提供拽的交互操作來(lái)更新這個(gè)范圍,可謂相輔相成。
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圖表范圍選擇器使用詳解的詳細(xì)內(nèi)容,更多關(guān)于Flutter圖表范圍選擇器的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android實(shí)現(xiàn)個(gè)性化的進(jìn)度條
這篇文章主要介紹了Android實(shí)現(xiàn)個(gè)性化的進(jìn)度條 的相關(guān)資料,需要的朋友可以參考下2016-07-07Android ViewPager與radiogroup實(shí)現(xiàn)關(guān)聯(lián)示例
本篇文章主要介紹了Android ViewPager與radiogroup實(shí)現(xiàn)關(guān)聯(lián)示例,具有一定的參考價(jià)值,有興趣的可以了解一下。2017-03-03android實(shí)現(xiàn)http中請(qǐng)求訪問(wèn)添加cookie的方法
這篇文章主要介紹了android實(shí)現(xiàn)http中請(qǐng)求訪問(wèn)添加cookie的方法,實(shí)例分析了兩種添加cookie的技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-10-10Android開發(fā)實(shí)現(xiàn)圓形圖片功能示例
這篇文章主要介紹了Android開發(fā)實(shí)現(xiàn)圓形圖片功能,涉及Android實(shí)現(xiàn)圓形圖片的界面布局與CirImageView組件相關(guān)使用操作技巧,需要的朋友可以參考下2019-04-04Android TextSwitcher實(shí)現(xiàn)文字上下翻牌效果(銅板街)
這篇文章主要介紹了Android TextSwitcher實(shí)現(xiàn)文字上下翻牌效果(銅板街),需要的朋友可以參考下2017-05-05Android Compose Column列表不自動(dòng)刷新問(wèn)題
這篇文章主要介紹了Android Compose Column列表數(shù)據(jù)更新列表不刷新的問(wèn)題,總的來(lái)說(shuō)這并不是一道難題,那為什么要拿出這道題介紹?拿出這道題真正想要傳達(dá)的是解題的思路,以及不斷優(yōu)化探尋最優(yōu)解的過(guò)程。希望通過(guò)這道題能給你帶來(lái)一種解題優(yōu)化的思路2023-01-01詳解flutter之網(wǎng)絡(luò)請(qǐng)求dio,請(qǐng)求,攔截器簡(jiǎn)單示例
這篇文章主要介紹了詳解flutter之網(wǎng)絡(luò)請(qǐng)求dio,請(qǐng)求,攔截器簡(jiǎn)單示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06簡(jiǎn)單實(shí)現(xiàn)Android彈出菜單效果
這篇文章主要為大家詳細(xì)介紹了簡(jiǎn)單實(shí)現(xiàn)Android彈出菜單效果的相關(guān)代碼,感興趣的小伙伴們可以參考一下2016-06-06