上次写了一个折线图的教程,这次写了一个简单的带动画的控件,效果如下
知识点有:
- AnimatedBuilder
- AnimationController
- Tween
- Interval
- CustomPaint
让我们开始吧:
1. 首先,先编写里面那个圆圈+对勾的控件
我们最开始的代码是这样的:
import 'package:flutter/material.dart';
class _AnimationCircle extends CustomPainter {
double value;
@override
void paint(Canvas canvas, Size size) {}
@override
bool shouldRepaint(_AnimationCircle oldDelegate) {
return value != oldDelegate.value;
}
}
复制代码
里面的value,是我们动画改变的时候的值,当值不一样的时候我们需要重绘,然后我们还需要定义3个坐标,分别是✔️️的3个点,坐标范围是0-1,这样我们可以通过乘以size来获得实际的坐标位置,然后_AnimationCircle类中添加下面3个属性:
Offset point1;
Offset point2;
Offset point3;
复制代码
然后我们的对勾,在控件的中心位置,我们可以根据下图大概确定这3个点的坐标。第一个点我们从边上开始,然后再移动到距离的一半,这样看起来比较漂亮
static const Offset point1 = Offset(0.0, 0.5);
static const Offset point2 = Offset(0.3, 0.8);
static const Offset point3 = Offset(0.7, 0.3);
复制代码
然后还需要3个值,用来在显示动画的时候定义1点到2点之间的开始和结束坐标,2点到3点之间的结束坐标。
Offset point1_2Start;
Offset point1_2End;
Offset point3End;
复制代码
然后还有圆形的弧度开始和结束值:
double arcStart;
double arcEnd;
复制代码
然后现在我们的代码是这个样子:
import 'package:flutter/material.dart';
class _AnimationCircle extends CustomPainter {
static const Offset point1 = Offset(0.0, 0.5);
static const Offset point2 = Offset(0.3, 0.8);
static const Offset point3 = Offset(0.7, 0.3);
double value;
Offset point1_2Start;
Offset point1_2End;
Offset point3End;
double point1_2StartValue;
double point1_2EndValue;
double point3EndValue;
double arcStart;
double arcEnd;
@override
void paint(Canvas canvas, Size size) {}
@override
bool shouldRepaint(_AnimationCircle oldDelegate) {
return value != oldDelegate.value;
}
}
复制代码
然后我们这个控件,有4个值需要传入,看动画能看出来,我们的对勾不是动画开始就开始画,而是等过了一段时间才开始画,所以我们添加如下构造函数:
_AnimationCircle({
@required this.value,
@required this.point1_2StartValue,
@required this.point1_2EndValue,
@required this.point3EndValue,
});
复制代码
第一个value,我们用来计算圆形的弧度,第二个值,用来计算1点到2点的起始坐标,第三个值,用来计算1点到2点的结束位置坐标,第四个值,用来计算第三个点的结束坐标,2点到3点之间的线段起始坐标一直是2点,所以这里只有结束坐标。
2. 然后我们开始下一个控件的定义,这个控件用来计算上述坐标给_AnimationCircle
class _AnimationCircleWidgetState extends State<_AnimationCircleWidget>
with SingleTickerProviderStateMixin<_AnimationCircleWidget> {
AnimationController controller;
@override
void initState() {
controller = AnimationController(vsync: this, lowerBound: 0, upperBound: 1);
controller
..forward()
..addListener(() {
setState(() { });
});
super.initState();
}
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: _AnimationCircle(),
);
}
}
复制代码
上面的代码比较简单,就不多说了,然后关键部分在build方法里这些值的计算,从动画效果可以看出,圆形绘制完毕后对勾也绘制完了,并且弧形是先绘制出来的,然后才是1点到2点的动画,再然后1到2点和2点到3点的动画。
我们把动画时间分为4段,分别是0-0.25、0.25-0.5、0.5-0.75、0.75-1。
1点到2点的动画绘制时,需要让point1_2End
先变大,point1_2End
大于point1_2Start
才可以绘制线段出来,所以我们在第三段动画时间里传递point1_2EndValue
值,在point1_2StartValue
没有传递之前point1_2StartValue
都是0,所以绘制出来了1点到2点的线段。
在第四段动画时间里再传递point1_2StartValue
、point1_2EndValue
、point3EndValue
,就完成了计算。
这部分逻辑稍微复杂一些,不明白的话可以多看几遍就懂了,然后代码如下:
class _AnimationCircleWidgetState extends State<_AnimationCircleWidget>
with SingleTickerProviderStateMixin<_AnimationCircleWidget> {
AnimationController controller;
@override
void initState() {
controller = AnimationController(vsync: this, lowerBound: 0, upperBound: 1);
controller
..forward()
..addListener(() {
setState(() { });
});
super.initState();
}
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: _AnimationCircle(
value: controller.value,
// 在第四段时间结束后,point1_2Start的值就是1了
point1_2StartValue: Interval(0.75, 1).transform(controller.value),
// 在第三段时间结束后,point1_2End的值就是1了,
// 在第三段时间刚结束的时候point1_2Start还是0,
// 因为point1_2Start这个值在第四段时间才是非0
point1_2EndValue: Interval(0.5, 0.75).transform(controller.value),
// 在第四段时间结束后,point3End的值就是1了
point3EndValue: Interval(0.75, 1).transform(controller.value),
),
);
}
}
复制代码
3. 然后我们修改_AnimationCircle类把动画控件显示出来,到目前为止的完整代码如下,我都注释了所以就不再解释了
import 'dart:math' as math;
import 'package:flutter/material.dart';
//author:liuhc
class _AnimationCircle extends CustomPainter {
static const Offset point1 = Offset(0.0, 0.5);
static const Offset point2 = Offset(0.3, 0.8);
static const Offset point3 = Offset(0.7, 0.3);
double value;
Offset point1_2Start;
Offset point1_2End;
Offset point3End;
double point1_2StartValue;
double point1_2EndValue;
double point3EndValue;
double arcStart;
double arcEnd;
_AnimationCircle({
@required this.value,
@required this.point1_2StartValue,
@required this.point1_2EndValue,
@required this.point3EndValue,
}) {
//arcStart这样计算,看起来就是在转圈,否则只是单纯的绘制圆形,可以把下面的2去掉查看效果就明白了
arcStart = math.pi + math.pi * value * 2;
//math.pi * 2 的弧度是一个完整的圆形,这样动画结束的时候value是1,也就正好画出了一个圆形
arcEnd = math.pi + math.pi * value;
point1_2Start = point1 + (point2 - point1) * point1_2StartValue;
point1_2End = point1 + (point2 - point1) * point1_2EndValue;
point3End = point2 + (point3 - point2) * point3EndValue;
}
@override
void paint(Canvas canvas, Size size) {
double minValue = math.min(size.width, size.height);
Paint paint = Paint()
//画笔颜色
..color = Colors.blue
//边缘平滑
..strokeCap = StrokeCap.round
//画笔宽度
..strokeWidth = 3
//不填充
..style = PaintingStyle.stroke;
//这个判断是因为在动画的第三段对勾才开始绘制,在之前point1_2Start和point1_2End都是point1
//只有他们不相等的时候再绘制,才不会在动画刚开始的时候有个小点,可以把这个判断去掉看效果就明白了
//之所以* minValue,是因为把point1、2、3的范围0-1当成了比例来用,所以* minValue就是实际的坐标了
if (point1_2Start != point1_2End) {
canvas.drawLine(point1_2Start * minValue, point1_2End * minValue, paint);
}
//只有他们不相等的时候再绘制,才不会在动画刚开始的时候有个小点,可以把这个判断去掉看效果就明白了
//之所以* minValue,是因为把point1、2、3的范围0-1当成了比例来用,所以* minValue就是实际的坐标了
if (point2 != point3End) {
canvas.drawLine(point2 * minValue, point3End * minValue, paint);
}
canvas.drawArc(Rect.fromLTWH(0, 0, minValue, minValue), arcStart, arcEnd, false, paint);
}
@override
bool shouldRepaint(_AnimationCircle oldDelegate) {
return value != oldDelegate.value;
}
}
class _AnimationCircleWidget extends StatefulWidget {
@override
_AnimationCircleWidgetState createState() => _AnimationCircleWidgetState();
}
class _AnimationCircleWidgetState extends State<_AnimationCircleWidget>
with SingleTickerProviderStateMixin<_AnimationCircleWidget> {
AnimationController controller;
@override
void initState() {
controller = AnimationController(vsync: this, duration: Duration(seconds: 2), lowerBound: 0, upperBound: 1);
controller
..forward()
..addListener(() {
setState(() {});
});
super.initState();
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: _AnimationCircle(
value: controller.value,
// 在第四段时间结束后,point1_2Start的值就是1了
point1_2StartValue: Interval(0.75, 1).transform(controller.value),
// 在第三段时间结束后,point1_2End的值就是1了,
// 在第三段时间刚结束的时候point1_2Start还是0,
// 因为point1_2Start这个值在第四段时间才是非0
point1_2EndValue: Interval(0.5, 0.75).transform(controller.value),
// 在第四段时间结束后,point3End的值就是1了
point3EndValue: Interval(0.75, 1).transform(controller.value),
),
);
}
}
void main() {
runApp(
MaterialApp(
home: Test(),
),
);
}
class Test extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: RaisedButton(
child: Text("测试"),
onPressed: () {
showDialog(
barrierDismissible: false,
context: context,
builder: (context) {
return Center(
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(10.0)),
),
height: 120.0,
width: 120.0,
padding: EdgeInsets.all(30.0),
child: _AnimationCircleWidget(),
),
);
},
);
},
),
),
);
}
}
复制代码
然后我们运行一下,发现是这样的
左边的对勾没了,这是因为我们在完整动画时间的第四段(0.75-1)里,下面2个point1_2Start
和point1_2End
最后是相等的
point1_2Start = point1 + (point2 - point1) * point1_2StartValue;
point1_2End = point1 + (point2 - point1) * point1_2EndValue;
复制代码
看第一张图可以看出来,对勾左边的线段是右边线段的一半,所以我们在传递值的地方改一下,我们将值的范围从0-1改为从0-0.5,我们改_AnimationCircleWidgetState的build方法
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: _AnimationCircle(
value: controller.value,
// 注意,改动在这里,加了一个Tween,可以和上面的方法对比一下就知道区别了
// 在第四段时间结束后,point1_2Start的值就是0.5了
point1_2StartValue: Tween(begin: 0, end: 0.5).transform(Interval(0.75, 1).transform(controller.value)),
// 在第三段时间结束后,point1_2End的值就是1了,
// 在第三段时间刚结束的时候point1_2Start还是0,
// 因为point1_2Start这个值在第四段时间才是非0
point1_2EndValue: Interval(0.5, 0.75).transform(controller.value),
// 在第四段时间结束后,point3End的值就是1了
point3EndValue: Interval(0.75, 1).transform(controller.value),
),
);
}
复制代码
这样改动后,
point1_2Start = point1 + (point2 - point1) * point1_2StartValue;
point1_2End = point1 + (point2 - point1) * point1_2EndValue;
复制代码
point1_2Start的值在动画结束后就是point1 + (point2 - point1) * 0.5
,这样我们左边的对勾就显示出来了,并且是右边那条线段的一半,我们再运行一下,然后效果就是第一张图的效果了
4. 完整代码如下:
import 'dart:math' as math;
import 'package:flutter/material.dart';
///
/// 描述:动画演示
/// 作者:liuhc
class _AnimationCircle extends CustomPainter {
static const Offset point1 = Offset(0.0, 0.5);
static const Offset point2 = Offset(0.3, 0.8);
static const Offset point3 = Offset(0.7, 0.3);
double value;
Offset point1_2Start;
Offset point1_2End;
Offset point3End;
double point1_2StartValue;
double point1_2EndValue;
double point3EndValue;
double arcStart;
double arcEnd;
_AnimationCircle({
@required this.value,
@required this.point1_2StartValue,
@required this.point1_2EndValue,
@required this.point3EndValue,
}) {
//arcStart这样计算,看起来就是在转圈,否则只是单纯的绘制圆形,可以把下面的2去掉查看效果就明白了
arcStart = math.pi + math.pi * value * 2;
//math.pi * 2 的弧度是一个完整的圆形,这样动画结束的时候value是1,也就正好画出了一个圆形
arcEnd = math.pi + math.pi * value;
point1_2Start = point1 + (point2 - point1) * point1_2StartValue;
point1_2End = point1 + (point2 - point1) * point1_2EndValue;
point3End = point2 + (point3 - point2) * point3EndValue;
}
@override
void paint(Canvas canvas, Size size) {
double minValue = math.min(size.width, size.height);
Paint paint = Paint()
//画笔颜色
..color = Colors.blue
//边缘平滑
..strokeCap = StrokeCap.round
//画笔宽度
..strokeWidth = 3
//不填充
..style = PaintingStyle.stroke;
//这个判断是因为在动画的第三段对勾才开始绘制,在之前point1_2Start和point1_2End都是point1
//只有他们不相等的时候再绘制,才不会在动画刚开始的时候有个小点,可以把这个判断去掉看效果就明白了
//之所以* minValue,是因为把point1、2、3的范围0-1当成了比例来用,所以* minValue就是实际的坐标了
if (point1_2Start != point1_2End) {
canvas.drawLine(point1_2Start * minValue, point1_2End * minValue, paint);
}
//只有他们不相等的时候再绘制,才不会在动画刚开始的时候有个小点,可以把这个判断去掉看效果就明白了
//之所以* minValue,是因为把point1、2、3的范围0-1当成了比例来用,所以* minValue就是实际的坐标了
if (point2 != point3End) {
canvas.drawLine(point2 * minValue, point3End * minValue, paint);
}
canvas.drawArc(Rect.fromLTWH(0, 0, minValue, minValue), arcStart, arcEnd, false, paint);
}
@override
bool shouldRepaint(_AnimationCircle oldDelegate) {
return value != oldDelegate.value;
}
}
class _AnimationCircleWidget extends StatefulWidget {
@override
_AnimationCircleWidgetState createState() => _AnimationCircleWidgetState();
}
class _AnimationCircleWidgetState extends State<_AnimationCircleWidget>
with SingleTickerProviderStateMixin<_AnimationCircleWidget> {
AnimationController controller;
@override
void initState() {
controller = AnimationController(vsync: this, duration: Duration(seconds: 2), lowerBound: 0, upperBound: 1);
controller
..forward()
..addListener(() {
setState(() {});
});
super.initState();
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: _AnimationCircle(
value: controller.value,
// 在第四段时间结束后,point1_2Start的值就是0.5了
point1_2StartValue: Interval(0.75, 1.0).transform(controller.value),
// 在第三段时间结束后,point1_2End的值就是1了,
// 在第三段时间刚结束的时候point1_2Start还是0,
// 因为point1_2Start这个值在第四段时间才是非0
point1_2EndValue: Interval(0.5, 0.75).transform(controller.value),
// 在第四段时间结束后,point3End的值就是1了
point3EndValue: Interval(0.75, 1.0).transform(controller.value),
),
);
}
}
void main() {
runApp(
MaterialApp(
home: Test(),
),
);
}
class Test extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("动画演示"),
),
body: Center(
child: RaisedButton(
child: Text("测试"),
onPressed: () {
showDialog(
barrierDismissible: false,
context: context,
builder: (context) {
return Center(
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(10.0)),
),
height: 120.0,
width: 120.0,
padding: EdgeInsets.all(30.0),
child: _AnimationCircleWidget(),
),
);
},
);
},
),
),
);
}
}
复制代码