Preface: THE author is too busy recently, and can not think of a good content to write (in fact, the plug-in development series only wrote a 😭), really pigeons for a long time. I’m going to focus on the cool UI implementation, playing the Canvas, animation, and Render layers first.

As a front-end developer, WHEN it comes to animation effects, I always want artists to input JSON files and then load them by Lottie, which ensures both efficiency and performance. 😄 Until the day I saw a wavy progress ball, I couldn’t think of any reason to make it into a GIF or JSON file. After all, the loading progress is completely controlled by the code. Der ~ play it yourself. The author spent the whole Sunday afternoon with the family kung fu tea, and finally developed a decent one, which can keep around 60 frames in performance debug environment.View the source code point here, the preview effect is shown below:

I divided this dynamic effect into three layers of Canvas: circular background, circular progress bar and two layers of waves. 3 animation: arc forward animation, two layers of wave movement animation.

  1. First, draw the circular background. It is very simple to draw a circle. There is no need to redraw this canvas.
import 'package:flutter/material.dart';

class RoundBasePainter extends CustomPainter {
  final Color color;

  RoundBasePainter(this.color);

  @override
  voidpaint(Canvas canvas, Size size) { Paint paint = Paint() .. isAntiAlias =true. style = PaintingStyle.stroke .. strokeWidth =7.0
      ..color = color;
    // Draw the background of the progress bar
    canvas.drawCircle(size.center(Offset.zero), size.width / 2, paint);
    // Save the canvas state
    canvas.save();
    // Restore the canvas state
    canvas.restore();
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
Copy the code
  1. To draw the arc progress bar, we need to understand that our starting point is -90°, and we need to convert the progress into Angle to draw the arc;
import 'dart:math';

import 'package:flutter/material.dart';

class RoundProgressPainter extends CustomPainter {
  final Color color;
  final double progress;

  RoundProgressPainter(this.color, this.progress);

  @override
  voidpaint(Canvas canvas, Size size) { Paint paint = Paint() .. isAntiAlias =true. style = PaintingStyle.stroke .. strokeWidth =7.0
      ..color = color;
    / / draw arc
    canvas.drawArc(
        Rect.fromCircle(
            center: size.center(Offset.zero), radius: size.width / 2),
        -pi / 2.// The starting point is -90°
        pi * 2 * progress, / / progress * 360 °
        false,
        paint);
    canvas.save();
    canvas.restore();
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true; }}Copy the code
  1. To draw waves, the principle is as follows: Curve is realized by Bezier curve, then a complete region is connected by PATH (see figure), and then this region is cut into a circle by clip.

Code for dart is fast:

import 'package:flutter/material.dart';

class WavyPainter extends CustomPainter {
  // Wave curvature
  final double waveHeight;
  / / progress [0, 1]
  final double progress;
  // Offset the wave area in the X-axis direction to achieve the scrolling effect
  final double offsetX;

  final Color color;

  WavyPainter(this.progress, this.offsetX, this.color, {this.waveHeight = 24});

  @override
  voidpaint(Canvas canvas, Size size) { Paint paint = Paint() .. isAntiAlias =true. style = PaintingStyle.fill .. strokeWidth =1.5
      ..color = color;
    drawWave(canvas, size.center(Offset(0.0)), size.width / 2, paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;
  }

  void drawWave(Canvas canvas, Offset center, double radius, Paint paint) {
    // Circle clippingcanvas.save(); Path clipPath = Path() .. addOval(Rect.fromCircle(center: center, radius: radius)); canvas.clipPath(clipPath);// Compute the ordinate of the point backwards
    double wavePointY = (1 - progress) * radius * 2;

    // Point3 is the center point, the diameter of the wave is the radius of the circle, a total of 5 points, plus two closed loop points (p6, P7)
    Offset point1 = Offset(center.dx - radius * 3 + offsetX, wavePointY);
    Offset point2 = Offset(center.dx - radius * 2 + offsetX, wavePointY);
    Offset point3 = Offset(center.dx - radius + offsetX, wavePointY);
    Offset point4 = Offset(center.dx + offsetX, wavePointY);
    Offset point5 = Offset(center.dx + radius + offsetX, wavePointY);

    Offset point6 = Offset(point5.dx, center.dy + radius + waveHeight);
    Offset point7 = Offset(point1.dx, center.dy + radius + waveHeight);

    // Bezier curve control point
    Offset c1 =
        Offset(center.dx - radius * 2.5 + offsetX, wavePointY + waveHeight);
    Offset c2 =
        Offset(center.dx - radius * 1.5 + offsetX, wavePointY - waveHeight);
    Offset c3 =
        Offset(center.dx - radius * 0.5 + offsetX, wavePointY + waveHeight);
    Offset c4 =
        Offset(center.dx + radius * 0.5 + offsetX, wavePointY - waveHeight);

    // Connect bezier curvesPath wavePath = Path() .. moveTo(point1.dx, point1.dy) .. quadraticBezierTo(c1.dx, c1.dy, point2.dx, point2.dy) .. quadraticBezierTo(c2.dx, c2.dy, point3.dx, point3.dy) .. quadraticBezierTo(c3.dx, c3.dy, point4.dx, point4.dy) .. quadraticBezierTo(c4.dx, c4.dy, point5.dx, point5.dy) .. lineTo(point6.dx, point6.dy) .. lineTo(point7.dx, point7.dy) .. close();/ / to drawcanvas.drawPath(wavePath, paint); canvas.restore(); }}Copy the code
  1. Add animation. Focus on the animation of the wave, which is actually aboveBezier curve area, the X-axis direction of repeated uniform movement, coupled with the effect of bezier curve, can produce up and down wave effect. And through the following figure, it can be determinedThe translation distance is the diameter of the circle.

I created the Package, and the code below is the actual implementation of the controls exposed to the caller, as well as all the animation.

library round_wavy_progress;

import 'package:flutter/material.dart';
import 'package:round_wavy_progress/painter/round_base_painter.dart';
import 'package:round_wavy_progress/painter/round_progress_painter.dart';
import 'package:round_wavy_progress/painter/wavy_painter.dart';
import 'package:round_wavy_progress/progress_controller.dart';

class RoundWavyProgress extends StatefulWidget {
  RoundWavyProgress(this.progress, this.radius, this.controller,
      {Key? key,
      this.mainColor,
      this.secondaryColor,
      this.roundSideColor = Colors.grey,
      this.roundProgressColor = Colors.white})
      : super(key: key);

  final double progress;
  final double radius;
  final ProgressController controller;
  final Color? mainColor;
  final Color? secondaryColor;
  final Color roundSideColor;
  final Color roundProgressColor;

  @override
  _RoundWavyProgressState createState() => _RoundWavyProgressState();
}

class _RoundWavyProgressState extends State<RoundWavyProgress>
    with TickerProviderStateMixin {
  late AnimationController wareController;
  late AnimationController mainController;
  late AnimationController secondController;

  late Animation<double> waveAnimation;
  late Animation<double> mainAnimation;
  late Animation<double> secondAnimation;

  double currentProgress = 0.0;

  @override
  void initState() {
    super.initState();
    widget.controller.stream.listen((event) {
      print(event);
      wareController.reset();
      waveAnimation = Tween(begin: currentProgress, end: event as double)
          .animate(wareController);
      currentProgress = event;
      wareController.forward();
    });

    wareController = AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 1200)); mainController = AnimationController( vsync:this,
      duration: Duration(milliseconds: 3200)); secondController = AnimationController( vsync:this,
      duration: Duration(milliseconds: 1800)); waveAnimation = Tween(begin: currentProgress, end: widget.progress) .animate(wareController); mainAnimation = Tween(begin:0.0, end: widget.radius * 2).animate(mainController);
    secondAnimation =
        Tween(begin: widget.radius * 2, end: 0.0).animate(secondController);

    wareController.forward();
    mainController.repeat();
    secondController.repeat();
  }

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(builder: (context, constraints) {
      final viewportSize = Size(constraints.maxWidth, constraints.maxHeight);
      return AnimatedBuilder(
          animation: mainAnimation,
          builder: (BuildContext ctx, Widget? child) {
            return AnimatedBuilder(
                animation: secondAnimation,
                builder: (BuildContext ctx, Widget? child) {
                  return AnimatedBuilder(
                      animation: waveAnimation,
                      builder: (BuildContext ctx, Widget? child) {
                        return Stack(
                          children: [
                            RepaintBoundary(
                              child: CustomPaint(
                                size: viewportSize,
                                painter: WavyPainter(
                                    waveAnimation.value,
                                    mainAnimation.value,
                                    widget.mainColor ??
                                        Theme.of(context).primaryColor),
                                child: RepaintBoundary(
                                  child: CustomPaint(
                                    size: viewportSize,
                                    painter: WavyPainter(
                                        waveAnimation.value,
                                        secondAnimation.value,
                                        widget.secondaryColor ??
                                            Theme.of(context)
                                                .primaryColor
                                                .withOpacity(0.5)),
                                    child: RepaintBoundary(
                                      child: CustomPaint(
                                        size: viewportSize,
                                        painter: RoundBasePainter(
                                            widget.roundSideColor),
                                        child: RepaintBoundary(
                                          child: CustomPaint(
                                            size: viewportSize,
                                            painter: RoundProgressPainter(
                                                widget.roundProgressColor,
                                                waveAnimation.value),
                                          ),
                                        ),
                                      ),
                                    ),
                                  ),
                                ),
                              ),
                            ),
                            Align(
                              alignment: Alignment.center,
                              child: Text(
                                '${(waveAnimation.value * 100).toStringAsFixed(2)}% ',
                                style: TextStyle(
                                    fontSize: 18, color: widget.roundProgressColor, fontWeight: FontWeight.bold), ), ), ], ); }); }); }); }); }}Copy the code

I’m not going to go into detail here. It’s not too difficult. I would like to tell you the benefits of RepaintBoundary, which can control the redrawing of its smaller particle size under the control of this control. (For details, please check:Pub. Flutter – IO. Cn/documentati…At the same time, the nesting of AnimatedBuilder is really disgusting. Due to time constraints, I have not checked to see if there is a better implementation, but at least this implementation is very good in terms of performance and effect.

At the end of the day, this progress ball has been uploaded to my GitHub. Welcome fork and Star. I will spend some time this week refining, abstracting a cleaner API and publishing it to pub; At the same time, there are more cool UI is being written, I will try to spare spare time to write better articles and communicate with you, come on 💪🏻~~~

I hope we can learn and progress together!!