一、先上一個效果圖

Ruler-flutter.png
二、說明一下
很簡單的一個自定義view:就是繪制一個小三角,一頓計算畫線,繪制數(shù)字??上蜃蠡瑒映咦踊卣{小三角指定的當前值。
三、具體實現(xiàn)思路說明
- 先講一下靜態(tài)的尺子繪制
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));
}
到此為止,靜態(tài)的尺子就已經繪制完成。下面說一下,尺子的滾動,以及對應的值回調。
4.GestureDetector是處理事件的widget,它有一個回調onPanEnd,里面可以拿到DragEndDetails 這個可以拿到很多東西,例如當前點擊位置的x,y軸信息,滑動的增量值等。我們使用的就是增量值,就是本次滑動多遠DragUpdateDetails.delta.dx。當然我們需要對增量值進行處理,例如不允許右滑,或者往左邊只允許滑動80%等處理。然后把處理后的值給 ValueNotifier<double>,它可以觸發(fā)自定義的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的倍數(shù)的時候繪制最長刻度,并在刻度下面繪制文字
/// 當刻度是5的倍數(shù)的時候繪制繪制第二長刻度
/// 其他情況繪制一般的刻度
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;
}
}