上个部分写了重绘,了解之后就可以自定义一个带动画的统计图出来了。效果如下图所示,不过没有截取到进入的动画效果:
首先把基础界面做好,为了方便展示,用灰色的底色。已经写好的数据就自动略过吧。
class TestPage extends StatelessWidget {
final List<PieDescribe> pies = [
new PieDescribe(
values: [400, 200, 300, 800],
colors: [Colors.red, Colors.blue, Colors.yellow, Colors.green]),
new PieDescribe(
values: [600, 200, 700],
colors: [Colors.red, Colors.blue, Colors.yellow]),
new PieDescribe(
values: [700, 800, 400, 600],
colors: [Colors.red, Colors.blue, Colors.yellow, Colors.green])
];
int _currentNum = 0;
@override
Widget build(BuildContext context) {
PieChart pieChart = new PieChart(pies[_currentNum]);
return new Material(
color: Colors.grey,
child: new Center(
child: new InkWell(
child: new SizedBox(
width: 300.0,
height: 300.0,
child: pieChart,
),
onTap: () {
if (_currentNum < 2) {
_currentNum++;
} else {
_currentNum = 0;
}
pieChart.changePies(pies[_currentNum]);
},
),
),
);
}
}
定义一个类来放一些乱起八糟的基本设置数据,比如每个扇形间隔的宽度,动画时间等等,方便改。
(动画时间一般在200-300毫秒之间最好,我取的450就自动忽略吧)
// 一个用来放乱七八糟的数据的类,方便修改
class _ExtraValues {
final double start_radians = -pi / 2; // 起始点的弧度
final double inner_circle_radius_percent = 0.6; // 内部圆的半径占比
final Duration animation_duration = new Duration(milliseconds: 450); // 动画时间
final double space = 3.0; // 扇形之间的间隔
final Color space_color = Colors.white; // 间隔的颜色
}
然后我们需要一个用来描述整个图形的类,就先放两个属性吧,数值和颜色。
// 图表的属性描述类
class PieDescribe {
final List<num> values; // 数值
final List<Color> colors; // 颜色
PieDescribe({@required this.values, @required this.colors});
}
往下就到了定制扇形图的部分,因为要加入动画,所以就需要是一个StatefulWidget
了,当然还得把定义好基本数据的类实例化,以方便之后在绘制图形的时候使用。
// 扇形图
class PieChart extends StatefulWidget {
final PieDescribe describe; // 扇形图具体信息
final _ExtraValues values = new _ExtraValues(); // 乱七八糟的其他相关信息 ---> 为了改的时候好找
_PieChartState _state;
PieChart(this.describe);
@override
State<StatefulWidget> createState() {
_state = new _PieChartState(describe, values);
return _state;
}
void changePies(PieDescribe describe) {
_state.changePies(describe);
}
}
class _PieChartState extends State<PieChart> with TickerProviderStateMixin {
final _ExtraValues values; // 乱七八糟的其他相关信息
PieDescribe describe; // 扇形图具体信息
// 动画相关
AnimationController _controller;
CurvedAnimation _animation;
// 绘制相关
PiesPainter _painter;
_PieChartState(this.describe, this.values);
@override
void initState() {
super.initState();
// 初始化动画Controller与Animation,系统提供了CurvedAnimation,封装了各种不同类型的物理动画
_controller = new AnimationController(
vsync: this, duration: values.animation_duration);
_animation =
new CurvedAnimation(parent: _controller, curve: Curves.easeOut);
_controller.forward();
}
@override
Widget build(BuildContext context) {
List<double> radians = initValues();
_painter = new PiesPainter(
radians, describe.colors, values, _animation, values.start_radians);
return new SizedBox.expand(
child: new CustomPaint(painter: _painter),
);
}
// 数值计算 ---> 每个扇形所占的弧度
List<double> initValues() {
// 弧度的数组
List<double> radians = new List<double>();
// 取传进来的所有的值的和
num total_value = 0.0;
describe.values.forEach((num) {
total_value += num;
});
// 计算每个元素的弧度 ---> 然后返回结果
describe.values.forEach((num) {
double the_radians = num / total_value * 2 * pi;
radians.add(the_radians);
});
return radians;
}
// 改变传入的数据
void changePies(PieDescribe describe) {
this.describe = describe;
List<double> radians = initValues();
_painter.changeRadians(radians, describe.colors);
_controller.forward(from: 0.0);
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
}
最后就是对扇形图的绘制了,当然,这需要那么一点点初中数学,以及Dart
当中的math
这个包中的相关方法。
由于在使用时数据可能会变化,每次都去重新加载就显得不合适了,所以支持改变数据也就是必然要做的了,当然它还是需要加入一些动画效果来让数据的切换看起来不那么突兀。
class PiesPainter extends CustomPainter {
final _ExtraValues values;
final CurvedAnimation animation;
final double start_radians;
final Paint painter = new Paint();
final List<Offset> end_points = new List<Offset>();
List<double> last_radians = new List<double>(); //上一次的扇形弧度数组
List<double> radians; //当前的扇形弧度数组
List<Color> colors; //当前的扇形颜色
int _num; // 所包含扇形的数量
PiesPainter(this.radians, this.colors, this.values, this.animation,
this.start_radians)
: super(repaint: animation) {
_num = radians.length;
painter.style = PaintingStyle.fill;
painter.strokeWidth = values.space;
for (int i = 0; i < _num; i++) {
last_radians.add(0.0);
}
}
@override
void paint(Canvas canvas, Size size) {
//剪切画布
Rect rect = Offset.zero & size;
canvas.clipRect(rect);
Offset center = Offset(size.width / 2, size.width / 2); // 扇形图的中心点
double outer_radius = size.width / 2; // 外部圆半径
double inner_radius =
outer_radius * values.inner_circle_radius_percent; // 内部圆半径
double pie_start_radians = start_radians;
double lines_radians = 0.0;
for (int i = 0; i < _num; i++) {
// 当前每个扇形弧度的计算,需要根据之前传入的Animation所返回的值来计算
double current_radians =
last_radians[i] + (radians[i] - last_radians[i]) * animation.value;
// 外部圆 ---> 请自备三角函数 ---> 或者略过不看
painter.color = colors[i];
Rect outer_rect = Rect.fromCircle(center: center, radius: outer_radius);
canvas.drawArc(
outer_rect, pie_start_radians, current_radians, true, painter);
pie_start_radians = pie_start_radians + current_radians;
lines_radians = lines_radians + current_radians;
double end_x = outer_radius + outer_radius * sin(lines_radians);
double end_y = outer_radius - outer_radius * cos(lines_radians);
end_points.add(Offset(end_x, end_y));
}
// 内部圆 ---> 请自备三角函数 ---> 或者略过不看
painter.color = values.space_color;
Rect inner_rect = Rect.fromCircle(center: center, radius: inner_radius);
canvas.drawArc(inner_rect, 0.0, 2 * pi, true, painter);
// 最后绘制相邻扇形中间的间隔线
end_points.forEach((end_point) {
canvas.drawLine(center, end_point, painter);
});
end_points.clear();
}
// 更改属性 ---> 重绘时记录上一次的弧度值
void changeRadians(List<double> new_radians, List<Color> colors) {
// 老数组赋值
last_radians = radians;
// 新数组赋值
radians = new_radians;
// 确定绘制的循环次数 ---> 因为新数组可能大于老数组,所以只能取较大的值
_num = radians.length > last_radians.length
? radians.length
: last_radians.length;
for (int i = 0; i < _num; i++) {
// 如果新数组较大,就需要为老数组增加多余的值,0.0
if (last_radians.length <= i) {
last_radians.add(0.0);
}
// 如果老数组较大,就需要为新数组增加多余的值,0.0,并且颜色需要对应添加进来
if (radians.length <= i) {
radians.add(0.0);
colors.add(this.colors[i]);
}
}
// 最后设置颜色
this.colors = colors;
}
@override
bool shouldRepaint(PiesPainter old) => false;
}
OK,现在已经基本能用了。