One, foreword

I have analyzed the implementation process of a complex Loading animation in my previous article on iOS Complex Animation. Now I have deliberately watched the animation of Flutter, so I have an idea to use Flutter to realize this animation as a practice. Finally, the effect of Flutter is as follows

Now let’s re-analyze the implementation process of this animation

Step analysis of animation

The animation in the above image does seem a bit complicated at first glance, but as we step through it, we’ll see that it’s not that difficult. Take a closer look and you will find that the general steps are as follows:

1, first out of a circle

2. A process in which a circle is squeezed horizontally and vertically, forming an elliptical shape and eventually returning to a circle

3, the lower left corner of the circle, the lower right corner and the top of the circle protrude a small part in sequence respectively (the inner triangle is stretched)

4. The shape formed by the circle and bulge becomes a triangle after being rotated around (the triangle stays the same and the circle shrinks)

5. On the left side of the triangle, there are two animations for drawing rectangular borders, enclosing the triangle in the rectangle

The rectangle is filled with waves from the bottom up

7. Enlarge the filled rectangle to full screen and pop out Welcome

The general steps are as above, and we will implement each step step by step.

Third, pull the silk from the cocoon

1. The analysis

Since everything in Flutter is a widget, we probably need the following widgets first based on our analysis

  • circular
  • Triangle,
  • Two rectangular borders
  • The waves
  • TextThe text

First we need to create an animation controller, then the animation three elements in turn

  • AnimationController
  • CurvedAnimation
  • Tween

2. Implement circular changes

(w -> width, h -> height)

  • (h = 0, w = 0)There is no circle
  • (h = 120, w = 120)The circle goes from small to large
  • (h = 120, w = 120) -> (h = 120, w = 120) -> (h = 120, w = 120) -> (h = 120, w = 120) -> (h = 120, w = 120) -> (h = 120, w = 120)The process by which a circle becomes an ellipse
  • (h = 0, w = 0)Circular disappear

The above process is how the circle changes over the entire animation cycle, so we use TweenSequence to realize the time proportion of each time period

  // The width of the circle changes at the beginning
  static TweenSequence circleWidthTweenSequence = TweenSequence([
    TweenSequenceItem(tween: Tween(begin: 0.0, end: 120.0), weight: 5),
    TweenSequenceItem(tween: Tween(begin: 120.0, end: 130.0), weight: 10),
    TweenSequenceItem(tween: Tween(begin: 130.0, end: 120.0), weight: 10),
    TweenSequenceItem(tween: ConstantTween<double> (120.0), weight: 20),
    TweenSequenceItem(tween: Tween(begin: 120.0, end: 0.0), weight: 10),
    TweenSequenceItem(tween: ConstantTween<double> (0.0), weight: 45),]);// The initial height change of the circle
  static TweenSequence circleHeightTweenSequence = TweenSequence([
    TweenSequenceItem(tween: Tween(begin: 0.0, end: 120.0), weight: 5),
    TweenSequenceItem(tween: ConstantTween<double> (120.0), weight: 20),
    TweenSequenceItem(tween: Tween(begin: 120.0, end: 130.0), weight: 10),
    TweenSequenceItem(tween: Tween(begin: 130.0, end: 120.0), weight: 10),
    TweenSequenceItem(tween: Tween(begin: 120.0, end: 0.0), weight: 10),
    TweenSequenceItem(tween: ConstantTween<double> (0.0), weight: 45),]);Copy the code

So based on that, you can animate the entire life cycle of the circle

.@override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (BuildContext context, Widget child) {
        return Center(
          child: ClipRRect(
            borderRadius: BorderRadius.circular(60), child: Container( color: Colors.purple, height: _circleHeightTween.value, width: _circleWidthTween.value, ), ), ); }); }Copy the code

Results the following

3. Implement triangle changes

The process of triangle change is actually very simple, mainly the following steps

  • Triangles go from 0 to large
  • The left, right and top angles of the triangle are elongated
  • rotating

