Flutter CustomPaint的使用。

1,131 阅读7分钟

以完成下面git中的效果为引导,来讲CustomPaint的使用。

QQ20220808-112022-HD.gif

首先创建画布

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,
  );
}

接下来要做的就是开始创建画笔,以及设置画笔的属性。

属性名类型默认值说明
stylePaintingStyle.fill .stroke画笔类型
isAntiAliasbooltruecanvas上的图片和线条是否抗锯齿
colorColor0xFF000000画笔颜色
strokeWidthdouble0.0画笔宽度
strokeCapStrokeCapStrokeCap.butt笔头类型
strokeJoinStrokeJoinStrokeJoin.miter具体区别看StrokeJoin枚举的介绍里面有gif解析
strokeMiterLimitdobule4.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图中显示不太明显)五个元素。所以需要五只画笔。

image.png

接下来的代码均在 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();

}

到这里已经设置完毕了。 这里是完整的代码

到了这里我在想能不能设置进度条两种颜色的渐变,而不是单纯的透明度。类似于这样

image.png

这时我看到了 Paint 对象的 shader属性,

shader是一个抽象类,具体的实现有GradientImageShader两种,shader不为null时color属性无效。

很明显我需要的是 Gradient。

Gradient :渐变有三种,

  • liner
  • radial
  • sweep

想要使用它需要先引入头文件

//需要引入头文件

import 'dart:ui' as ui;
ui.Gradient.radial

然后这样即可,

ui.Gradient.radial

image.png

liner、radial、sweep他们三个的区别,mac中按command加左键进去看API,里面介绍非常清楚。

他们都有一个共同的属性

TileMode tileMode = TileMode.clamp,

TileMode是个枚举

  • clamp
  • repeated
  • mirror
  • decal

每种枚举对应的类型效果,API里面也写的非常清楚,而且提供的有png链接可以查看样式。

例如clamp效果:

1 1 1

通过查看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);
  }
}

编译之后效果:

QQ20220808-143525-HD.gif

代码文件链接