美文网首页
Flutter canvas波浪加载组件

Flutter canvas波浪加载组件

作者: 倪大头 | 来源:发表于2022-11-25 14:11 被阅读0次
    28350_1669639768.gif

    创建canvas画板:

    return CustomPaint(
        size: Size(100, 100),
        painter: WavePainter(
          progress: 0.5,
          waveColor: Colors.blue,
        ),
    )
    

    创建一个WavePainter继承CustomPainter:

    class WavePainter extends CustomPainter {
      final double progress;
      final Color waveColor;
    
      WavePainter({
        required this.progress,
        required this.waveColor,
      });
    
      final Paint wavePaint = Paint();
      double painterHeight = 0; // 波浪总体高度
      double waveWidth = 0; // 波浪宽度
      double waveHeight = 0; // 波浪浪尖高度
    
      @override
      void paint(Canvas canvas, Size size) {
        painterHeight = size.height;
        waveWidth = size.width / 2;
        waveHeight = size.height * 0.06;
    
        // 绘制波浪
        drawWave(
          canvas,
          Offset(-4 * waveWidth,
              painterHeight + waveHeight),
          waveColor,
        );
      }
    
      Path drawWave(Canvas canvas, Offset startPoint, Color color) {
        Path wavePath = Path();
        wavePath.moveTo(startPoint.dx, startPoint.dy);
        wavePath.relativeLineTo(0, -painterHeight * progress);
    
        int waveCount = 3;
        for (int i = 0; i < waveCount; i++) {
          wavePath.relativeQuadraticBezierTo(
              waveWidth / 2, -waveHeight * 2, waveWidth, 0);
          wavePath.relativeQuadraticBezierTo(
              waveWidth / 2, waveHeight * 2, waveWidth, 0);
        }
        wavePath.relativeLineTo(0, painterHeight);
        wavePath.relativeLineTo(-waveWidth * waveCount * 2.0, 0);
        canvas.drawPath(wavePath, wavePaint..color = color);
        return wavePath;
      }
    
      @override
      bool shouldRepaint(WavePainter oldDelegate) {
        return false;
      }
    }
    

    绘制波浪的方法写在drawWave中,在CustomPaint外面套一个Container看下效果先:


    位图1.png

    Container的clipBehavior属性去掉可以砍出波浪的具体位置,现在波浪是静止的

    下面波浪动起来,给WavePainter传入一个Animation<double>动画:

    AnimationController _waveCtrl;
    _waveCtrl = AnimationController(
          duration: Duration(seconds: 1),
          vsync: this,
    )..repeat();
    
    WavePainter(
       waveColor: widget.waveColor,
       progress: progress,
       flow: _waveCtrl,
    )
    
    class WavePainter extends CustomPainter {
      final double progress;
      final Color waveColor;
      final Animation<double> flow;
    
      WavePainter({
        required this.progress,
        required this.waveColor,
        required this.flow,
      }) : super(repaint: flow);
    
      // 省略重复代码...
    
      @override
      void paint(Canvas canvas, Size size) {
        // 省略重复代码...
    
        // 绘制波浪
        drawWave(
          canvas,
          Offset(-4 * waveWidth + 2 * waveWidth * flow.value,
              painterHeight + waveHeight),
          waveColor,
        );
      }
    
      Path drawWave(Canvas canvas, Offset startPoint, Color color) {
        // 省略重复代码...
      }
    
      @override
      bool shouldRepaint(WavePainter oldDelegate) {
        return oldDelegate.flow != flow;
      }
    }
    
    219_1669687025.gif

    再来一道底波:

    @override
    void paint(Canvas canvas, Size size) {
      // 省略重复代码...
    
      // 绘制波浪
      drawWave(
        canvas,
        Offset(-4 * waveWidth + 2 * waveWidth * flow.value,
            painterHeight + waveHeight),
        waveColor,
      );
    
      // 绘制底波
      drawWave(
        canvas,
        Offset(-4 * waveWidth + 4 * waveWidth * flow.value,
            painterHeight + waveHeight),
        waveColor.withAlpha(80),
      );
    }
    

    底波横向移动速度翻倍4 * waveWidth * flow.value,透明度80%
    外层Container的clipBehavior属性改为Clip.antiAlias,再动态更新progress的值:


    220.gif

    接下来加上文字:

    @override
    void paint(Canvas canvas, Size size) {
      // 省略重复代码...
    
      // 绘制文字
      drawText(canvas, size, textColor);
    
      // 绘制波浪
      drawWave(
        canvas,
        Offset(-4 * waveWidth + 2 * waveWidth * flow.value,
            painterHeight + waveHeight),
        waveColor,
      );
    
      // 绘制底波
      drawWave(
        canvas,
        Offset(-4 * waveWidth + 4 * waveWidth * flow.value,
            painterHeight + waveHeight),
        waveColor.withAlpha(80),
      );
    }
    
    void drawText(Canvas canvas, Size size, Color color) {
        // 文字内容
        String text = '加载中...';
    
        // 文字样式
        TextStyle textStyle = TextStyle(
          fontSize: 15,
          fontWeight: FontWeight.bold,
          color: color,
        );
    
        // 最大行数
        int maxLines = 2;
    
        // 文字画笔
        _textPainter
          ..text = TextSpan(
            text: text,
            style: textStyle,
          )
          ..maxLines = maxLines
          ..textDirection = TextDirection.ltr;
    
        // 绘制文字
        _textPainter.layout(maxWidth: size.width);
        // 文字Size
        Size textSize = _textPainter.size;
        _textPainter.paint(
          canvas,
          Offset(
            (size.width - textSize.width) / 2,
            size.height / 2 + (size.height / 2 - textSize.height) / 2,
          ),
        );
      }
    

    文字绘制完发现和波浪混一起就看不到了


    位图.png

    中间尝试过用blendMode和colorFilter让文字和波浪重叠的部分混色,效果不太理想
    采取另一种办法,绘制两遍文字,波浪上方绘制一遍,波浪下方绘制一遍:

    @override
    void paint(Canvas canvas, Size size) {
      // 省略重复代码...
    
      // 绘制波浪上方文字
      drawText(canvas, size, textColor);
    
      // 绘制波浪
      Path wavePath = drawWave(
        canvas,
        Offset(-4 * waveWidth + 2 * waveWidth * flow.value,
            painterHeight + waveHeight),
        waveColor,
      );
    
      // 绘制底波
      drawWave(
        canvas,
        Offset(-4 * waveWidth + 4 * waveWidth * flow.value,
            painterHeight + waveHeight),
        waveColor.withAlpha(80),
      );
    
      // 绘制波浪下方文字
      canvas.clipPath(wavePath);
      drawText(canvas, size, Colors.white);
    }
    

    绘制波浪下方文字时用clipPath沿着波浪wavePath裁剪了一下,这样文字就只在波浪上显示了,不会超出波浪范围:


    位图.png

    完整代码,对波浪组件进行了封装:

    import 'dart:async';
    import 'package:flutter/material.dart';
    
    class WaveLoading extends StatefulWidget {
      // 进度
      final double progress;
      // 波浪颜色
      final Color waveColor;
      // 尺寸
      final Size size;
      // 圆角半径
      final double borderRadius;
      // 动画时长
      final Duration duration;
      // 文字
      final String text;
      // 字号
      final double fontSize;
      // 文字颜色
      final Color? textColor;
      // 是否需要省略号
      final bool needEllipsis;
    
      const WaveLoading({
        Key? key,
        this.progress = 0.6,
        this.waveColor = Colors.blue,
        this.size = const Size(100, 100),
        this.borderRadius = 0,
        this.duration = const Duration(seconds: 1),
        this.text = '加载中',
        this.fontSize = 15,
        this.textColor,
        this.needEllipsis = true,
      }) : super(key: key);
    
      @override
      State<WaveLoading> createState() => _WaveLoadingState();
    }
    
    class _WaveLoadingState extends State<WaveLoading>
        with TickerProviderStateMixin {
      late AnimationController _waveCtrl;
      Timer? _timer;
      ValueNotifier<int> _ellipsisCount = ValueNotifier(1); // 文字省略号点的个数
    
      @override
      void initState() {
        super.initState();
        // 初始化动画控制器
        _initAnimationCtrl();
      }
    
      @override
      void dispose() {
        _waveCtrl.dispose();
        _timer?.cancel();
        super.dispose();
      }
    
      // 初始化动画控制器
      void _initAnimationCtrl() {
        _waveCtrl = AnimationController(
          duration: widget.duration,
          vsync: this,
        )..repeat();
    
        if (widget.needEllipsis) {
          // 有省略号才初始化计时器
          _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
            if (_ellipsisCount.value < 3) {
              _ellipsisCount.value++;
            } else {
              _ellipsisCount.value = 1;
            }
          });
        }
      }
    
      @override
      Widget build(BuildContext context) {
        double progress = widget.progress > 1 ? 1 : widget.progress;
    
        return RepaintBoundary(
          child: CustomPaint(
            size: widget.size,
            painter: WavePainter(
              waveColor: widget.waveColor,
              borderRadius: widget.borderRadius,
              progress: progress,
              repaint: Listenable.merge([_waveCtrl, _ellipsisCount]),
              flow: _waveCtrl,
              ellipsisCount: _ellipsisCount,
              text: widget.text,
              textColor: widget.textColor ?? widget.waveColor,
              fontSize: widget.fontSize,
              needEllipsis: widget.needEllipsis,
            ),
          ),
        );
      }
    }
    
    class WavePainter extends CustomPainter {
      final Listenable repaint;
      final Animation<double> flow;
      final ValueNotifier<int> ellipsisCount;
      final double progress;
      final Color waveColor;
      final double borderRadius;
      final String text;
      final double fontSize;
      final Color textColor;
      final bool needEllipsis;
    
      WavePainter({
        required this.repaint,
        required this.flow,
        required this.ellipsisCount,
        required this.progress,
        required this.waveColor,
        required this.borderRadius,
        required this.text,
        required this.fontSize,
        required this.textColor,
        required this.needEllipsis,
      }) : super(repaint: repaint);
    
      final Paint wavePaint = Paint();
      final Paint borderPaint = Paint();
      final TextPainter _textPainter = TextPainter();
      double painterHeight = 0;
      double waveWidth = 0;
      double waveHeight = 0;
    
      @override
      void paint(Canvas canvas, Size size) {
        painterHeight = size.height;
        waveWidth = size.width / 2;
        waveHeight = size.height * 0.06;
    
        borderPaint
          ..style = PaintingStyle.fill
          ..color = waveColor.withAlpha(15);
    
        // 绘制背景
        Path borderPath = Path();
        borderPath.addRRect(
            RRect.fromRectXY(Offset.zero & size, borderRadius, borderRadius));
        canvas.clipPath(borderPath);
        canvas.drawPath(borderPath, borderPaint);
    
        // 绘制波浪上方文字
        drawText(canvas, size, textColor);
    
        // 绘制波浪
        Path wavePath = drawWave(
          canvas,
          Offset(-4 * waveWidth + 2 * waveWidth * flow.value,
              painterHeight + waveHeight),
          waveColor,
        );
    
        // 绘制底波
        drawWave(
          canvas,
          Offset(-4 * waveWidth + 4 * waveWidth * flow.value,
              painterHeight + waveHeight),
          waveColor.withAlpha(80),
        );
    
        // 绘制波浪下方文字
        canvas.clipPath(wavePath);
        drawText(canvas, size, Colors.white);
      }
    
      Path drawWave(Canvas canvas, Offset startPoint, Color color) {
        Path wavePath = Path();
        wavePath.moveTo(startPoint.dx, startPoint.dy);
        wavePath.relativeLineTo(0, -painterHeight * progress);
    
        int waveCount = 3;
        for (int i = 0; i < waveCount; i++) {
          wavePath.relativeQuadraticBezierTo(
              waveWidth / 2, -waveHeight * 2, waveWidth, 0);
          wavePath.relativeQuadraticBezierTo(
              waveWidth / 2, waveHeight * 2, waveWidth, 0);
        }
        wavePath.relativeLineTo(0, painterHeight);
        wavePath.relativeLineTo(-waveWidth * waveCount * 2.0, 0);
        canvas.drawPath(wavePath, wavePaint..color = color);
        return wavePath;
      }
    
      void drawText(Canvas canvas, Size size, Color color) {
        // 文字内容
        String content = text;
        if (needEllipsis) {
          String ellipsis = '.' * ellipsisCount.value;
          content += ellipsis.toString();
        }
    
        // 文字样式
        TextStyle textStyle = TextStyle(
          fontSize: fontSize,
          fontWeight: FontWeight.bold,
          color: color,
        );
    
        // 最大行数
        int maxLines = 2;
    
        // 文字画笔
        _textPainter
          ..text = TextSpan(
            text: content,
            style: textStyle,
          )
          ..maxLines = maxLines
          ..textDirection = TextDirection.ltr;
    
        // 文字Size,如果repaint不为空,说明需要省略号,计算时拼接上三个点,得出最大宽度
        Size textSize = sizeWithLabel(
          needEllipsis ? text + '...' : text,
          textStyle,
          maxLines,
        );
    
        // 绘制文字
        _textPainter.layout(maxWidth: size.width);
        _textPainter.paint(
          canvas,
          Offset(
            (size.width - textSize.width) / 2,
            size.height / 2 + (size.height / 2 - textSize.height) / 2,
          ),
        );
      }
    
      // 计算文字Size
      Size sizeWithLabel(String text, TextStyle textStyle, int maxLines) {
        TextSpan textSpan = TextSpan(text: text, style: textStyle);
        TextPainter textPainter = TextPainter(
            text: textSpan, maxLines: maxLines, textDirection: TextDirection.ltr);
        textPainter.layout();
    
        return textPainter.size;
      }
    
      @override
      bool shouldRepaint(WavePainter oldDelegate) {
        return oldDelegate.repaint != repaint ||
            oldDelegate.flow != flow ||
            oldDelegate.progress != progress ||
            oldDelegate.waveColor != waveColor ||
            oldDelegate.borderRadius != borderRadius ||
            oldDelegate.text != text ||
            oldDelegate.textColor != textColor ||
            oldDelegate.fontSize != fontSize ||
            oldDelegate.needEllipsis != needEllipsis ||
            oldDelegate.ellipsisCount != ellipsisCount;
      }
    }
    

    使用:

    TextButton(
      child: Text(
        'show',
        style: TextStyle(
          fontSize: 30,
        ),
      ),
      onPressed: () {
         showDialog(
            context: context,
            barrierDismissible: true,
            barrierColor: Colors.transparent,
            builder: (context) {
              return Center(
                 child: Container(
                   decoration: BoxDecoration(
                     color: Colors.white,
                     borderRadius: BorderRadius.circular(10.w),
                     // 阴影
                     boxShadow: const [
                        BoxShadow(
                           color: Colors.grey,
                           offset: Offset(0, 1), // 阴影xy轴偏移量
                           blurRadius: 0.1, // 阴影模糊程度
                           spreadRadius: 0.1, // 阴影扩散程度
                        ),
                    ],
                  ),
                  clipBehavior: Clip.antiAlias,
                  child: Obx(() {
                    // 这里为了实时刷新,使用了Getx状态管理框架,换成其他方式亦可
                    return WaveLoading(
                       size: Size(200.w, 200.w),
                       progress: progress.value / 100,
                       text: '${progress.value}%',
                       fontSize: 36.sp,
                       needEllipsis: false,
                    );
                  }),
                ),
             );
           },
         ).then((value) {
             progress.value = 0;
             timer?.cancel();
         });
         // 模拟progress更新
         timer = Timer.periodic(Duration(milliseconds: 100), (timer) {
             if (progress.value < 100) {
                progress.value++;
             }
             // debugPrint(progress.value.toString());
         });
      },
    )
    

    相关文章

      网友评论

          本文标题:Flutter canvas波浪加载组件

          本文链接:https://www.haomeiwen.com/subject/jacxfdtx.html