Knowing how the triangle changes, we first need to draw a triangle. Since we don’t have triangle widgets, we need to do this manually. It is also easy to implement complex shapes in Flutter. Flutter provides us with a CustomPainter abstract class. We just inherit and implement paint and shouldRepaint

class TrianglePainter extends CustomPainter { Color color; Paint _paint = Paint() .. strokeWidth =5.0. color = Colors.purple .. isAntiAlias =true
    ..strokeJoin = StrokeJoin.round;
  Path _path = Path();
  double left, right, top;
  TrianglePainter({this.left, this.right, this.top});

  @override
  void paint(Canvas canvas, Size size) {
    final _width = size.width;
    final _height = size.height;
    _path.moveTo(left * _width, 0.85 * _height);
    _path.lineTo(right * _width, 0.85 * _height);
    _path.lineTo(0.5 * _width, top * _height);
    _path.close();
    canvas.drawPath(_path, _paint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => true;
}
Copy the code

And then the Tween of the triangle is this

// Triangle size changes
  static TweenSequence triangleSizeTweenSequence = TweenSequence([
    TweenSequenceItem(tween: Tween(begin: 0.0, end: 120.0), weight: 15),
    TweenSequenceItem(tween: ConstantTween<double> (120.0), weight: 85),]);// The left, right, and top changes of a triangle
  static TweenSequence triangleLeftTweenSequence = TweenSequence([
    TweenSequenceItem(tween: ConstantTween<double> (0.2), weight: 15),
    TweenSequenceItem(tween: Tween(begin: 0.2, end: 0.02), weight: 10),
    TweenSequenceItem(tween: ConstantTween<double> (0.02), weight: 75),]);static TweenSequence triangleRightTweenSequence = TweenSequence([
    TweenSequenceItem(tween: ConstantTween<double> (0.8), weight: 25),
    TweenSequenceItem(tween: Tween(begin: 0.8, end: 0.98), weight: 10),
    TweenSequenceItem(tween: ConstantTween<double> (0.98), weight: 65),]);static TweenSequence triangleTopTweenSequence = TweenSequence([
    TweenSequenceItem(tween: ConstantTween<double> (0.05), weight: 35),
    TweenSequenceItem(tween: Tween(begin: 0.05, end: 0.1), weight: 10),
    TweenSequenceItem(tween: ConstantTween<double> (0.1), weight: 55),]);// The overall rotation process
  static TweenSequence rotationTweenSequence = TweenSequence([
    TweenSequenceItem(tween: ConstantTween<double> (0.0), weight: 45),
    TweenSequenceItem(tween: Tween(begin: 0.0, end: 2.0), weight: 10),
    TweenSequenceItem(tween: ConstantTween<double> (2.0), weight: 45),]);Copy the code

Finally get the Tween value to render the animation

.@override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (BuildContext context, Widget child) {
        returnCenter( child: Transform( alignment: Alignment.center, transform: Matrix4.rotationZ(Math.pi * _rotationTween.value), child: Container( height: _triangleSizeTween.value, width: _triangleSizeTween.value, child: CustomPaint( painter: TrianglePainter( left: _triangleLeftTween.value, right: _triangleRightTween.value, top: _triangleTopTween.value, ), ), ), ), ); }); }Copy the code

The final triangle animation changes as follows

4. Change the rectangle box

Also, we have to use CustomPainter for rectangle changes

class SquarePainter extends CustomPainter {
  double progress;
  Color color;
  finalPaint _paint = Paint() .. strokeCap = StrokeCap.round .. style = PaintingStyle.stroke .. strokeWidth =5;
  SquarePainter({this.progress, this.color = Colors.purple});

  @override
  void paint(Canvas canvas, Size size) {
    _paint.color = color;
    if (progress > 0) {
      var path = createPath(4, size.width);
      PathMetric pathMetric = path.computeMetrics().first;
      Path extractPath =
          pathMetric.extractPath(0.0, pathMetric.length * progress); canvas.drawPath(extractPath, _paint); }}@override
  bool shouldRepaint(CustomPainter oldDelegate) => true;

