preface

References:

  • Understand Bessel curves in depth
  • The third order Bezier curve fits the quarter circle
  • The perception of the appearance of a Flutter

In a previous article, I realized the change of the theme imitating Kuan. Actually, at that time, some people in the group casually said, “This looks so cool, can I use Flutter to realize it?” In my opinion, most things made with Flutter can be realized by heart.

Flutter cool ripple routing animation

After my last post, someone commented on a dribbble design that looked like this:

At first glance, the Flutter is cool, but visually difficult. Since the Flutter has a thermal overload, start by using the most basic components.

Is it possible to animate two simple circles?

That was the idea I had at first, but eventually I gave it up (maybe you can do it).

The reason:

  • (1) Observing the above animation, when there is a straight line in the middle of the animation, it is not difficult to imagine that if a circle can be large enough to form a straight line in the mobile phone screen, then the DP of this circle should reach 100,000 (data in the test), and it can never reach an absolute straight line.

  • (2) Even if it gives people a sense of straight line visually, apart from the cost of performance, from the perspective of animation, the radius of the circle expands from the size of a button (suppose 50DP) to 100,000, and the range of data perceived by people is only 50DP ~ 1000DP. This part of the data only takes up 1% of the entire animation, so the duration of the animation is extremely difficult to control.

The implementation scheme of this article involves the following mathematical knowledge:

  • Cosine theorem
  • Bessel curve
  • Rotation of spatial coordinate points

In fact, the realization of the whole animation will not take too much time, because IT is the first time for me to learn the use of Bezier curve, and review some knowledge of high school, but also took some time.

First, understand Bessel curve

According to the number of control points, Bessel curve can be divided into:

  • First-order Bezier curve (2 control points)
  • Second-order Bezier curve (3 control points)
  • Third-order Bessel curve (4 control points)
  • Bessel curve of order N (N +1 control points)

In the Path class of Flutter, second-order Bessel and third-order Bessel need only 2 and 3 points as parameters. This is an important point to note. Path defaults to the current point as the initial point.

1. First order Bessel

The point of the curve at each moment corresponds to the formula:

2. Second order Bessel

The point of the curve at each moment corresponds to the formula:

3. Third-order Bessel

The point of the curve at each moment corresponds to the formula:

The figure in this part is taken from the first reference article at the top of the article, and the formula is from Baidu Baike. It can better depict its trajectory with the GIF.

Using Bezier curves to draw circles

Since the specific principle of this part is very detailed in the second reference at the top of this paper, we are mainly concerned with the implementation of the whole animation.

1. Draw an approximate 1/4 arc

Calculated parameters

We plot it using a third-order Bezier curve, and we need to compute a parameter h.

Figure is from reference article 2

According to the equation of the circle and the equation of the third-order Bessel curve, h can be solved as 0.552…

Of course we don’t want to overwrite this value to be as accurate as possible. The expression for evaluating it.

Double h = (math.sqrt(2) -1.0) * 4.0/3.0; double h = (math.sqrt(2) -1.0) * 4.0/3.0;Copy the code

Therefore, in the unit circle, the four Bessel control points corresponding to 1/4 arc are:

(0, 1) (1, h) (h, 1) (1, 0))Copy the code

This is one of the directions, h is scaling up as the radius of the circle increases.

Let me draw these four points

The background grid code is from Reference article 3

Draw an arc based on the four points

2, splice into a whole circle

code

