CustomPainter 这个类前文讲过,就在贝塞尔曲线那块,不了解的可以爬楼,但这个类不难,其主要功能就是提供用户绘制各种各样的控件。
本文主要记录讲解微信拍照按钮的效果实现,其按钮效果大体如下:
效果图
![](https://img.haomeiwen.com/i3282504/0cc7e4d7fc0012b2.gif)
实现思路观察
由效果图可知,这个按钮效果分两个阶段。
-
第一阶段:有两个圆,我称之为按钮圆(前景圆),背景圆。当长按按钮时:背景圆变大,按钮圆变小。此处我设置半径变化倍率为 1.5 倍,由动画控制。
-
第二阶段:当半径变化完成之后,在背景圆上画圆形进度条,进度条进度通过动画控制。
变量定义与初始化
我们需要三只画笔,分别画背景圆,按钮圆,圆形进度条,以及控制半径变化的动画值以及控制进度条的动画值。
- 定义
final double firstProgress; //第一段动画控制值,值范围[0,1]
final double secondProgress; //第二段动画控制值,值范围[0,1]
//主按钮的颜色
final Color buttonColor = Colors.white;
//进度条相关参数
final double progressWidth = 5; //进度条 宽度
final Color progressColor = Colors.green; //进度条颜色
//主按钮背后一层的颜色,也是progress绘制时的背景色
Color progressBackgroundColor;
//背景圆的画笔
Paint backGroundPaint;
//主按钮画笔
Paint btnPaint;
//进度条画笔
Paint progressPaint;
- 初始化
WeChatShotVideoBtn(this.firstProgress, this.secondProgress) {
progressBackgroundColor = buttonColor.withOpacity(0.7);
//初始化画笔
backGroundPaint = Paint()
..style = PaintingStyle.fill
..color = progressBackgroundColor;
btnPaint = Paint()
..style = PaintingStyle.fill
..color = buttonColor;
progressPaint = Paint()
..style = PaintingStyle.stroke
..color = progressColor
..strokeWidth = progressWidth;
}
绘制
画圆需要知道圆心和半径,这三个圆的圆心都是同一个,我们根据之前的分析思路画圆即可,根据第一阶段传入的动画值控制半径不断的画圆,当第一阶段动画结束后,根据第二阶段的动画值画进度条即可。具体绘制如下,注释很全。
@override
void paint(Canvas canvas, Size size) {
//初始化的圆半径,就是在动画开始前的圆半径
final double initRadius = size.width * 0.5;
// 底部最大的圆
final double center = size.width * 0.5;
//圆心
final Offset circleCenter = Offset(center, center);
//设置背景圆半径,让背景圆的半径随着动画控制值的变化,此处变为按钮圆半径的1.5倍
final double backGroundRadius = initRadius * (1 + (firstProgress / 2));
//画背景圆
canvas.drawCircle(circleCenter, backGroundRadius, backGroundPaint);
// 按钮圆,按钮圆初始半径刚开始时应减去 进度条的宽度,在长按时按钮圆半径变小
final double initBtnCircleRadius = initRadius - progressWidth;
//长按时,按钮圆半径根据动画变为初始按钮圆的1/2倍
final double circleRadius = initBtnCircleRadius * (1 / (1 + firstProgress));
//画按钮圆
canvas.drawCircle(circleCenter, circleRadius, btnPaint);
// 第二阶段,进度条的绘制,表示第二阶段动画启动
if (secondProgress > 0) {
//secondProgress 值转化为度数
final double angle = 360.0 * secondProgress;
//角度转化为弧度
final double sweepAngle = deg2Rad(angle);
final double progressCircleRadius = backGroundRadius - progressWidth;
final Rect arcRect =
Rect.fromCircle(center: circleCenter, radius: progressCircleRadius);
//这里画弧度的时候它默认起点是从3点钟方向开始
// 所以这里的开始角度向前调整90度让它从12点钟方向开始画弧
canvas.drawArc(arcRect, back90, sweepAngle, false, progressPaint);
}
}
页面控制
值得注意的是,平时我们基本都是一个动画控制器,所以混入的类是 SingleTickerProviderStateMixin,但这里有两个动画控制器,所以混入的类应该是:TickerProviderStateMixin。
接下来我们要做的就是控制动画控制器,在长按的时候启动动画控制器:
_animationController2.forward();
在取消长按的时候重置以及还原动画控制器:
_animationController2.reverse();
_animationController3.value = 0;
_animationController3.stop();
为触发这些控制,我们引入手势控件:
GestureDetector
插波广告,有兴趣的小伙伴关注公众号,和我一起学习,本文代码在文末。
![](https://img.haomeiwen.com/i3282504/733a127980687be9.jpg)
完整代码
import 'dart:ui';
import 'package:flutter/material.dart';
import 'dart:math' as math;
class PainterPageFirst extends StatefulWidget {
@override
_PainterPageFirstState createState() => _PainterPageFirstState();
}
class _PainterPageFirstState extends State<PainterPageFirst>
with TickerProviderStateMixin {
AnimationController _animationController1;
AnimationController _animationController2;
AnimationController _animationController3;
@override
void initState() {
// TODO: implement initState
super.initState();
_animationController1 =
AnimationController(duration: Duration(seconds: 2), vsync: this)
..addListener(() {
setState(() {});
})
..repeat();
_animationController2 =
AnimationController(duration: Duration(milliseconds: 500), vsync: this)
..addListener(() {
setState(() {});
})
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
//按钮过渡动画完成后启动录制视频的进度条动画
_animationController3.forward();
}
});
//第二个控制器
_animationController3 =
AnimationController(duration: Duration(seconds: 8), vsync: this)
..addListener(() {
setState(() {});
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Painter&Animation"),
),
body: Container(
margin: EdgeInsets.only(top: 10),
alignment: Alignment.center,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
width: 200,
height: 200,
color: Colors.red,
alignment: Alignment.center,
child: CustomPaint(
painter: CirclePainter1(progress: _animationController1.value),
size: Size(150, 150),
),
),
Container(
margin: EdgeInsets.only(top: 20),
width: 200,
height: 200,
color: Colors.black,
alignment: Alignment.center,
child: GestureDetector(
onLongPress: () {
_animationController2.forward();
},
onLongPressUp: () {
_animationController2.reverse();
_animationController3.value = 0;
_animationController3.stop();
},
child: CustomPaint(
painter: WeChatShotVideoBtn(
_animationController2.value, _animationController3.value),
size: Size(100, 100),
),
),
),
],
),
),
);
}
@override
void dispose() {
// TODO: implement dispose
_animationController1?.dispose();
_animationController3?.dispose();
_animationController2?.dispose();
super.dispose();
}
}
class CirclePainter1 extends CustomPainter {
Paint _paint = Paint()
..style = PaintingStyle.fill
..color = Colors.greenAccent;
final double progress;
CirclePainter1({this.progress});
@override
void paint(Canvas canvas, Size size) {
// TODO: implement paint
final double center = size.width * 0.5;
final double radius = size.width * 0.5;
// 圆的中心点位置
final Offset centerOffset = Offset(center, center);
final Rect rect = Rect.fromCircle(center: centerOffset, radius: radius);
final double startAngle = 0;
final double angle = 360.0 * progress;
final double sweepAngle = (angle * (math.pi / 180.0));
// 画圆弧 按照角度来画圆弧,后面看效果图会发现起点从0开始画的时候是3点钟方向开始的
canvas.drawArc(rect, startAngle, sweepAngle, true, _paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
return true;
}
}
class WeChatShotVideoBtn extends CustomPainter {
final double firstProgress; //第一段动画控制值,值范围[0,1]
final double secondProgress; //第二段动画控制值,值范围[0,1]
//主按钮的颜色
final Color buttonColor = Colors.white;
//进度条相关参数
final double progressWidth = 5; //进度条 宽度
final Color progressColor = Colors.green; //进度条颜色
final back90 = deg2Rad(-90.0); //往前推90度 从12点钟方向开始
//主按钮背后一层的颜色,也是progress绘制时的背景色
Color progressBackgroundColor;
//背景圆的画笔
Paint backGroundPaint;
//主按钮画笔
Paint btnPaint;
//进度条画笔
Paint progressPaint;
WeChatShotVideoBtn(this.firstProgress, this.secondProgress) {
progressBackgroundColor = buttonColor.withOpacity(0.7);
//初始化画笔
backGroundPaint = Paint()
..style = PaintingStyle.fill
..color = progressBackgroundColor;
btnPaint = Paint()
..style = PaintingStyle.fill
..color = buttonColor;
progressPaint = Paint()
..style = PaintingStyle.stroke
..color = progressColor
..strokeWidth = progressWidth;
}
@override
void paint(Canvas canvas, Size size) {
//初始化的圆半径,就是在动画开始前的圆半径
final double initRadius = size.width * 0.5;
// 底部最大的圆
final double center = size.width * 0.5;
//圆心
final Offset circleCenter = Offset(center, center);
//设置背景圆半径,让背景圆的半径随着动画控制值的变化,此处变为按钮圆半径的1.5倍
final double backGroundRadius = initRadius * (1 + (firstProgress / 2));
//画背景圆
canvas.drawCircle(circleCenter, backGroundRadius, backGroundPaint);
// 按钮圆,按钮圆初始半径刚开始时应减去 进度条的宽度,在长按时按钮圆半径变小
final double initBtnCircleRadius = initRadius - progressWidth;
//长按时,按钮圆半径根据动画变为初始按钮圆的1/2倍
final double circleRadius = initBtnCircleRadius * (1 / (1 + firstProgress));
//画按钮圆
canvas.drawCircle(circleCenter, circleRadius, btnPaint);
// 第二阶段,进度条的绘制,表示第二阶段动画启动
if (secondProgress > 0) {
//secondProgress 值转化为度数
final double angle = 360.0 * secondProgress;
//角度转化为弧度
final double sweepAngle = deg2Rad(angle);
final double progressCircleRadius = backGroundRadius - progressWidth;
final Rect arcRect =
Rect.fromCircle(center: circleCenter, radius: progressCircleRadius);
//这里画弧度的时候它默认起点是从3点钟方向开始
// 所以这里的开始角度向前调整90度让它从12点钟方向开始画弧
canvas.drawArc(arcRect, back90, sweepAngle, false, progressPaint);
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
//角度转弧度
num deg2Rad(num deg) => deg * (math.pi / 180.0);
网友评论