  Path createPath(int sides, double radius) {
    Path path = Path();
    // Draw a rectangle according to the coefficients of the triangle
    double wFartor = 0.02; / / lower left
    double hFactor = 0.85; / / right
    double tFactor = 0.10; // Top triangle
    path.moveTo(wFartor * radius, hFactor * radius);
    for (int i = 1; i <= sides; i++) {
      double x, y;
      if (i == 1) {
        x = wFartor * radius;
        y = -tFactor * radius;
      } else if (i == 2) {
        x = radius;
        y = -tFactor * radius;
      } else if (i == 3) {
        x = radius;
        y = radius * hFactor;
      } else {
        x = wFartor * radius;
        y = radius * hFactor;
      }
      path.lineTo(x, y);
    }
    path.close();
    returnpath; }}Copy the code

Note that we need to use PathMetric to get the path, similar to the Android pathMeasure.getSegment (), where the Tween of the two linear rectangles is shown below

 // Linear rectangle changes
  static TweenSequence rectTweenSequence1 = TweenSequence([
    TweenSequenceItem(tween: ConstantTween<double> (0.0), weight: 55),
    TweenSequenceItem(tween: Tween(begin: 0.0, end: 1.0), weight: 10),
    TweenSequenceItem(tween: Tween(begin: 1.0, end: 1.0), weight: 35),]);static TweenSequence rectTweenSequence2 = TweenSequence([
    TweenSequenceItem(tween: ConstantTween<double> (0.0), weight: 65),
    TweenSequenceItem(tween: Tween(begin: 0.0, end: 1.0), weight: 10),
    TweenSequenceItem(tween: ConstantTween<double> (1.0), weight: 25),]);Copy the code

Since it’s a change of two rectangles, we use Stack wrap

.@override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (BuildContext context, Widget child) {
        return Center(
          child: Transform(
            alignment: Alignment.center,
            transform: Matrix4.rotationZ(Math.pi * _rotationTween.value),
            child: Stack(
              alignment: Alignment.center,
              children: [
                Container(
                  height: _triangleSizeTween.value,
                  width: _triangleSizeTween.value,
                  child: CustomPaint(
                    painter: SquarePainter(progress: _rect1Tween.value),
                  ),
                ),
                Container(
                  height: _triangleSizeTween.value,
                  width: _triangleSizeTween.value,
                  child: CustomPaint(
                    painter: SquarePainter(
                      progress: _rect2Tween.value,
                      color: Color(0xff40e0b0),),),)],),),); }); }Copy the code

The final result is as follows

5. Achieve water wave change and amplification effect

