Flutter快速实现自定义折线图,支持数据改变过渡动画
最终效果如下:
绘制
先创建一个CustomPainter,使用canvas绘制。
import 'dart:ui';
import 'package:flutter/material.dart';
/// 自定义折线图
class LineChartPainter extends CustomPainter {
Paint _outlinePaint;
Paint _axisXPaint;
Paint _valuePaint;
/// 左边Y轴标签
TextPainter _leftLabelPainter;
List<int> _yTitle = [40, 30, 20, 10, 0, -10]; // y轴刻度数量
List<double> _yData;
final int averageY = 5; // y轴平均份数
final int gapY = 10; // y轴平均间隔值
final double _yTitlePadding = 5;
LineChartPainter(this._yData, { Listenable repaint})
: super(repaint: repaint) {
_outlinePaint = Paint()
..color = Colors.grey
..style = PaintingStyle.stroke
..strokeWidth = 1;
_axisXPaint = Paint()
..color = Colors.grey
..style = PaintingStyle.stroke
..strokeWidth = 1;
_valuePaint = Paint()
..color = Colors.blue
..style = PaintingStyle.stroke
..strokeWidth = 1.5;
_leftLabelPainter = TextPainter()..textDirection = TextDirection.ltr;
}
@override
void paint(Canvas canvas, Size size) {
print("${this},,,$size");
/// 外边框矩形
canvas.drawRect(
Rect.fromLTRB(0, 0, size.width, size.height), _outlinePaint);
/// x轴
for (var i = 0; i < 4; i++) {
canvas.drawLine(Offset(0, size.height / averageY * (i + 1)),
Offset(size.width, size.height / averageY * (i + 1)), _axisXPaint);
}
/// y轴标题
for (var i = 0; i <= 5; i++) {
_leftLabelPainter.text = TextSpan(
text: _yTitle[i].toString(), style: TextStyle(color: Colors.black));
_leftLabelPainter.layout();
_leftLabelPainter.paint(
canvas,
Offset(-_leftLabelPainter.width - _yTitlePadding,
size.height / 5 * i - _leftLabelPainter.height / 2),
);
}
/// 数据
/// 需要将y轴数据转换为height中对应比例的高度。
var points = <Offset>[];
for (var i = 0; i < _yData.length; i++) {
points.add(Offset(size.width / _yData.length * i,
translateValue(size.height, _yData[i])));
}
canvas.drawPoints(PointMode.polygon, points, _valuePaint);
}
@override
bool shouldRepaint(LineChartPainter oldDelegate) {
return oldDelegate._yData != _yData;
}
/// 转换y坐标
double translateValue(double height, double rawValue) {
/// y轴真实值总长度
var valueSum = averageY * gapY;
/// 真实值和height计算比例
var scale = height / valueSum;
var result = rawValue * scale;
return height - result - height / averageY;
}
}
实现过渡动画
使用CustomPaint包裹CustomPainter,为了在数据变化时,有动画效果,使用ImplicitlyAnimatedWidget。
实现动画效果的重点是forEachTween
和lerp
方法。
注意:State不要继承ImplicitlyAnimatedWidgetState
,直接继承AnimatedWidgetBaseState
即可,它内部实现了对AnimationController
的监听,并实时刷新Animation的value,自动调用forEachTween方法。
import 'dart:ui';
import 'package:flutter/material.dart';
import 'line_chart_painter.dart';
/// 包装折线图 数据更新时展示过渡动画
class LineChart extends ImplicitlyAnimatedWidget {
final Size size;
final LineChartData lineChartData;
final Duration duration;
const LineChart(
{ Key key,
this.size,
@required this.lineChartData,
@required this.duration})
: super(key: key, duration: duration);
@override
_CustomCanvasState createState() {
return _CustomCanvasState();
}
}
class _CustomCanvasState extends AnimatedWidgetBaseState<LineChart> {
DataTween _dataTween;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraint) {
print(
"widget size:${widget.size} ${constraint.maxWidth}...${constraint.maxHeight}");
var constrainSize =
widget.size ?? Size(constraint.maxWidth, constraint.maxHeight);
if (constrainSize.width == double.infinity) {
throw FlutterError("必须为组件或父widget设置一个有效宽度");
}
if (constrainSize.height == double.infinity) {
throw FlutterError("必须为组件或父widget设置一个有效高度");
}
return CustomPaint(
foregroundPainter: LineChartPainter(
_dataTween.evaluate(animation).data,
repaint: animation),
size: constrainSize,
);
},
);
}
@override
void forEachTween(TweenVisitor visitor) {
/// 第一个参数是初始的tween,第二个参数是目标值,第三个是生成tween的回调。
_dataTween = visitor(
_dataTween, widget.lineChartData, (value) => DataTween(begin: value));
}
}
class DataTween extends Tween<LineChartData> {
DataTween({ LineChartData begin, LineChartData end})
: super(begin: begin, end: end);
@override
LineChartData lerp(double t) => LineChartData.lerp(begin, end, t);
}
class LineChartData {
List<double> data;
LineChartData({ this.data});
/// 计算动画更新时的数据
/// begin表示动画开始时的数据,end是结束时的数据,t是动画估值器,从0到1,代表动画运行的进度。
static LineChartData lerp(LineChartData begin, LineChartData end, double t) {
/// 根据begin和end每个对应的值,因为值类型是double,所以使用系统自带的lerpDouble来计算值。
LineChartData result;
if (begin.data != null &&
end.data != null &&
begin.data.length == end.data.length) {
result = LineChartData(
data: List.generate(begin.data.length, (index) {
return lerpDouble(begin.data[index], end.data[index], t);
}));
} else if (begin.data.length > end.data.length) {
result = LineChartData(
data: List.generate(end.data.length, (index) {
return lerpDouble(begin.data[index], end.data[index], t);
}));
} else if (begin.data.length < end.data.length) {
result = LineChartData(
data: List.generate(begin.data.length, (index) {
return lerpDouble(begin.data[index], end.data[index], t);
}));
}
return result;
}
}
使用
class StudyCanvasPage extends StatefulWidget {
const StudyCanvasPage({ Key key}) : super(key: key);
@override
_StudyCanvasPageState createState() => _StudyCanvasPageState();
}
class _StudyCanvasPageState extends State<StudyCanvasPage> {
var check = false;
List<double> _yData = [29.2, 29.8, 29.3, 29.2, 29.4, 29.1, 29.3, 29.2, 29.5, 29.2, 29.2, 29, 29.3, 29.2, 29.4, 29.1, 29.3, 29.2, 29.5, 29.2];
List<double> _yData1 = [12.2, 19.8, 19.3, 9.2, 19.4, 19.1, 19.3, 19.2, 19.5, 19.2, 19.2, 19, 19.3, 19.2, 19.4, 19.1, 19.3, 19.2, 19.5, 19.2,];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('study canvas'),
),
body: SafeArea(
child: Container(
child: Column(
children: [
ElevatedButton(
onPressed: () {
setState(() {
check = !check;
});
},
child: Text("切换数据"),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(28.0),
child: LineChart(
lineChartData:
LineChartData(data: check ? _yData : _yData1),
duration: Duration(milliseconds: 300),
),
),
),
],
),
),
),
);
}
}
还没有评论,来说两句吧...