以完成下面git中的效果为引导,来讲CustomPaint的使用。
首先创建画布
class RecordCustomPaint extends CustomPainter {
@override
void paint(Canvas canvas, Size size){}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
然后引用
return CustomPaint(
painter: RecordCustomPaint(),
size: widget.size,
);
}
接下来要做的就是开始创建画笔,以及设置画笔的属性。
| 属性名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| style | PaintingStyle | .fill .stroke | 画笔类型 |
| isAntiAlias | bool | true | canvas上的图片和线条是否抗锯齿 |
| color | Color | 0xFF000000 | 画笔颜色 |
| strokeWidth | double | 0.0 | 画笔宽度 |
| strokeCap | StrokeCap | StrokeCap.butt | 笔头类型 |
| strokeJoin | StrokeJoin | StrokeJoin.miter | 具体区别看StrokeJoin枚举的介绍里面有gif解析 |
| strokeMiterLimit | dobule | 4.0 | 斜接限制 |
创建画笔之前需要做需求分析。
项目有两段动画,首先是第一段动画外圈变宽。然后是第二段动画,内外圈的进度条开始。
创建动画
late AnimationController _animationController2;
late AnimationController _animationController3;
int countdownSeconds = 15;
@override
void initState() {
// TODO: implement initState
super.initState();
_animationController2 =
AnimationController(duration: Duration(milliseconds: 100), vsync: this)
..addListener(() {
setState(() {});
})
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
//按钮过渡动画完成后启动录制视频的进度条动画
_animationController3.forward();
widget.onStart();
}
});
//第二个控制器
_animationController3 =
AnimationController(duration: Duration(seconds: widget.seconds), vsync: this)
..addListener(() {
setState(() {});
print("_animationController3.value -- ${_animationController3.value}");
if (_animationController3.value == 1){
_animationController2.reverse();
_animationController3.value = 0;
_animationController3.stop();
widget.onEnd();
}
});
}
逻辑就是第一段动画结束之后开启第二段动画。
然后把这两段动画的进度传递进RecordCustomPaint类里,并在内部进行画笔的初始化。
动画中一共有内圈/内圈进度条 外圈/外圈进度条 以及内外圈之前的灰色区域(渐变色,gif图中显示不太明显)五个元素。所以需要五只画笔。
接下来的代码均在 RecordCustomPaint 类中实现
属性
final double firstProgress; //第一段动画控制值,值范围[0,1]
final double secondProgress; //第二段动画控制值,值范围[0,1]
//主按钮的颜色
final Color buttonColor = Colors.red;
final Color mainColor = kColor(223,104,104, 1);
//进度条相关参数
final double progressWidth = kFit(10); //进度条 宽度
final Color progressColor = Colors.white; //kColor(223,104,104, 1); //进度条颜色
final double back90 = deg2Rad(-90.0).toDouble(); //往前推90度 从12点钟方向开始
//主按钮画笔
late Paint mainBtnPaint;
//画笔颜色
Color mainBtnColor = Colors.white;
//主按钮进度条画笔
late Paint mainBtnProgressPaint;
//主按钮和外圈之间的阴影画笔
late Paint spacerShadowPaint;
//主按钮和外圈之间的阴影渐变色
List<Color> spacerShadowColors = [Colors.red, Colors.yellow];
//外圈画笔
late Paint outerRingPaint;
//外圈画笔颜色
Color outerRingColor = Colors.white;
//进度条画笔
late Paint progressPaint;
初始化画笔
RecordCustomPaint(this.firstProgress, this.secondProgress,Size tsize) {
// 按钮圆,按钮圆初始半径刚开始时应减去 进度条的宽度,在长按时按钮圆半径变小
final double initBtnCircleRadius = tsize.width * 0.5 - kFit(67);
//内圈按钮的背景
mainBtnPaint = Paint()
//填充模式
..style = PaintingStyle.fill
//画笔颜色
..color = mainBtnColor;
//画笔宽度 填充模式不用设置画笔宽度
//..strokeWidth = initBtnCircleRadius;
final double center = tsizeidth * 0.5;
//内圈按钮的进度条 这里没设置颜色是因为颜色需要跟着进度条渐变
mainBtnProgressPaint = Paint()
//填充模式
..style = PaintingStyle.fill
///画笔的宽度
..strokeWidth = initBtnCircleRadius;
//关于
final double outerRingRadius = initBtnCircleRadius +kFit(7.5);
//内外圈之间的渐变色 为了使看起来明显暂时使用 红到黄渐变 [Colors.red, Colors.yellow]
spacerShadowPaint = Paint()
//中空模式 空心圆
..style = PaintingStyle.stroke
..shader = ui.Gradient.radial(Offset(center, center),outerRingRadius, spacerShadowColors, null, TileMode.mirror ,null, null, 0)
//画笔宽度
..strokeWidth = kFit(15);
//按钮外圈
outerRingPaint = Paint()
..style = PaintingStyle.stroke
..color = outerRingColor
..strokeWidth = kFit(6);
//进度条
progressPaint = Paint()
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round
..strokeWidth = progressWidth;
}
style属性:
PaintingStyle.fill 实心圆 不用设置画笔宽度
PaintingStyle.stroke 空心圆
关于空心圆半径设置
在设置空心圆的时候,关于设置圆半径需要注意一下,例如你设置圆的半径是 10像素,画笔宽度是 4像素,那么画出来的空心圆直径为 8像素. 也就是说画笔的笔尖在 10像素 上,然后笔宽4像素,内外测各占用2个像素。上面就用到了这个逻辑.
看上面代码,内圈宽度是 initBtnCircleRadius ,然后内外圈中间的阴影画笔宽度是 15,那么阴影画笔的半径应该应该设置多少才能刚好跟内圈拼接上?如下:
final double outerRingRadius = initBtnCircleRadius +kFit(7.5);
上方代码中的渐变色,
//需要引入头文件
import 'dart:ui' as ui;
ui.Gradient.radial
使用方法,可以点击进去看类的介绍,比较详细。
接下来就就是要用画笔在画布上开始绘制图形。
@override
void paint(Canvas canvas, Size size) {
outerRingPaint.strokeWidth = kFit(6) + kFit(10) * firstProgress;
//进度条渐变色给一个默认透明度不然开始看起来颜色比较淡
double color_opacity = secondProgress + 0.2;
if (color_opacity > 1){
color_opacity = 1;
}
// 半径,
final double center = size.width * 0.5;
//圆心
final Offset circleCenter = Offset(center, center);
// 按钮圆,按钮圆初始半径刚开始时应减去 内外圈中间的阴影宽度和外圈进度条的宽度。
final double initBtnCircleRadius = size.width * 0.5 - kFit(72);
//逆时针90°旋转画布,因为画笔是从90°开始绘画的
canvas.translate(0.0, size.width);
canvas.rotate(back90);
//15是内外圈的间隔,6是变粗之前的外圈宽度 kFit(5) * firstProgress是外圈进度条需要变粗的宽度
final double outerRingRadius = initBtnCircleRadius + kFit(15) + kFit(6) + kFit(5) * firstProgress;
//角度转化为弧度
final double outerRingSweepAngle = deg2Rad(360.0).toDouble();
///画主按钮
_drawMainBtn(){
//角度转化为弧度
final double sweepAngle = deg2Rad(360.0).toDouble();
final Rect btnArcRect =
Rect.fromCircle(center: circleCenter, radius: initBtnCircleRadius);
canvas.drawArc(btnArcRect, 0, sweepAngle, true, mainBtnPaint);
}
//主按钮进度条
_drawMainBtnProgress(){
//内圈圆渐变色 随时动画的时间 颜色透明度慢慢变为 1
mainBtnProgressPaint.color = kColor(238, 172, 3, color_opacity);
if (secondProgress > 0) {
//secondProgress 值转化为度数
final double angle = 360.0 * secondProgress;
//角度转化为弧度
final double sweepAngle = deg2Rad(angle).toDouble();
final Rect btnArcRect =
Rect.fromCircle(center: circleCenter, radius: initBtnCircleRadius);
canvas.drawArc(btnArcRect, 0, sweepAngle, true, mainBtnProgressPaint);
}
}
//主按钮和外圈之间的阴影
_drwaSpacerShadow(){
//这里为什么+7.5 因为画笔的宽度是15 为什么这么做 上面的 “关于空心圆半径设置” 有解释
final Rect arcRect =
Rect.fromCircle(center: circleCenter, radius:initBtnCircleRadius + kFit(7.5));
canvas.drawArc(arcRect, 0, outerRingSweepAngle, false, spacerShadowPaint);
}
//外圈
_drawOuterRing(){
final Rect arcRect =
Rect.fromCircle(center: circleCenter, radius: outerRingRadius);
canvas.drawArc(arcRect, 0, outerRingSweepAngle, false, outerRingPaint);
}
//绘制外圈进度条
_drawOuterRingProgress(){
if (secondProgress > 0) {
progressPaint.color = kColor(238, 172, 3, color_opacity);
//画笔设置的 ..strokeCap = StrokeCap.round 所以起始点会超出12点钟半个笔头的宽度,把他转换为角度并旋转,这样起始点就会在正12点钟方向。
var offset = asin(progressWidth * 0.5 / outerRingRadius);
final double angle = 360.0 * secondProgress;
final double progressCircleRadius = outerRingRadius;
final double sweepAngle = deg2Rad(angle).toDouble();
final Rect arcRect =
Rect.fromCircle(center: circleCenter, radius: progressCircleRadius);
canvas.drawArc(arcRect, offset, sweepAngle, false, progressPaint);
}
}
//绘制主按钮
_drawMainBtn();
//绘制主按钮渐变色
_drawMainBtnProgress();
//阴影绘制
_drwaSpacerShadow();
//绘制外圈
_drawOuterRing();
//绘制外圈进度条
_drawOuterRingProgress();
}
到这里已经设置完毕了。 这里是完整的代码
到了这里我在想能不能设置进度条两种颜色的渐变,而不是单纯的透明度。类似于这样
这时我看到了 Paint 对象的 shader属性,
shader是一个抽象类,具体的实现有Gradient和ImageShader两种,shader不为null时color属性无效。
很明显我需要的是 Gradient。
Gradient :渐变有三种,
- liner
- radial
- sweep
想要使用它需要先引入头文件
//需要引入头文件
import 'dart:ui' as ui;
ui.Gradient.radial
然后这样即可,
ui.Gradient.radial
liner、radial、sweep他们三个的区别,mac中按command加左键进去看API,里面介绍非常清楚。
他们都有一个共同的属性
TileMode tileMode = TileMode.clamp,
TileMode是个枚举
- clamp
- repeated
- mirror
- decal
每种枚举对应的类型效果,API里面也写的非常清楚,而且提供的有png链接可以查看样式。
例如clamp效果:
通过查看API了解到,我需要的应该是
ui.Gradient.sweep
tileMode:TileMode.clamp
修改以下代码,通过 红变黄 来演示。
1.初始化画笔代码修改
//内圈进度条
mainBtnProgressPaint = Paint()
..style = PaintingStyle.fill
..shader = ui.Gradient.sweep(Offset(center, center), [Colors.red, Colors.yellow], null, TileMode.clamp ,deg2Rad(-90.0).toDouble(), math.pi * 2, null)
..strokeWidth = initBtnCircleRadius;
//完全进度条
progressPaint = Paint()
..style = PaintingStyle.stroke
..shader = ui.Gradient.sweep(Offset(center, center), [Colors.red, Colors.yellow], null, TileMode.clamp ,deg2Rad(-90.0).toDouble(), math.pi * 2, null)
..strokeCap = StrokeCap.round
..strokeWidth = progressWidth;
2.绘制代码修改
//主按钮渐变
_drawMainBtnProgress(){
//内圈圆渐变色
if (secondProgress > 0) {
//secondProgress 值转化为度数
final double angle = 360.0 * secondProgress;
//角度转化为弧度
final double sweepAngle = deg2Rad(angle).toDouble();
final Rect btnArcRect =
Rect.fromCircle(center: circleCenter, radius: initBtnCircleRadius);
canvas.drawArc(btnArcRect, 0, sweepAngle, true, mainBtnProgressPaint);
}
}
//绘制外圈进度条
_drawOuterRingProgress(){
if (secondProgress > 0) {
var offset = asin(progressWidth * 0.5 / outerRingRadius);
final double angle = 360.0 * secondProgress;
final double progressCircleRadius = outerRingRadius;
final double sweepAngle = deg2Rad(angle).toDouble();
final Rect arcRect =
Rect.fromCircle(center: circleCenter, radius: progressCircleRadius);
canvas.drawArc(arcRect, offset, sweepAngle, false, progressPaint);
}
}
编译之后效果: