主要通过这个博文学习通过Flutter自定义视图来绘制柱形图.
这年头没个图片都不好意思发博客了:
在柱形图上方显示具体数值,x轴下方显示具体日期,忽略上方的日月年的tabBar.
首先介绍几个主要的方法:
canvas.drawRect(Rect rect, Paint paint); //绘制矩形
canvas.drawLine(Offset p1, Offset p2, Paint paint); //绘制一条直线
canvas.drawParagraph(Paragraph paragraph, Offset offset); //绘制文字
下面开始撸代码:
class CustomChartPaint extends CustomPainter {
Paint _bgPaint;
Paint _barPaint;
Paint _linePaint;
Color backgroundColor = Color(0xFFF6F6F6); //条形图背景颜色
Color barColor = Color(0xFF5858D6); //条形图颜色
static const Color textColor = Color(0xFF5858D6); //字体颜色
Color lineColor = Color(0xFFD1D1D6); //X轴颜色
double barWidth = 22.0; //条形图宽度
double barMargin = 20.0; //两条形图间距
double topMargin = 22.0; //图表与其他试图上边距
double textTopMargin = 8.0; //X轴文字与X轴间距
static const double xFontSize = 12.0; //X轴文字的字体大小
double bottomTextDescent = 20;
double bottomTextHeight = 20;
final List<double> dataList;
final List<String> bottomList;
final String xText;
List<String> bottomTextList;
List targetPercentList;
List percentList;
CustomChartPaint(this.dataList, this.bottomList, this.xText){
//print("max" + (dataList.reduce(max)+dataList.reduce(min)).toString());
setBottomTextList(bottomList);
setDataList(dataList, dataList.reduce(max)+dataList.reduce(min));
init();
}
init(){
_bgPaint = new Paint()
..isAntiAlias = true //是否启动抗锯齿
..color = backgroundColor; //画笔颜色
_barPaint = new Paint()
..color = barColor
..isAntiAlias = true
..style = PaintingStyle.fill; //绘画风格,默认为填充;
_linePaint = new Paint()
..color = lineColor
..strokeWidth = 0.5;
}
setBottomTextList(List<String> bottomStringList) {
this.bottomTextList = bottomStringList;
}
setDataList(List<double> list, double max) {
targetPercentList = new List<double>();
if (max == 0) max = 1;
for (int i = 0; i < list.length; i++) {
targetPercentList.add((1 - list[i] / max)); //当前数据占总长度的百分比
}
// TODO Make sure percentList.length() == targetPercentList.length()
}
Rect bgRect;
Rect fgRect;
@override
void paint(Canvas canvas, Size size) {
print("size: " + size.toString()); //画布大小
int i = 1;
if (targetPercentList.length != 0) {
for (double f in targetPercentList) {
bgRect = Rect.fromLTRB(barMargin * i + barWidth * (i - 1),
topMargin,
(barMargin + barWidth) * i,
size.height - bottomTextHeight - textTopMargin);
canvas.drawRect(bgRect, _bgPaint); //绘制背景柱形
double rectLeft = barMargin * i + barWidth * (i - 1);
double rectRight = topMargin + (size.height - topMargin - bottomTextHeight - textTopMargin) * targetPercentList[i - 1];
//print("rectX: " + rectLeft.toString() + ", rectY: " + rectRight.toString());
fgRect = Rect.fromLTRB(rectLeft, rectRight,
(barMargin + barWidth) * i,
size.height - bottomTextHeight - textTopMargin);
canvas.drawRect(fgRect, _barPaint); //绘制前景柱形
Offset textOffset = new ui.Offset(rectLeft - barWidth/2, rectRight - 18); /// 18为具体数值与柱形顶部的间距
_drawParagraph(canvas, textOffset, dataList[i-1].toString()); //在bar上描绘具体数值
i++;
}
double mViewWidth = (bottomTextList.length + 1.5) * (barWidth + barMargin); //整个视图的宽度
Offset start = ui.Offset(0, size.height - bottomTextHeight - textTopMargin);
Offset end = ui.Offset(mViewWidth, size.height - bottomTextHeight - textTopMargin);
canvas.drawLine(start, end, _linePaint); //画一条X轴
if (bottomTextList != null && bottomTextList.isNotEmpty) {
i = 1;
for (String bottomText in bottomTextList) {
Offset bottomTextOffset = new Offset(barMargin * i + barWidth * (i - 2) + barWidth/2, size.height - bottomTextDescent - textTopMargin);
_drawParagraph(canvas, bottomTextOffset, bottomText); //绘制X轴描述
i++;
}
}
Offset bottomTextOffset = new Offset(barMargin * i + barWidth * (i - 1), size.height - bottomTextDescent - textTopMargin);
_drawParagraph(canvas, bottomTextOffset, xText); //绘制X轴变量名
}
}
_drawParagraph(Canvas canvas, Offset offset, String text, {double fontSize : xFontSize, Color textColor: textColor}){
ParagraphBuilder paragraphBuilder = new ui.ParagraphBuilder(
new ui.ParagraphStyle(
textAlign: TextAlign.center, //在ui.ParagraphConstraints(width: barWidth * 2);所设置的宽度中居中显示
fontSize: fontSize,
),
)
..pushStyle(ui.TextStyle(color: textColor));
paragraphBuilder.addText(text);
ParagraphConstraints pc = ui.ParagraphConstraints(width: barWidth * 2); //字体可用宽度
//这里需要先layout, 后面才能获取到文字高度
Paragraph textParagraph = paragraphBuilder.build()..layout(pc);
canvas.drawParagraph(textParagraph, offset); //描绘offset所表示的位置上描绘文字text
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
在这简单介绍几个实用的方法:
List.reduce(max) //求list列表中最大值
List.reduce(min) //求list列表中的最小值
另外针对于:
double mViewWidth = (bottomTextList.length + 1.5) * (barWidth + barMargin); //整个视图的宽度
其中这个1.5是为了预留给x轴中"天"的长度,避免跟屏幕挨得很近影响视觉效果.
其中重点说一下这个描绘文字的,其他构建矩形跟画直线相对简单,与Android原生自定义view很像,我们直接看方法然后看他的参数名就能理解.此处不详述了.
我这里先简单说一下Android原生绘制文字的方法吧:
fgPaint.setTextSize(textSize);
String text = targetPercentList.get(i-1).toString();
float sizeWidth = fgPaint.measureText(text);
canvas.drawText( text, barMargin* i + barWidth * (i - 1) + barWidth /2 - sizeWidth /2, topMargin + (int) ((getHeight()
- topMargin
- bottomTextHeight
- textTopMargin) * targetPercentList.get(i - 1)) - 20, fgPaint); //在bar上描绘具体数值
首先我们有drawText方法,而且measureText方法可以来测量文字的宽度,再通过文字的宽度来定位,让该文字显示在柱形图的上方,并且是居中的,这里的位置计算大家画个图理解一下.(这里的20是柱形图文字与柱形图的间距)
但是在Flutter就根本没有drawText方法了,我们只能通过 canvas.drawParagraph(Paragraph paragraph, Offset offset);方法来实现,其中Offset是文字的位置,而且Flutter也没有measureText方法来计算文字宽度,这就一脸懵逼了,不能计算文字宽度,我们最多也就能让文字定位在柱形图上面,但是不能跟柱形对其并居中显示.
这就主要看另一个参数 Paragraph了.
这里拿柱形图上方文字来举例吧,因为文字太长,柱形图的宽度不够显示,所以可以通过下面的方法设置他的可用长度,
ParagraphConstraints pc = ui.ParagraphConstraints(width: barWidth * 2);
设置了字体的可用长度为 2倍barWidth,不得不佩服我的机制,.
然后就是居中问题了,
ParagraphBuilder paragraphBuilder = new ui.ParagraphBuilder(
new ui.ParagraphStyle(
textAlign: TextAlign.center, //在ui.ParagraphConstraints(width: barWidth * 2);所设置的宽度中居中显示
fontSize: fontSize,
),
)
Offset textOffset = new ui.Offset(rectLeft - barWidth/2, rectRight - 18); /// 18为具体数值与柱形顶部的间距
示意图如下,(兄弟们原谅我三更半夜没来及用画图工具画个好看的示意图了,只能手工来了),
其中这个offset主要是关于我们之前设置的的 pc 的定位,至于文字已经在barWidth*2这个长度中居中了,那么只需要将这个长度的容器居中放在柱形图的上方就OK了.X轴下面的文字也是这个道理,相信就不用我再赘述了.
好了最后就是在我们的StatelessWidget中设置SingleChildScrollView,使我们的柱形图可以在横向左右滑动,即使柱形图太长一屏显示不完也没关系啦~~~~
return new SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: new Container(
child: CustomPaint(
size: new Size((bottomTextList.length + 1.5) * 42.0, 400.0),
painter: CustomChartPaint(dataList, bottomTextList, xText),
),
),
);
另外补充一点就是这个TextStyle一定要这么写,如果去掉了ui则会报错
..pushStyle(ui.TextStyle(color: textColor));
The argument type 'TextStyle (where TextStyle is defined in D:\software\Android\Flutter\packages\flutter\lib\src\painting\text_style.dart)' can't be assigned to the parameter type 'TextStyle (where TextStyle is defined in D:\software\Android\Flutter\bin\cache\pkg\sky_engine\lib\ui\text.dart)'.
至于这个ui是啥:
import 'dart:ui' deferred as ui;