- 原文地址:How to build a circular slider in Flutter
- 原文做者:David Anaya
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:DevMcryYu
- 校對者:MollyAredtana,JasonLinkinBright
你是否也曾想要經過爲滑塊添加雙重滑塊或修改其佈局來讓它看起來不那麼無聊?html
在這篇文章中我會展現如何經過整合 GestureDetector 以及 Canvas 來在 Flutter 中構建一個圓形滑塊。前端
若是你對構建它的過程不感興趣,僅僅是爲了獲取此部件並使用它,那麼你可使用我在 pub.dartlang.org/packages/fl… 發佈的程序包。android
大多數狀況下你並不會須要它。但想象一下:若是你想要用戶選定一個時間段,或者只是想要一個比直線形狀更有趣一點的常規滑塊的場景時,就可使用圓形滑塊。ios
咱們要準備的第一件事就是建立一個真正的滑塊。爲此,咱們要用一個完美的圓形做爲背景,在它的基礎上再畫一個根據用戶交互能夠動態顯示的圓。爲了實現咱們的想法,咱們將用到一個名爲 CustomPaint 的特殊部件,它提供一個容許讓咱們自由創做的畫布(Canvas)。git
當滑塊渲染完成之後,咱們但願用戶可以和它進行交互,所以咱們選擇使用 GestureDetector 封裝它來捕獲點擊及拖動事件。github
完整流程是:canvas
(只需關注上圖黃色部分)後端
咱們要作的第一件事就是畫兩個圓。一個靜態樣式(無需改變),另外一個則是動態的樣式(響應用戶交互),我使用兩個 Painter 來分別繪製它們。bash
兩個 Painter 都繼承自 CustomPainter —— 一個由 Flutter 提供並實現 paint()
及 shouldRepaint()
方法的類。第一個方法用來繪製咱們想要繪製的形狀,第二個方法在有變化時進行從新繪製的時候調用。對於 BasePainter 而言咱們永遠不會須要重繪,所以它的返回值老是 false。而對於 SliderPainter 來講它老是返回 true,由於每次更改都意味着用戶移動了滑塊,必須更新所選擇的項。ide
import 'package:flutter/material.dart';
class BasePainter extends CustomPainter {
Color baseColor;
Offset center;
double radius;
BasePainter({@required this.baseColor});
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = baseColor
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke
..strokeWidth = 12.0;
center = Offset(size.width / 2, size.height / 2);
radius = min(size.width / 2, size.height / 2);
canvas.drawCircle(center, radius, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
複製代碼
能夠看到,paint()
方法得到一個 Canvas 和一個 Size 參數。Canvas 提供一組方法可讓咱們繪製任何形狀:圓形、直線、圓弧、矩形等等。Size 參數便是畫布的尺寸,由畫布適配的部件尺寸決定。咱們還須要一個 Paint,容許咱們定製樣式、顏色以及其餘東西。
如今 BasePainter 的功能用法已經不言自明,然而 SliderPainter 卻有一點兒不尋常,如今咱們不只要繪製一個圓弧而非圓,還須要繪製 Handler。
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_circular_slider/src/utils.dart';
class SliderPainter extends CustomPainter {
double startAngle;
double endAngle;
double sweepAngle;
Color selectionColor;
Offset initHandler;
Offset endHandler;
Offset center;
double radius;
SliderPainter(
{@required this.startAngle,
@required this.endAngle,
@required this.sweepAngle,
@required this.selectionColor});
@override
void paint(Canvas canvas, Size size) {
if (startAngle == 0.0 && endAngle == 0.0) return;
Paint progress = _getPaint(color: selectionColor);
center = Offset(size.width / 2, size.height / 2);
radius = min(size.width / 2, size.height / 2);
canvas.drawArc(Rect.fromCircle(center: center, radius: radius),
-pi / 2 + startAngle, sweepAngle, false, progress);
Paint handler = _getPaint(color: selectionColor, style: PaintingStyle.fill);
Paint handlerOutter = _getPaint(color: selectionColor, width: 2.0);
// 繪製 handler
initHandler = radiansToCoordinates(center, -pi / 2 + startAngle, radius);
canvas.drawCircle(initHandler, 8.0, handler);
canvas.drawCircle(initHandler, 12.0, handlerOutter);
endHandler = radiansToCoordinates(center, -pi / 2 + endAngle, radius);
canvas.drawCircle(endHandler, 8.0, handler);
canvas.drawCircle(endHandler, 12.0, handlerOutter);
}
Paint _getPaint({@required Color color, double width, PaintingStyle style}) =>
Paint()
..color = color
..strokeCap = StrokeCap.round
..style = style ?? PaintingStyle.stroke
..strokeWidth = width ?? 12.0;
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
複製代碼
再一次地,咱們獲取了 center 和 radius 的值,但咱們此次繪製的是圓弧。SliderPainter 將根據用戶交互反饋的值做爲 start、end 和 sweap 屬性的值,以便於咱們根據這些參數來繪製圓弧。值得一提的是咱們須要從初始角度中減去 pi/2,由於咱們的滑塊的圓弧的起始位置是在圓形的正上方,而 drawArc()
方法使用 x 軸正軸做爲起始位置。
當咱們繪製好圓弧之後咱們就須要準備繪製 Handler 了。爲此,咱們將分別繪製兩個圓,一個在內部填充,一個在外部包裹。我調用了一些工具集函數用來將弧度轉換爲圓的座標。你能夠在 Github 倉庫內查閱這些函數。
目前來看,僅僅使用 CustomPaint 以及兩個 Painter 就已經足夠繪製想要的東西了。然而它們仍是不可以進行交互。所以就要使用 GestureDetector 來對它進行封裝。這樣一來咱們就能夠在畫布上對用戶事件作出相應處理。
一開始咱們將爲 Handler 賦初值,當獲取這些 Handler 的座標後,咱們將按照如下策略執行操做:
由於咱們須要分別計算出座標值、新的角度值再傳遞給 Handler 和 Painter,因此咱們的 CircularSliderPaint 必須是一個 StatefulWidget。
import 'package:flutter/material.dart';
import 'package:flutter_circular_slider/src/base_painter.dart';
import 'package:flutter_circular_slider/src/slider_painter.dart';
import 'package:flutter_circular_slider/src/utils.dart';
class CircularSliderPaint extends StatefulWidget {
final int init;
final int end;
final int intervals;
final Function onSelectionChange;
final Color baseColor;
final Color selectionColor;
final Widget child;
CircularSliderPaint(
{@required this.intervals,
@required this.init,
@required this.end,
this.child,
@required this.onSelectionChange,
@required this.baseColor,
@required this.selectionColor});
@override
_CircularSliderState createState() => _CircularSliderState();
}
class _CircularSliderState extends State<CircularSliderPaint> {
bool _isInitHandlerSelected = false;
bool _isEndHandlerSelected = false;
SliderPainter _painter;
/// 用弧度製表示的起始角度,用來肯定 init Handler 的位置。
double _startAngle;
/// 用弧度製表示的結束角度,用來肯定 end Handler 的位置。
double _endAngle;
/// 用弧度製表示的選擇區間的絕對角度(夾角)
double _sweepAngle;
@override
void initState() {
super.initState();
_calculatePaintData();
}
// 咱們須要使用 gesture detector 來更新此部件,
// 當父部件重建本身時也是如此。
@override
void didUpdateWidget(CircularSliderPaint oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.init != widget.init || oldWidget.end != widget.end) {
_calculatePaintData();
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onPanDown: _onPanDown,
onPanUpdate: _onPanUpdate,
onPanEnd: _onPanEnd,
child: CustomPaint(
painter: BasePainter(
baseColor: widget.baseColor,
selectionColor: widget.selectionColor),
foregroundPainter: _painter,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: widget.child,
),
),
);
}
void _calculatePaintData() {
double initPercent = valueToPercentage(widget.init, widget.intervals);
double endPercent = valueToPercentage(widget.end, widget.intervals);
double sweep = getSweepAngle(initPercent, endPercent);
_startAngle = percentageToRadians(initPercent);
_endAngle = percentageToRadians(endPercent);
_sweepAngle = percentageToRadians(sweep.abs());
_painter = SliderPainter(
startAngle: _startAngle,
endAngle: _endAngle,
sweepAngle: _sweepAngle,
selectionColor: widget.selectionColor,
);
}
_onPanUpdate(DragUpdateDetails details) {
if (!_isInitHandlerSelected && !_isEndHandlerSelected) {
return;
}
if (_painter.center == null) {
return;
}
RenderBox renderBox = context.findRenderObject();
var position = renderBox.globalToLocal(details.globalPosition);
var angle = coordinatesToRadians(_painter.center, position);
var percentage = radiansToPercentage(angle);
var newValue = percentageToValue(percentage, widget.intervals);
if (_isInitHandlerSelected) {
widget.onSelectionChange(newValue, widget.end);
} else {
widget.onSelectionChange(widget.init, newValue);
}
}
_onPanEnd(_) {
_isInitHandlerSelected = false;
_isEndHandlerSelected = false;
}
_onPanDown(DragDownDetails details) {
if (_painter == null) {
return;
}
RenderBox renderBox = context.findRenderObject();
var position = renderBox.globalToLocal(details.globalPosition);
if (position != null) {
_isInitHandlerSelected = isPointInsideCircle(
position, _painter.initHandler, 12.0);
if (!_isInitHandlerSelected) {
_isEndHandlerSelected = isPointInsideCircle(
position, _painter.endHandler, 12.0);
}
}
}
}
複製代碼
這裏有幾點須要注意:
onSelectionChange()
的緣由。didUpdateWidget()
方法。RenderBox.globalToLocal()
方法來對它們進行轉換。該方法使用部件的 Context 做爲參考。有了這些,咱們也就擁有了打造圓形滑塊的一切須要。
因爲篇幅有限,在這裏並無展開講解全部的細節。你能夠查看本項目的倉庫,我會樂於回答評論中的任何問題。
在最終的版本里我添加了一些額外的功能,好比自定義選擇區間和 Handler 的顏色;若是你想實現相似時鐘的樣式(小時和分鐘)你能夠根據需求進行選擇。爲了方便各位使用,我一樣將全部內容打包放進了一個最終的部件內。
你也能夠經過從 pub.dartlang.org/packages/fl… 導入本庫的方式來使用這個部件。
文章至此告一段落,感謝各位的閱讀!
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。