final List<Offset> _firstControllerPoints = <Offset>[]; final List<Offset> _secondControllerPoints = <Offset>[]; final List<Offset> _thirdControllerPoints = <Offset>[]; final List<Offset> _fourthControllerPoints = <Offset>[]; Double h = (math.sqrt(2) -1.0) * 4.0/3.0; double h = (math.sqrt(2) -1.0) * 4.0/3.0; void generateControllerPoints(Offset circelCenter, double circleRadius) { h = h * circleRadius; // ------------------------------ _firstControllerPoints.add(Offset( circelCenter.dx - circleRadius, circelCenter.dy, )); _firstControllerPoints.add(Offset( circelCenter.dx - circleRadius, circelCenter.dy - h, )); _firstControllerPoints.add(Offset( circelCenter.dx - h, circelCenter.dy - circleRadius, )); _firstControllerPoints.add(Offset( circelCenter.dx, circelCenter.dy - circleRadius, )); // ------------------------------ _secondControllerPoints.add(Offset( circelCenter.dx, circelCenter.dy - circleRadius, )); _secondControllerPoints.add(Offset( circelCenter.dx + h, circelCenter.dy - circleRadius, )); _secondControllerPoints.add(Offset( circelCenter.dx + circleRadius, circelCenter.dy - h, )); _secondControllerPoints.add(Offset( circelCenter.dx + circleRadius, circelCenter.dy, )); // ------------------------------ _thirdControllerPoints.add(Offset( circelCenter.dx + circleRadius, circelCenter.dy, )); _thirdControllerPoints.add(Offset( circelCenter.dx + circleRadius, circelCenter.dy + h, )); _thirdControllerPoints.add(Offset( circelCenter.dx + h, circelCenter.dy + circleRadius, )); _thirdControllerPoints.add(Offset( circelCenter.dx, circelCenter.dy + circleRadius, )); // ------------------------------ _fourthControllerPoints.add(Offset( circelCenter.dx, circelCenter.dy + circleRadius, )); _fourthControllerPoints.add(Offset( circelCenter.dx - h, circelCenter.dy + circleRadius, )); _fourthControllerPoints.add(Offset( circelCenter.dx - circleRadius, circelCenter.dy + h, )); _fourthControllerPoints.add(Offset( circelCenter.dx - circleRadius, circelCenter.dy, )); } Path getCirclePath() { final Path path = Path(); path.moveTo( _firstControllerPoints[0].dx, _firstControllerPoints[0].dy, ); path.cubicTo( _firstControllerPoints[1].dx, _firstControllerPoints[1].dy, _firstControllerPoints[2].dx, _firstControllerPoints[2].dy, _firstControllerPoints[3].dx, _firstControllerPoints[3].dy, ); path.cubicTo( _secondControllerPoints[1].dx, _secondControllerPoints[1].dy, _secondControllerPoints[2].dx, _secondControllerPoints[2].dy, _secondControllerPoints[3].dx, _secondControllerPoints[3].dy, ); path.cubicTo( _thirdControllerPoints[1].dx, _thirdControllerPoints[1].dy, _thirdControllerPoints[2].dx, _thirdControllerPoints[2].dy, _thirdControllerPoints[3].dx, _thirdControllerPoints[3].dy, ); path.cubicTo( _fourthControllerPoints[1].dx, _fourthControllerPoints[1].dy, _fourthControllerPoints[2].dx, _fourthControllerPoints[2].dy, _fourthControllerPoints[3].dx, _fourthControllerPoints[3].dy, ); return path; }Copy the code

Actually 12 control points are enough, because the initial point is the position of the current point.

Three, the realization of animation

1. Segmentation of animation

I divided the entire animation into four parts

Right circle animation:

  • 1. From a small circle to a larger one, the maximum height can fill the screen.
  • 2. Gradually straighten 1/4 of the arc to simulate the effect of continuous expansion of the circle.

Left circle animation:

  • 1. Gradually transition the line to a 1/4 arc.
  • 2. From a large circle to a smaller circle.

2. Implement the first part of animation

The first part does nothing more than slowly increasing the radius of a given circle and then changing the position of the center in time.

As follows:

At this point we need to think about the question, when should we end the first animation?

After repeated attempts at animation, I chose the end of the circle just as it expanded to the end point of the screen, but there was a new problem:

How do I figure out what radius R is right at the end of the screen?

I am very confident and strong in mathematics, of course…





The last

High school students strongly want to let everyone know that it contributes to the law of cosines, I will not behead the image.

Known conditions:

  • The distance from the button center to the screen endpoint
  • The radian value of the button’s center coordinates to the vector corresponding to the screen endpoint

And then finally, the law of cosines

double getRadius( Offset point1, Offset point2, ) { point1 = Offset(point1.dx, -point1.dy); point2 = Offset(point2.dx, -point2.dy); double r; final Offset line = point2 - point1; final double k = line.dy / line.dx; final double angle = math.atan(k); Print (' ====>$line'); final double cosx = math.cos(angle); print('cosx====>$cosx'); final double sinx = math.sin(angle); print('sinx====>$sinx'); final double cos2x = math.pow(cosx, 2).toDouble() - math.pow(sinx, 2); final double l = line.distance; r = math.sqrt(math.pow(l, 2) / (2 * (1 + cos2x))); return r; }Copy the code

Point1, point2 is the center of the button and the top of the screen coordinates.

Part I Final effect of animation:

3. Implement the second part of the animation

This part of the animation takes the longest time. In order to facilitate the animation design, I first reduced the circle radius at the end of the first part of the animation.

We simulate the change of the arc by rotating the last two control points of the fourth order Bessel.

Here is:






We only need to rotate the last two or the first two of a set of control points, so the math involved is that we need to figure out exactly where one point is rotated relative to another by a given Angle.

Code implementation:

double getNegative(num number) { if (number.isNegative) { return -1; } return 1; } double getVectorInitAngle(Offset vector) { if (vector.dx == 0) { if (vector.dy > 0) { return math.pi / 2; } else { return math.pi * 3 / 2; } } if (vector.dy == 0) { if (vector.dx > 0) { return 0; } else { return math.pi; } } if (vector.dx > 0 && vector.dy > 0) { return math.atan2(vector.dx, vector.dy); // print(math.atan2(vector.dx, vector.dy) * 180 / math.pi); } else if (vector.dx < 0 && vector.dy > 0) { return math.atan2(vector.dx, vector.dy) + math.pi; // print(180 + math.atan2(vector.dx, vector.dy) * 180 / math.pi); } else if (vector.dx < 0 && vector.dy < 0) { return math.pi / 2 - math.atan2(vector.dx, vector.dy); // print(90 - math.atan2(vector.dx, vector.dy) * 180 / math.pi); } else if (vector.dx > 0 && vector.dy < 0) { return math.atan2(vector.dx, vector.dy) + math.pi; // print(180 + math.atan2(vector.dx, vector.dy) * 180 / math.pi); } return 0; } Offset rotateOffset(Offset point, double angle, [Offset origin]) { Offset tmp; origin ?? = const Offset(0, 0); final Offset vector = point - origin; If (Angle == 0.0) {return point; } print (' vector = = = = $vector initial Angle = = = = = = > ${getVectorInitAngle (vector)} "); print('angle====${angle * 180 / math.pi}'); tmp = Offset( origin.dx + vector.distance * math.cos(angle + getVectorInitAngle(vector)), origin.dy + vector.distance * math.sin(angle + getVectorInitAngle(vector)), ); return tmp; }Copy the code

The second part is the final effect of animation

Add up the previous part of the animation

So let’s set up all the points that we need to rotate, and let’s see.

There’s a new problem. It’s not what we want.

We continue to reduce the radius for easy observation and print out all the auxiliary points.

Find out the reasons as follows:

  • The initial position of the control point of the second arc is the end point of the first arc, and so on.
  • In the second animation, part of the point rotates with a fixed point, causing an unexpected change in Bessel who uses the point.

This is why it was mentioned above that only 12 Bezier control points are needed for a circle, since four points are shared.

The solution

A set of second-order Bessels is added at points where the path does not meet expectations due to shared control points, which are used to connect the endpoint of the upper group to the starting point of the next group.

Let’s fill in the color and see:

Using the actual calculated radius:

That’s about it for the whole animation.

4. Realize the third and fourth parts of animation

These two parts of the animation will not go into detail, is to create a new circle, and the previous circle animation data are all the opposite, the center of the circle coordinates are different.

The preliminary implementation

Add some Boolean values, and when the third part of the animation starts, create an entire rectangle on the left side, then subtract the paths and cancel the red helper button.

Look again at the effect:

Looking at the original animation, there is a displacement animation for the components before and on the routing page, as well as a displacement animation when the left circle is about to reach its minimum.

We’ll do all the processing.

The final result

On Android:

Ffmpeg GIF slows down during conversion for some reason. In addition, the page displacement animation before routing and the page displacement animation after routing need to add 😑.

As well as the details mentioned by the group, I failed to optimize, met the pit, the last part of the current animation is implemented in a less elegant way 😳.

Four, component packaging

Make this route easier to use.

My priority was to wrap it as a PageRouteBuilder, and I ended up wrapping it as a component because there was too much animation involved.

You just need to use this button anywhere:

CoolButton(
    curPageAccentColor: Color(0xff013bca),
    buttonColor: Color(0xfffcb7d6),
    nextButtonColor: Colors.white,
    pushPage: PushPage(),
),
Copy the code

Dart has the entire animation implementation in lib, including performance optimization and detail handling, and needs to change the code.

The demo address:

MYS_Flutter

Five, the conclusion

  • The code is written in a hurry, many places are not standard, I will improve after introducing this route into my own project.
  • The last post “cool ripple Route animation” was published in February this year, comparing the format and layout of the writing article with the present one, I can also see my progress in this aspect in the past five months.
  • “MYS_Flutter” was first created two years ago, and the last update of the other demo inside it was also two years ago 😶.
  • I don’t have a variety of projects on hand, so I don’t need to set up various wheels all day long to learn Flutter. For me, I always like to try new things, which is why I tried Flutter.

happy coding !