- 原文地址: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、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。