Realizing the change of water wave is a little more complicated, because all the animations in the whole process are controlled by an AnimationController, so we also need an Animation to control the effect of the oscillation of water wave. But our _HWAnimatePageState is inherited in SingleTickerProviderStateMixin, there is only a ` AnimationController. Based on this, the wave-animation is extracted into a separate widget, which can be seen in the custom WAVe_progress source code. Draw the wave-code as follows

  // Draw a water ripple animation
   Paint wavePaint = newPaint().. color = waveColor;// Water wave amplitude
   double amp = 2.0;
   double p = progress / 100.0;
   double baseHeight = (1 - p) * size.height;

   Path path = Path();
   path.moveTo(0.0, baseHeight);
   for (double i = 0.0; i < size.width; i++) {
     path.lineTo(
         i,
         baseHeight +
             Math.sin((i / size.width * 2 * Math.pi) +
                     (animation.value * 2 * Math.pi)) * amp - 15);
   }

   path.lineTo(size.width, size.height - 15);
   path.lineTo(0.0, size.height - 15);
   path.close();
   canvas.drawPath(path, wavePaint);
Copy the code

The Tween process for the water wave to increase and then display the text is as follows

// The water wave rises and changes animation
 static TweenSequence waveTweenSequence = TweenSequence([
   TweenSequenceItem(tween: ConstantTween<double> (0.0), weight: 75),
   TweenSequenceItem(tween: Tween(begin: 0.0, end: 1.0), weight: 10),
   TweenSequenceItem(tween: ConstantTween<double> (1.0), weight: 15),]);// Water wave width and height change
 static TweenSequence waveWidthTweenSequence = TweenSequence([
   TweenSequenceItem(tween: ConstantTween<double> (120.0), weight: 80),
   TweenSequenceItem(tween: Tween(begin: 120.0, end: screenWidth), weight: 10),
   TweenSequenceItem(tween: ConstantTween<double>(screenWidth), weight: 10),]);static TweenSequence waveHeightTweenSequence = TweenSequence([
   TweenSequenceItem(tween: ConstantTween<double> (120.0), weight: 80),
   TweenSequenceItem(
       tween: Tween(begin: 120.0, end: screenHeight), weight: 10),
   TweenSequenceItem(tween: ConstantTween<double>(screenHeight), weight: 10),]);// The last displayed text changes
 static TweenSequence textSizeTweenSequence = TweenSequence([
   TweenSequenceItem(tween: ConstantTween<double> (0), weight: 85),
   TweenSequenceItem(tween: Tween(begin: 0.0, end: 30.0), weight: 10),
   TweenSequenceItem(tween: ConstantTween<double> (50), weight: 5),]);Copy the code

According to Tween, the effect is as follows

6. Realize combined animation

Place all the animations from the previous steps on a Stack

 @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (BuildContext context, Widget child) {
        return Center(
          child: Transform(
            alignment: Alignment.center,
            transform: Matrix4.rotationZ(Math.pi * _rotationTween.value),
            child: Stack(
              alignment: Alignment.center,
              children: [
                ClipRRect(
                  borderRadius: BorderRadius.circular(60),
                  child: Container(
                    color: Colors.purple,
                    height: _circleHeightTween.value,
                    width: _circleWidthTween.value,
                  ),
                ),
                Container(
                  height: _triangleSizeTween.value,
                  width: _triangleSizeTween.value,
                  child: CustomPaint(
                    painter: TrianglePainter(
                      left: _triangleLeftTween.value,
                      right: _triangleRightTween.value,
                      top: _triangleTopTween.value,
                    ),
                  ),
                ),
                Container(
                  height: _triangleSizeTween.value,
                  width: _triangleSizeTween.value,
                  child: CustomPaint(
                    painter: SquarePainter(progress: _rect1Tween.value),
                  ),
                ),
                Container(
                  height: _triangleSizeTween.value,
                  width: _triangleSizeTween.value,
                  child: CustomPaint(
                    painter: SquarePainter(
                      progress: _rect2Tween.value,
                      color: Color(0xff40e0b0),
                    ),
                  ),
                ),
                Container(
                  height: _waveHeightTween.value,
                  width: _waveWidthTween.value,
                  child: WaveProgress(
                    size: 120,
                    borderWidth: 0.0,
                    backgroundColor: Colors.transparent,
                    borderColor: Colors.transparent,
                    waveColor: Color(0xff40e0b0),
                    progress: 100 * _waveProgressTween.value,
                    offsetY: _waveOffsetYTween.value,
                  ),
                ),
                Text(
                  'Welcome', style: TextStyle( fontSize: _textSizeTween.value, color: Colors.white, ), ) ], ), ), ); }); }Copy the code

This way, each widget will animate according to the Tween value it depends on, and then load the animation.

Four, the last

In fact, compared with the original iOS development, Flutter is more convenient to implement some effects, such as Layout, such as Hero animation, so I am more optimistic about Flutter. I will also share more knowledge about Flutter later. For example, this animation is not too difficult to analyze every step, but we should have enough patience to analyze and rise to the challenge. Finally all the source code you can see here, welcome star!