Flutter自定义可滑动尺子
2020-11-25 本文已影响0人
旺仔_100
一、先上一个效果图
Ruler-flutter.png
二、说明一下
很简单的一个自定义view:就是绘制一个小三角,一顿计算画线,绘制数字。可向左滑动尺子回调小三角指定的当前值。
三、具体实现思路说明
- 先讲一下静态的尺子绘制
1.绘制三角形,使用path,不熟悉path的同学可以先学习下path。因为绘制起点是原点,为了给三角形一点空间,可以移动一下画布。
canvas.translate(widget.leftPadding - widget.sanWidth / 2, 3);
三角形其实也很好绘制,就是等边三角形的三个点,依次relativeLineTo进行连接。
Path path = Path();
path
..moveTo(0, 0)
..relativeLineTo(widget.sanWidth, 0)
..relativeLineTo(-widget.sanWidth / 2, sqrt(3) * widget.sanWidth / 2)
..close();
canvas.drawPath(path, Paint()..color = Colors.purple);
2.绘制尺子,其实就是 canvas.drawLine(),计算一下在那个位置需要划线,依次划线。每10线的时候需要用粗的画笔,然后还需要在线的下面画文字。每5个线的时候需要用第二短的线 画线。其他的都用最短最细的线 划线。
for (int i = 0; i <= widget.maxScale; i++) {
if (i % 10 == 0) {
canvas.drawLine(
Offset(i * 5.0, 0),
Offset(i * 5.0, widget.line3Height),
Paint()
..color = widget.lineColor
..strokeWidth = widget.lineWidth);
drawText(i, canvas);
} else if (i % 5 == 0) {
canvas.drawLine(
Offset(i * 5.0, 0),
Offset(i * 5.0, widget.line2Height),
Paint()
..color = widget.lineColor
..strokeWidth = widget.lineWidth);
} else {
canvas.drawLine(
Offset(i * 5.0, 0),
Offset(i * 5.0, widget.line1Height),
Paint()
..color = widget.lineColor
..strokeWidth = widget.line1Width);
}
}
3.绘制文字,flutter的绘制文字比Android稍微复杂一点点,但是可配置性也更加好一点。
void drawText(int i, Canvas canvas) {
var textPainter = TextPainter(
text: TextSpan(
text: "$i", style: TextStyle(fontSize: 12, color: Colors.black)),
textDirection: TextDirection.ltr,
textAlign: TextAlign.left);
textPainter.layout();
textPainter.paint(
canvas, Offset(i * 5.0 - 2, widget.line3Height + widget.paddingText));
}
到此为止,静态的尺子就已经绘制完成。下面说一下,尺子的滚动,以及对应的值回调。
4.GestureDetector是处理事件的widget,它有一个回调onPanEnd,里面可以拿到DragEndDetails 这个可以拿到很多东西,例如当前点击位置的x,y轴信息,滑动的增量值等。我们使用的就是增量值,就是本次滑动多远DragUpdateDetails.delta.dx。当然我们需要对增量值进行处理,例如不允许右滑,或者往左边只允许滑动80%等处理。然后把处理后的值给 ValueNotifier<double>,它可以触发自定义的widget重新绘制。
class RulerState extends State<RulerWidget> {
ValueNotifier<double> _dx = ValueNotifier(0.0);
@override
Widget build(BuildContext context) {
return Container(
child: GestureDetector(
onPanUpdate: parse,
child: CustomPaint(
size: Size(
widget.maxScale * widget.unit +
widget.leftPadding * 2 +
widget.maxScale * widget.lineWidth,
widget.rulerHeight),
painter: RulerCustomPainter(widget, _dx),
),
),
);
}
double dx = 0;
parse(DragUpdateDetails details) {
dx += details.delta.dx;
if (dx > 0) {
dx = 0;
}
if (dx < -80 * widget.unit) {
dx = -80.0 * widget.unit;
}
_dx.value = dx;
if (widget.onChanged != null) {
widget.onChanged(dx);
}
}
}
上面的onChanged会回调对应的像素,稍微处理就是当前的刻度值。
滑动的原理实际上就是把画布向右边移动,关键代码
///重新绘制的时候
canvas.translate(dx.value, 0);
在自定义的widget中,一定记得把ValueNotifier<double> dx;传递给父类,否则不会重绘
RulerCustomPainter(this.widget, this.dx) : super(repaint: dx);
到此为止就完了,其实还是很简单的。附上完整代码。
class GestureDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "gestureDemo",
home: Scaffold(
appBar: AppBar(),
body: RulerWidget(onChanged: (double dx) {
// print("$dx");
print("当前刻度值是${dx / 5}");
}),
),
);
}
}
///绘制一个尺子
///
class RulerWidget extends StatefulWidget {
///尺子距离左边的距离
double leftPadding;
///尺子的线宽
double lineWidth;
double line1Width;
///尺子的线的高度
double line1Height;
///尺子第二高度
double line2Height;
///尺子的第三高度
double line3Height;
Color lineColor;
Color indicationColor;
///文字样式
TextStyle style;
///尺子的最大刻度
double maxScale;
/// 5个dp对应一个刻度
int unit;
///尺子的高度
double rulerHeight;
///刻度和尺子的距离
double paddingText;
///等边三角形的宽度
double sanWidth;
final void Function(double) onChanged;
RulerWidget(
{this.leftPadding = 5,
this.lineWidth = 2,
this.line1Width = 1,
this.line1Height = 10,
this.line2Height = 15,
this.line3Height = 20,
this.lineColor = Colors.blue,
this.indicationColor = Colors.purple,
this.maxScale = 100.0,
this.unit = 5,
this.rulerHeight = 50,
this.paddingText = 5,
this.sanWidth = 5,
@required this.onChanged,
this.style});
@override
State<StatefulWidget> createState() {
return RulerState();
}
}
class RulerState extends State<RulerWidget> {
ValueNotifier<double> _dx = ValueNotifier(0.0);
@override
Widget build(BuildContext context) {
return Container(
child: GestureDetector(
onPanUpdate: parse,
child: CustomPaint(
size: Size(
widget.maxScale * widget.unit +
widget.leftPadding * 2 +
widget.maxScale * widget.lineWidth,
widget.rulerHeight),
painter: RulerCustomPainter(widget, _dx),
),
),
);
}
double dx = 0;
parse(DragUpdateDetails details) {
dx += details.delta.dx;
if (dx > 0) {
dx = 0;
}
if (dx < -80 * widget.unit) {
dx = -80.0 * widget.unit;
}
_dx.value = dx;
if (widget.onChanged != null) {
widget.onChanged(dx);
}
}
}
class RulerCustomPainter extends CustomPainter {
RulerWidget widget;
ValueNotifier<double> dx;
RulerCustomPainter(this.widget, this.dx) : super(repaint: dx);
@override
void paint(Canvas canvas, Size size) {
/// 当刻度是10的倍数的时候绘制最长刻度,并在刻度下面绘制文字
/// 当刻度是5的倍数的时候绘制绘制第二长刻度
/// 其他情况绘制一般的刻度
canvas.clipRect(Offset.zero & size);
canvas.save();
canvas.translate(widget.leftPadding - widget.sanWidth / 2, 3);
Path path = Path();
path
..moveTo(0, 0)
..relativeLineTo(widget.sanWidth, 0)
..relativeLineTo(-widget.sanWidth / 2, sqrt(3) * widget.sanWidth / 2)
..close();
canvas.drawPath(path, Paint()..color = Colors.purple);
canvas.restore();
canvas.translate(widget.leftPadding, widget.leftPadding + 3);
///重新绘制的时候
canvas.translate(dx.value, 0);
for (int i = 0; i <= widget.maxScale; i++) {
if (i % 10 == 0) {
canvas.drawLine(
Offset(i * 5.0, 0),
Offset(i * 5.0, widget.line3Height),
Paint()
..color = widget.lineColor
..strokeWidth = widget.lineWidth);
drawText(i, canvas);
} else if (i % 5 == 0) {
canvas.drawLine(
Offset(i * 5.0, 0),
Offset(i * 5.0, widget.line2Height),
Paint()
..color = widget.lineColor
..strokeWidth = widget.lineWidth);
} else {
canvas.drawLine(
Offset(i * 5.0, 0),
Offset(i * 5.0, widget.line1Height),
Paint()
..color = widget.lineColor
..strokeWidth = widget.line1Width);
}
}
}
void drawText(int i, Canvas canvas) {
var textPainter = TextPainter(
text: TextSpan(
text: "$i", style: TextStyle(fontSize: 12, color: Colors.black)),
textDirection: TextDirection.ltr,
textAlign: TextAlign.left);
textPainter.layout();
textPainter.paint(
canvas, Offset(i * 5.0 - 2, widget.line3Height + widget.paddingText));
}
@override
bool shouldRepaint(RulerCustomPainter oldDelegate) {
return dx != oldDelegate.dx;
}
}