前言
本文講解如何使用 Flutter (Google 開源的 UI 工具包,幫助開發者通過一套程式碼庫高效構建多平臺精美應用,支援移動、Web、桌面和嵌入式平臺) 繪製一個帶有動畫效果的柱狀圖表,最終效果如下圖。
要繪製這樣的圖表普通的 Widget 難以實現,這時就需要 CustomPaint
和 CustomPainter
出場了,它們類似於 Web 裡面的 <canvas>
元素,CustomPaint
提供了一個繪製區域,而 CustomPainter
擁有具體的繪製方法。
CustomPaint 和 CustomPainter
CustomPaint
是用來提供畫布的控制元件,它使用傳入畫筆 painter
在 child
控制元件後面繪製圖形, ,foregroundPainter
畫筆繪製在 child
控制元件之前。size
屬性控制畫布的大小,假如定義了子控制元件 child
,那麼畫布的大小將由子控制元件的大小決定,size
屬性被忽略。
class CustomPaint extends SingleChildRenderObjectWidget {
const CustomPaint({
Key key,
this.painter,
this.foregroundPainter,
this.size = Size.zero,
this.isComplex = false,
this.willChange = false,
Widget child,
})
}
複製程式碼
CustomPainter
是一個抽象類,是實現繪製圖形的控制元件,要在畫布上繪製圖形需要實現它的 paint
方法。paint
方法有兩個引數,Canvas canvas
和 Size size
。Size
物件表示畫布的尺寸,Canvas
物件上是具體的繪製圖形的方法。
abstract class CustomPainter extends Listenable {
void paint(Canvas canvas, Size size);
bool shouldRepaint(covariant CustomPainter oldDelegate);
}
複製程式碼
Canvas canvas
物件主要的繪製圖形方法有
方法名 | 引數 | 效果 |
---|---|---|
drawColor |
Color color , BlendMode blendMode |
繪製顏色到畫布上 |
drawLine |
Offset p1 , Offset p2 , Paint paint |
兩點之間畫線 |
drawPaint |
Paint paint |
使用 [Paint] 填充畫布 |
drawRect |
Rect rect , Paint paint |
繪製矩形 |
drawRRect |
RRect rrect , Paint paint |
繪製帶圓角的矩形 |
drawOval |
Rect rect , Paint paint |
繪製橢圓 |
drawCircle |
Offset c , double radius , Paint paint |
繪製圓形 |
drawArc |
Rect rect , double startAngle , double sweepAngle , bool useCenter , Paint paint |
繪製弧形 |
drawPath |
Path path , Paint paint |
繪製路徑 |
drawImage |
Image image , Offset p , Paint paint |
繪製影像 |
drawPoints |
PointMode pointMode , List<Offset> points , Paint paint |
繪製多個點 |
要將圖形繪製到畫布上需要先建立一個繼承至 CustomPainter
的自定義畫筆,例如繪製一個矩形需要實現一個繪製矩形的畫筆 RectanglePainter
,然後在畫布 CustomPaint
上應用它。
class RectanglePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// 定義一個矩形
final Rect rect = Rect.fromLTWH(50.0, 50.0, 100.0, 100.0);
// 指定繪製的樣式
final Paint paint = Paint()
..color = Colors.orange
..strokeWidth = 4.0
..style = PaintingStyle.stroke
..isAntiAlias = true;
// 使用 drawRect 繪製矩形
canvas.drawRect(rect, paint);
}
@override
bool shouldRepaint(RectanglePainter oldDelegate) => false;
}
class Rectangle extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: CustomPaint(
// 使用 RectanglePainter 在畫布上繪製
painter: RectanglePainter(),
child: Container(
width: 300,
height: 300,
decoration: BoxDecoration(
border: Border.all(
width: 1.0,
color: Colors.grey[300],
),
),
),
),
);
}
}
複製程式碼
效果如圖
繪製柱狀圖表
介紹完畢,下面開始繪製柱狀圖表,第一步建立 BarChart
控制元件代表柱狀圖,它有兩個構造引數一個是 data
用來接收圖表資料,以及 xAxis
表示圖表橫軸標識。
class BarChart extends StatefulWidget {
final List<double> data;
final List<String> xAxis;
const BarChart({
@required this.data,
@required this.xAxis,
});
@override
_BarChartState createState() => _BarChartState();
}
class _BarChartState extends State<BarChart> with TickerProviderStateMixin {
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CustomPaint(
painter: BarChartPainter(
datas: widget.data,
xAxis: widget.xAxis,
),
child: Container(width: 300, height: 300),
),
],
);
}
}
複製程式碼
然後建立一個用來繪製的自定義畫筆 BarChartPainter
。
class BarChartPainter extends CustomPainter {
final List<double> datas;
final List<String> xAxis;
BarChartPainter({
@required this.xAxis,
@required this.datas,
});
@override
void paint(Canvas canvas, Size size) {
// TODO
}
@override
bool shouldRepaint(BarChartPainter oldDelegate) => true;
@override
bool shouldRebuildSemantics(BarChartPainter oldDelegate) => false;
}
複製程式碼
繪製座標軸
在 BarChartPainter
上定義一個 _drawAxis
方法用於繪製橫座標軸,使用一個由左上,左下,右下三個點控制的 Path
路徑繪製。
void _drawAxis(Canvas canvas, Size size) {
final double sw = size.width;
final double sh = size.height;
// 使用 Paint 定義路徑的樣式
final Paint paint = Paint()
..color = Colors.black87
..style = PaintingStyle.stroke
..strokeWidth = 1.0;
// 使用 Path 定義繪製的路徑,從畫布的左上角到左下角在到右下角
final Path path = Path()
..moveTo(0, 0)
..lineTo(0, sh)
..lineTo(sw, sh);
// 使用 drawPath 方法繪製路徑
canvas.drawPath(path, paint);
}
@override
void paint(Canvas canvas, Size size) {
_drawAxis(canvas, size);
}
複製程式碼
效果如下
繪製標識
在 BarChartPainter
上定義一個 _drawLabels
方法繪製縱軸標識。
void _drawLabels(Canvas canvas, Size size) {
final double gap = 50.0;
final double sh = size.height;
final List<double> yAxisLabels = [];
Paint paint = Paint()
..color = Colors.black87
..strokeWidth = 2.0;
// 使用 50.0 為間隔繪製比傳入資料多一個的標識
for (int i = 0; i <= datas.length; i++) {
yAxisLabels.add(gap * i);
}
yAxisLabels.asMap().forEach(
(index, label) {
// 標識的高度為畫布高度減去標識的值
final double top = sh - label;
final rect = Rect.fromLTWH(0, top, 4, 1);
final Offset textOffset = Offset(
0 - labelFontSize * 3,
top - labelFontSize / 2,
);
// 繪製 Y 軸右邊的線條
canvas.drawRect(rect, paint);
// 繪製文字需要用 `TextPainter`,最後呼叫 paint 方法繪製文字
TextPainter(
text: TextSpan(
text: label.toStringAsFixed(0),
style: TextStyle(fontSize: labelFontSize, color: Colors.black87),
),
textAlign: TextAlign.right,
textDirection: TextDirection.ltr,
textWidthBasis: TextWidthBasis.longestLine,
)
..layout(minWidth: 0, maxWidth: 24)
..paint(canvas, textOffset);
},
);
}
@override
void paint(Canvas canvas, Size size) {
_drawAxis(canvas, size);
_drawLabels(canvas, size);
}
複製程式碼
效果如下
繪製資料矩形
然後定義一個 _darwBars
方法將具體矩形和橫軸標識繪製出來。
List<Color> colors = [
Color(0xff8e43e7),
Color(0xffff4f81),
Color(0xff1cc7d0),
Color(0xff00aeff),
Color(0xff3369e7),
Color(0xffb84592),
Color(0xff2dde98),
Color(0xffff6c5f),
Color(0xff003666),
Color(0xffffc168),
Color(0xff050f2c),
];
void _darwBars(Canvas canvas, Size size) {
final sh = size.height;
final paint = Paint()..style = PaintingStyle.fill;
for (int i = 0; i < datas.length; i++) {
// 每個矩形使用預設的 colors 陣列裡面的顏色
paint.color = colors[i];
final double textFontSize = 14.0;
final double data = datas[i];
// 矩形的上邊緣為畫布高度減去資料值
final double top = sh - data;
// 矩形的左邊緣為當前索引值乘以矩形寬度加上矩形之間的間距
final double left = i * _barWidth + (i * _barGap) + _barGap;
// 使用 Rect.fromLTWH 方法建立要繪製的矩形
final rect = Rect.fromLTWH(left, top, _barWidth, data);
// 使用 drawRect 方法繪製矩形
canvas.drawRect(rect, paint);
final offset = Offset(
left + _barWidth / 2 - textFontSize * 1.2,
top - textFontSize * 2,
);
// 使用 TextPainter 繪製矩形上放的數值
TextPainter(
text: TextSpan(
text: data.toStringAsFixed(1),
style: TextStyle(fontSize: textFontSize, color: paint.color),
),
textAlign: TextAlign.center,
textDirection: TextDirection.ltr,
)
..layout(
minWidth: 0,
maxWidth: textFontSize * data.toString().length,
)
..paint(canvas, offset);
final xData = xAxis[i];
final xOffset = Offset(left + _barWidth / 2 - textFontSize, sh + 12);
// 繪製橫軸標識
TextPainter(
textAlign: TextAlign.center,
text: TextSpan(
text: '$xData',
style: TextStyle(fontSize: 12, color: Colors.black87),
),
textDirection: TextDirection.ltr,
)
..layout(
minWidth: 0,
maxWidth: size.width,
)
..paint(canvas, xOffset);
}
}
@override
void paint(Canvas canvas, Size size) {
_drawAxis(canvas, size);
_drawLabels(canvas, size);
_darwBars(canvas, size);
}
複製程式碼
效果如下
新增運動動畫
最後在 _BarChartState
裡使用一個 AnimationController
建立柱狀圖運動的動畫,關於動畫方面的知識可以查閱 從零開始的 Flutter 動畫 這篇文章。
class _BarChartState extends State<BarChart> with TickerProviderStateMixin {
AnimationController _controller;
final _animations = <double>[];
@override
void initState() {
super.initState();
double begin = 0.0;
List<double> datas = widget.data;
// 初始化動畫控制器,並呼叫 forward 方法啟動動畫
_controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 3000),
)..forward();
for (int i = 0; i < datas.length; i++) {
final double end = datas[i];
// 使用一個補間值 Tween 建立每個矩形的動畫值
final Tween<double> tween = Tween(begin: begin, end: end);
// 初始化陣列裡面的值
_animations.add(begin);
// 建立補間動畫
Animation<double> animation = tween.animate(
CurvedAnimation(
parent: _controller,
curve: Curves.ease,
),
);
_controller.addListener(() {
// 使用 setState 更新 _animations 陣列裡面的動畫值
setState(() {
_animations[i] = animation.value;
});
});
}
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CustomPaint(
// 最後向 BarChartPainter 傳入 _animations 陣列,實現動畫
painter: BarChartPainter(
datas: _animations,
xAxis: widget.xAxis,
animation: _controller,
),
child: Container(width: 300, height: 300),
),
],
);
}
}
複製程式碼
至此整個柱狀圖的繪製就完成了,傳入資料即可使用 ???
BarChart(
data: [180.0, 98.0, 126.0, 64.0, 118.0],
xAxis: ['一月', '二月', '三月', '四月', '五月'],
);
複製程式碼
完整程式碼地址:bar_chart.dart
總結
本文說明了什麼是 CustomPaint
和 CustomPainter
。以及如何使用它們繪製一個帶有動畫的柱狀圖表。
附言
準備寫一系列關於用 Flutter 畫圖表的文章,用來分享這方面的知識,這篇文章是這個系列的開篇,預計一共會寫 6 篇。
使用 Flutter 繪製圖表(一)柱狀圖?(本文) 使用 Flutter 繪製圖表(二)餅狀圖? 使用 Flutter 繪製圖表(三)折線圖? 使用 Flutter 繪製圖表(四)雷達圖? 使用 Flutter 繪製圖表(五)環狀圖? 使用 Flutter 繪製圖表(六)條形圖?
本文使用 mdnice 排版