Flutter 学习6:绘制动画
这个是我学习Flutter的一个系列文章:
- Flutter 学习1:开发环境、开发工具、初始化一个项目
- Flutter 学习2:从main.dart文件说起
- Flutter 学习3:转场、导航
- Flutter 学习4:集成到原有的项目中
- Flutter 学习5:开发Dart包和插件包
- Flutter 学习6:绘制动画
- Flutter 学习7:Dart语言基础
- Flutter 学习8:BottomSheet
- Flutter 学习9:Positioned、Transform等控件使用以及手势控制
- Flutter 学习10:NestedScrollView、SliverAppBar、TabBar
前面文章写了关于Flutter插件的开发,最近写了一个仿照微信拍照功能的的开发包。中间那个拍照按钮有一个在拍摄视频的时候有一个动画效果。于是就学习了下关于Flutter动画绘制的过程。
绘制图形
在Flutter中关于绘图有一个专门的类:CustomPainter
。它是一个抽象类,需要实现两个方法:
@override
void paint(Canvas canvas, Size size) {
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return null;
}
看字面意思就能理解,一个是在画板上画图,一个是告诉它什么时候需要重新绘图。
这个CustomPainter
是一个画板工具,不是一个widget,那它画出来的内容要展现出来,所以有一个专门的widget (CustomPaint
)。CustomPaint
有一个属性是painter
,就是CustomPainter
。
绘制一个圆:
class DemoPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final double center = size.width * 0.5;
final double radius = size.width * 0.5;
// 画笔
Paint paint = Paint()
..style = PaintingStyle.fill
..color = Colors.red;
// 圆的中心点位置
final Offset centerOffset = Offset(center, center);
//画圆 canvas还有很多draw方法,可以画方 画圆弧 画线 画图片等等。。
canvas.drawCircle(centerOffset, radius, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return oldDelegate != this;
}
}
widget使用:
Container(
width: 200,
height: 200,
color: Colors.teal,
alignment: Alignment.center,
child: CustomPaint(
painter: DemoPainter(),
size: Size(100, 100),
)
)
显示效果:
动画控制
Flutter有一个AnimationController
动画控制器。这个控制器就是按照给定的时间按照线性生成从0到1的数字来控制动画一帧帧执行。
这个控制器还可以绑定一个时钟SingleTickerProviderStateMixin
,可以控制Widget和动画执行同步进行,显示Widget的时候执行动画,不显示的时候暂停。
还是画上面那个圆,让它转360度把圆画出来。
class DemoPainter extends CustomPainter {
final double progress;
DemoPainter({this.progress});
@override
void paint(Canvas canvas, Size size) {
final double center = size.width * 0.5;
final double radius = size.width * 0.5;
// 画笔
Paint paint = Paint()
..style = PaintingStyle.fill
..color = Colors.red;
// 圆的中心点位置
final Offset centerOffset = Offset(center, center);
// canvas.drawCircle(centerOffset, radius, paint);
final Rect rect = Rect.fromCircle(center: centerOffset, radius: radius);
final double startAngle = 0;
final double angle = 360.0 * progress;
final double sweepAngle = (angle * (pi / 180.0));
// 画圆弧 按照角度来画圆弧,后面看效果图会发现起点从0开始画的时候是3点钟方向开始的
canvas.drawArc(rect, startAngle, sweepAngle, true, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return oldDelegate != this;
}
}
这里增加了一个progress
进度参数,是控制器的value从0-1。然后因为是慢慢画出一个圆所以用了canvas.drawArc
。
控制器如何使用呢:
class _DemoViewState extends State<DemoView> with SingleTickerProviderStateMixin {
//定义一个控制器
AnimationController controller;
@override
void initState() {
super.initState();
//初始化控制器 duration是动画执行时间
controller = AnimationController(duration: Duration(seconds: 2), vsync: this)
//设置监听 有变化就刷新Widget
..addListener((){
setState(() {});
})
..repeat();
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Painter Demo'),
),
body: Center(
child: Container(
width: 200,
height: 200,
color: Colors.teal,
alignment: Alignment.center,
child: CustomPaint(
painter: DemoPainter(progress: controller.value),
size: Size(100, 100),
),
),
),
);
}
}
效果:
微信拍视频按钮动画效果
文章开始说了要做一个类似微信拍照这样的功能控件,它上面的那个拍照按钮,按住就是拍视频,这时候这个按钮会有一个动画效果。
效果图:
这个效果看动画是分两段,第一段是按钮的底部半透明圆变大,中间白色的圆变小。第二段就是进度条绿色的进度条变化。按照这个思路所以我代码里面用了两个动画控制器:
//因为用了两个控制器所以这里要用TickerProviderStateMixin
class _DemoViewState extends State<DemoView> with TickerProviderStateMixin {
AnimationController preController;
AnimationController controller;
@override
void initState() {
super.initState();
//第一个控制器控制两个圆变大变小, 0.5秒内执行完成
preController = AnimationController(duration: Duration(milliseconds: 500), vsync: this)
..addListener(() {
setState(() {});
})
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
// 按钮过渡动画完成后启动录制视频的进度条动画
controller.forward();
}
});
//第二个控制器控制进度条,15秒内执行完成
controller = AnimationController(duration: Duration(seconds: 15), vsync: this)
..addListener((){
setState(() {});
});
}
@override
void dispose() {
preController.dispose();
controller.dispose();
super.dispose();
}
......
}
然后这个CustomPainter的代码就是画两个圆和一个进度条:
class CircleRecordPainter extends CustomPainter {
final double preProgress; //第一阶段控制器进度
final double progress; //第二阶段控制器进度
final Color buttonColor = Colors.white;
final double progressWidth = 5;
final Color progressColor = Colors.green;
final back90 = degToRad(-90.0);//往前推90度 从12点钟方向开始
Color progressBackgroundColor;
Paint bottomPaint;
Paint circlePaint;
Paint progressPaint;
CircleRecordPainter(this.preProgress, this.progress)
{
//初始化画笔
progressBackgroundColor = buttonColor.withOpacity(0.7);
bottomPaint = Paint()
..style = PaintingStyle.fill
..color = progressBackgroundColor;
circlePaint = Paint()
..style = PaintingStyle.fill
..color = buttonColor;
progressPaint = Paint()
..style = PaintingStyle.stroke
..color = progressColor
..strokeWidth = progressWidth;
}
@override
void paint(Canvas canvas, Size size) {
// 底部最大的圆
final double drawRadius = size.width * 0.5;
final double center = size.width * 0.5;
final Offset offsetCenter = Offset(center, center);
final double bottomCircleRadius = drawRadius * (1 + (preProgress / 2));
canvas.drawCircle(offsetCenter, bottomCircleRadius, bottomPaint);
// 白色的圆
final double circleRadiusStart = drawRadius - progressWidth;
final double circleRadius = circleRadiusStart * (1 / (1 + preProgress));
canvas.drawCircle(offsetCenter, circleRadius, circlePaint);
// 进度条
if (progress > 0) {
final double angle = 360.0 * progress;
final double sweepAngle = degToRad(angle);
final double progressCircleRadius = bottomCircleRadius - progressWidth;
final Rect arcRect = Rect.fromCircle(center: offsetCenter, radius: progressCircleRadius);
//这里画弧度的时候它默认起点是从3点钟方向开始的所以这里的开始角度调整了90度让它从12点钟方向开始走进度
canvas.drawArc(arcRect, back90, sweepAngle, false, progressPaint);
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
The End !