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
Text
The 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!