参考文档:https://medium.com/flutter/zero-to-one-with-flutter-part-two-5aa2f06655cb
原github地址:https://github.com/mravn/charts
本篇文章从几个Demo出发介绍了Animation、Tween和CustomPaint的使用,并且几个Demo实现效果是层层递进的,下面会详细记录Demo的编写过程,以及简要地介绍以上三个类的使用和注意事项。本文涉及到的知识点:
首先上图,该demo完成后的效果如下:
那么如何实现这么一个效果呢?这就需要使用到Animation、Tween和CustomPaint这三个类了,需要以下几个步骤:
class Bar {
static final Random _random = new Random();
static final double _maxHeight = 100.0;
final Color color;
final double height;
Bar(this.color, this.height);
/// 空bar
factory Bar.empty() => Bar(Colors.transparent, 0.0);
/// 带有随机颜色和随机高度的bar,高度范围
factory Bar.random() =>
Bar(ColorPalette.primary.random(), _random.nextDouble() * _maxHeight);
static Bar lerp(Bar begin, Bar end, double t) {
return Bar(Color.lerp(begin.color, end.color, t),
lerpDouble(begin.height, end.height, t));
}
}
值得注意的是,以上使用了factory工厂构造函数创建对象,还有在lerp静态函数中,使用了Color.lerp和lerpDouble(这个需要引入dart:math包),这个方法是为了之后的BarTween对象服务。
/// Bar属性线性变换插值器
class BarTween extends Tween {
BarTween(Bar begin, Bar end) : super(begin: begin, end: end);
@override
Bar lerp(double t) {
return Bar.lerp(begin, end, t);
}
}
其中lerp在本demo中必须重写,否则会没有动画过程。这个方法指代的是在动画t位置的状态,t的取值为[0.0,1.0]。根据Tween
/// Returns the value this variable has at the given animation clock value.
///
/// The default implementation of this method uses the [+], [-], and [*]
/// operators on `T`. The [begin] and [end] properties must therefore be
/// non-null by the time this method is called.
@protected
T lerp(double t) {
assert(begin != null);
assert(end != null);
return begin + (end - begin) * t;
}
/// Returns the interpolated value for the current value of the given animation.
///
/// This method returns `begin` and `end` when the animation values are 0.0 or
/// 1.0, respectively.
///
/// This function is implemented by deferring to [lerp]. Subclasses that want
/// to provide custom behavior should override [lerp], not [transform] (nor
/// [evaluate]).
///
/// See the constructor for details about whether the [begin] and [end]
/// properties may be null when this is called. It varies from subclass to
/// subclass.
@override
T transform(double t) {
if (t == 0.0)
return begin;
if (t == 1.0)
return end;
return lerp(t);
}
Bar类和动画变换规则类BarTween都已经编写就绪,下面就是绘制bar图形了。
/// BarChart绘制器 根据传入的动画对象,在画板canvas上用画笔paint绘制出对应的图形
class BarPainter extends CustomPainter {
final Animation animation;
static const barWidth = 10.0;
/// 需要将动画对象传递进来,并且指定repaint:animation
/// repaint是The painter will repaint whenever `repaint` notifies its listeners.
BarPainter(Animation animation)
: animation = animation,
super(repaint: animation);
@override
void paint(Canvas canvas, Size size) {
final bar = animation.value;
final paint = new Paint()
..color = bar.color
..style = PaintingStyle.fill;
canvas.drawRect(
Rect.fromLTWH((size.width - barWidth) / 2.0, size.height - bar.height,
barWidth, bar.height),
paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
关于画板Canvas和画笔Paint对象的使用,和Android原生使用类似,Canvas中提供了以下绘制方法:
class SingleBarPage extends StatefulWidget {
@override
_SingleBarPageState createState() => _SingleBarPageState();
}
class _SingleBarPageState extends State
with SingleTickerProviderStateMixin {
AnimationController _animation;
BarTween _barTween;
@override
void initState() {
super.initState();
_animation =
AnimationController(vsync: this, duration: Duration(milliseconds: 300));
_barTween = BarTween(Bar.empty(), Bar.random());
_animation.forward();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Charts"),
),
body: Center(
child: CustomPaint(
painter: BarPainter(_barTween.animate(_animation)),
size: Size(200.0, 100.0),
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.refresh),
onPressed: changeData,
),
);
}
// 通过改变BarTween,指定初始和结束Bar状态,来达到动画的效果
void changeData() {
setState(() {
_barTween = BarTween(_barTween.evaluate(_animation), Bar.random());
_animation.forward(from: 0.0);
});
}
}
值得主要的是,一个Bar的动画,需要指定AnimationController动画控制器、BarTween动画变换规则。还有bar图形的绘制需要用CustomPaint承载。
同样先上图,效果如下:
与第一个动画实例类似,也是需要四个步骤:
import 'package:flutter/material.dart';
import 'bar.dart';
class BarChart {
static const int barCount = 5;
final List bars;
BarChart(this.bars) : assert(bars.length == barCount);
factory BarChart.empty() => BarChart(List.filled(barCount, Bar.empty()));
factory BarChart.random() =>
BarChart(List.generate(barCount, (i) => Bar.random()));
static BarChart lerp(BarChart begin, BarChart end, double t) {
return BarChart(List.generate(
barCount, (i) => Bar.lerp(begin.bars[i], end.bars[i], t)));
}
}
class BarChartTween extends Tween {
BarChartTween(BarChart begin, BarChart end) : super(begin: begin, end: end);
@override
BarChart lerp(double t) {
return BarChart.lerp(begin, end, t);
}
}
class BarChartPainter extends CustomPainter {
Animation animation;
static const double barWidthFraction = 0.75;
BarChartPainter(this.animation) : super(repaint: animation);
@override
void paint(Canvas canvas, Size size) {
void drawBar(Bar bar, double x, double barWidth, Paint paint) {
// 将bar的颜色赋给画笔
paint..color = bar.color;
canvas.drawRect(
Rect.fromLTWH(x, size.height - bar.height, barWidth, bar.height),
paint);
}
final paint = Paint()..style = PaintingStyle.fill;
final chart = animation.value;
final barDistance = size.width / (1 + chart.bars.length);
final barWidth = barDistance * barWidthFraction;
// bar的x轴开始位置
var x = barDistance - barWidth / 2;
chart.bars.forEach((bar) {
drawBar(bar, x, barWidth, paint);
x += barDistance;
});
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
这篇文章到此打住,已经够长了。下一篇文章介绍几种更加复杂的Bar Demo。
该demo项目,完整代码,我的github地址:
https://github.com/shengleiRain/flutter_app/tree/master/animation_bar_demo