继上两篇所说,在功能实现后,补全这个虽然残缺但是比较有学习价值的DEMO:
Flutter - 仿Airbnb的价格区间筛选器。(一)
Flutter - 仿Airbnb的价格区间筛选器。(二)
Flutter-CustomPaint 绘制贝塞尔曲线图表(三)
如之前一样,我习惯把介绍写到注释里,这样方便联系代码,不会导致阅读混乱。
主要是两部分:
1, CustomPaint(),通过个我们可以绘制图表,他有三个参数要注意,
分别是:painter , child ,foregroundPainter
绘制顺序依次是 painter , child ,foregroundPainter 后者会覆盖前者
2,RangeSlider,用于刷新 左右截取的值,以演示路径截取的效果
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CustomPaint(
size: size,
/// 绘制顺序依次是 painter , child ,foregroundPainter 后者会覆盖前者
painter: buildPainter(),
//foregroundPainter: ,
// child: Container(
// width: size.width,
// height: size.height,
// color: Colors.white,
// ),
),
SizedBox(
height: 30,
),
RangeSlider(
values: RangeValues(leftValue,rightValue),
divisions: 100,
min: 0.0,
max: 1.0,
onChanged: (values){
debugPrint("range value : $values");
leftValue = values.start;
rightValue = values.end;
setState(() {
});
})
],
)
CustomPaint是主要部分,通过传入一个painter就可以绘制任何东西,先展示一下buildPainter()的初始化内容:
ChartPainter buildPainter(){
//基础数据
var dataList = [
ChartBean(x: "\$2000", y: 32),
ChartBean(x: "\$1100", y: 48),
ChartBean(x: "\$1400", y: 32),
ChartBean(x: "\$500", y: 24),
ChartBean(x: "\$800", y: 50),
ChartBean(x: "\$1800", y: 25),
ChartBean(x: "\$1200", y: 18),
ChartBean(x: "\$2000", y: 32),
ChartBean(x: "\$1100", y: 48),
ChartBean(x: "\$1400", y: 32),
];
return ChartPainter(
//左右截取的值 默认是 0 - 1
leftStart: leftValue,rightEnd: rightValue,
chartBeans: dataList,
lineColor: Colors.blueAccent,//线的颜色
//图表填充颜色
fillColors: [
Colors.orange.withOpacity(0.8),
Colors.orangeAccent.withOpacity(0.5)
],
lineWith: 3,
//y轴刻度数量
yAxisNum: dataList.length
);
}
ChartPainter就是我们自己的painter,它继承自CustomPainter,需要实现两个方法:
1,shouldRepaint(ChartPainter oldDelegate)是否重绘,
我们可以根据参数取到自己的变量值,然后根据需要来判断是否重绘(如,加个flag、左右截取值得变化或者动画的进度等等)
2,paint(Canvas canvas, Size size) 这个大家很熟悉了,画布和父widget给的大小,也是主要实现区域
@override
void paint(Canvas canvas, Size size) {
_init(size); //初始化
_drawLine(canvas, size); //绘制曲线
}
void _init(Size size) {
//这里增加一些边界,确保图表不是骑着屏幕边缘绘制的,美观
//同时 会存储dataList的最小和最大值 minMax (List)
initBorder(size);
initPath(size);//初始化路径
}
initPath()方法就是用来计算和生成曲线的路径了:
注意这里的start X,Y 和 end X,Y 分别是左下和右上。
void initPath(Size size) {
if (path == null) {
if (chartBeans != null && chartBeans.length > 0 && minMax[0] > 0) {
path = Path();
double preX,//前一个数据的 x 值
preY,//前一个数据的 y 值
currentX,
currentY;
int length = chartBeans.length;
//x轴 两值之间的间距
double W = _fixedWidth / (length - 1);
for (int i = 0; i < length; i++) {
if (i == 0) {
var key = startX;//第一个数的x值
// chartBeans[i].y / maxMin[0] 算出当前y值为最大的值得百分比 * 表高 得出具体所对应的Y轴值
//用 startY - y值 可以得到最终在屏幕上的y值
var value = (startY - chartBeans[i].y / minMax[0] * _fixedHeight);
//移动到对应数据的位置
path.moveTo(key, value);
continue;
}
//绘制完第一个点后,向右平移一个 宽度(w)
currentX = startX + W * i;
//前一个数据的 x 值
preX = startX + W * (i - 1);
//前一个数据的y值
preY = (startY - chartBeans[i - 1].y / minMax[0] * _fixedHeight);
currentY = (startY - chartBeans[i].y / minMax[0] * _fixedHeight);
//绘制贝塞尔路径 (可以本站搜索或者百度,有很详尽的介绍)
path.cubicTo((preX + currentX) / 2, preY, // control point 1
(preX + currentX) / 2, currentY, // control point 2
currentX, currentY);
//如果使用直线 可以直接.lineTo(...)
}
}
}
}
支持根据dataList的数据生成的曲线path就完成了,下面开始绘制
通过_drawLine(canvas, size)方法根据刚才生成的path进行绘制,具体如下:
_drawLine(Canvas canvas, Size size) {
if (chartBeans == null || chartBeans.length == 0) return;
var paint = Paint()
..isAntiAlias = true//抗锯齿
..strokeWidth = lineWith
..strokeCap = StrokeCap.round//这个是画笔结束后的样式:这里是圆形
..color = lineColor
..style = PaintingStyle.stroke;
///安卓原生端 可以根据PathMeasure 得到路径每个点的坐标
///flutter 是通过computeMetrics ,作用基本是一致的.
if (minMax[0] <= 0) return;
var pathMetrics = path.computeMetrics(forceClosed: false);//第二个参数是否要连接起点
///生成的list,每个元素代表一条path生成的Metrics,(咱们这里只有一个path,所以元素只有一个)
var list = pathMetrics.toList();
///这里我们根据 left right 对path 进行截取
var length = list.length.toInt() -
(list.length.toInt() * leftStart) - (list.length.toInt() * (1-rightEnd));
Path linePath = new Path();
//填充颜色区域
Path shadowPath = new Path();
for (int i = 0; i < length; i++) {
//开始抽取位置
double startExtr = list[i].length * (leftStart );
//结束抽取位置
double endExtr = list[i].length * (rightEnd);
var extractPath =
list[i].extractPath(startExtr, endExtr , startWithMoveTo: true);
//extractPath.getBounds()
linePath.addPath(extractPath, Offset(0, 0));
shadowPath = extractPath;
}
///给画笔添加着色器,可以通过各种渐变createShader()方法生成着色器
/// 这里使用的是线性渐变、还有RadialGradient,SweepGradient
if (fillColors != null) {
var shader = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
tileMode: TileMode.clamp,
colors: fillColors)
.createShader(Rect.fromLTRB(startX, endY, startX, startY));
///从path的最后一个点连接起始点,形成一个闭环
shadowPath
..lineTo(startX + (_fixedWidth * rightEnd) , startY)
..lineTo((startX+ (endX * leftStart ) ), startY)
..close();
///先画阴影
canvas
..drawPath(
shadowPath,
new Paint()
..shader = shader
..isAntiAlias = true
..style = PaintingStyle.fill);
}
//再画曲线,目的是防止阴影覆盖曲线
canvas.drawPath(linePath, paint);
}
至此,整个图表就绘制完成了,我们将变量left和right传给painter,然后通过range slider来调节这两个变量并刷新,已达到截取路径的效果,具体可以运行DEMO。
https://github.com/bladeofgod/chart_price_slider
PS:我们还可以传入点击position,并根据这个做更多的交互功能在上面,这个就待后面再实现吧。
:) 谢谢大家