preface

Was the last simple explosion effect, go on to find a code to write a Widget + particles fall curve of the tutorial, click on the explosion, but because of my math more slag completely look not to understand the author’s a heap of constant + combination is match which formula (which is also the author didn’t have any comments), so or wait until the weekends to have a look.

This time, let’s implement a bubbling background bar, rendering 👇:

  • The frame count looks a little low due to the Gif limit, but the actual frame count can be stable between 59 and 60

Original tutorial link: Portal. Yeah, same author as the Simple Explosion. This article fixes an additional problem of the author, that is, background switching optimization. Complete code: gitee.com/wenjie2018/… Flutter SDK: 2.2.2 Dependencies: simple_animations: ^3.0.1


Let’s start at a point

I think any complex animation is a combination of different unit animations, so I will go from one particle animation to the whole animation step by step.

Before the first particle is born, let’s draw up some rules, as shown below 👇 :

  • The offset is calculated from the top left corner of the screen, so x increments to the right and y increments down. You can adjust this to your liking.
  • The point of virtual boundaries is that particles can start off off the screen, enter the screen, and end off the screen, so there are no weird particles born/disappearing out of thin air.

Single point fixed animation

Ok, next let’s implement a simple animation of particles from the bottom -> top of the virtual boundary. The idea is like this:

  • Build the loop animation intervalLoopAnimationImplement, continuously perform the push particle action (if the particle List is not empty)
  • In order for particles to overlap, particle objects must be usedStackthe
  • The X-axis is in the middle for now, and the Tween value is 0.5 -> 0.5
  • The Y-axis goes from the bottom (1.2) to the top (-0.2) of the virtual boundary, so the Tween value is 1.2 -> -0.2
  • To get the x and y offset according to Tween, useCustomPainterTo draw particles
  • When the particle reaches the top of the virtual boundary, reset the animation progress of the particle to 0 (so that it reappears from below)
  • The loop to push particles is built, so all that remains is to add a particle element to the List when initializing. Since the number of particles is not dynamic, we can choose to pass it in the constructor

Next, the implementation code 👇 :


import 'package:flutter/material.dart';
import 'package:simple_animations/simple_animations.dart';
import 'package:supercharged/supercharged.dart';
import 'package:supercharged_dart/supercharged_dart.dart';

void main(a) {
  runApp(new MaterialApp(home: Page()));
}

class Page extends StatefulWidget {
  @override
  State<StatefulWidget> createState(a) => PageState();
}

class PageState extends State<Page> {

  @override
  void initState(a) {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Container(
          color: Colors.black87,
          child: Stack(
              children: <Widget>[
                // Bubble dynamic effect
                Positioned.fill(child: ParticlesWidget(1))])); }}/// ///////////////////////////////////////////////////////////////////////////
///
/// The bubble action Widget
///
/// ///////////////////////////////////////////////////////////////////////////
class ParticlesWidget extends StatefulWidget {
  // The number of particles
  final int numberOfParticles;

  ParticlesWidget(this.numberOfParticles);

  @override
  _ParticlesWidgetState createState(a) => _ParticlesWidgetState();
}

class _ParticlesWidgetState extends State<ParticlesWidget> {

  final List<ParticleModel> particles = [];

  @override
  void initState(a) {
    widget.numberOfParticles.times(() => particles.add(ParticleModel()));
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return LoopAnimation(
      tween: ConstantTween(1),
      builder: (context, child, dynamic _) {
        // If the particle animation is complete, start again
        _simulateParticles();
        returnCustomPaint( painter: ParticlePainter(particles), ); }); }/// If the particle animation ends, call [particleModel.restart]_simulateParticles() { particles .forEach((particle) => particle.checkIfParticleNeedsToBeRestarted()); }}/// ///////////////////////////////////////////////////////////////////////////
///
/// Draw logic
/ / / don't understand the reference: < https://book.flutterchina.club/chapter10/custom_paint.html#custompainter >
///
/// ///////////////////////////////////////////////////////////////////////////
class ParticlePainter extends CustomPainter {
  List<ParticleModel> particles;

  ParticlePainter(this.particles);

  @override
  void paint(Canvas canvas, Size size) {
    finalpaint = Paint().. color = Colors.white.withAlpha(50);

    particles.forEach((particle) {
      final progress = particle.progress();
      final MultiTweenValues<ParticleOffsetProps> animation =
      particle.tween.transform(progress);
      final position = Offset(
        animation.get<double>(ParticleOffsetProps.x) * size.width,
        animation.get<double>(ParticleOffsetProps.y) * size.height,
      );
      canvas.drawCircle(position, size.width * 0.2 * particle.size, paint);
    });
  }

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

/// ///////////////////////////////////////////////////////////////////////////
///
/// Particle object, containing various properties
///
/// ///////////////////////////////////////////////////////////////////////////
enum ParticleOffsetProps { x, y }

class ParticleModel {
  /// Particle coordinate tween
  late MultiTween<ParticleOffsetProps> tween;
  /// Particle size
  late double size;
  /// Animation transition time
  late Duration duration;
  /// Start time of animation
  late Duration startTime;

  ParticleModel() {
    restart();
  }

  /// Reset the particle properties
  restart() {

    // For the Y-axis: 0 for the top of the screen, 1 for the bottom of the screen, -0.2 for the 20% outside the top, and 1.2 for the bottom 20%
    // Start (x, y)
    final startPosition = Offset(0.5.1.2);
    // End coordinates (X, y)
    final endPosition = Offset(0.5, -0.2); tween = MultiTween<ParticleOffsetProps>() .. add(ParticleOffsetProps.x, startPosition.dx.tweenTo(endPosition.dx)) .. add(ParticleOffsetProps.y, startPosition.dy.tweenTo(endPosition.dy));// Animation transition time
    duration = 3.seconds;
    // Start time
    startTime = DateTime.now().duration();
    // Particle size
    size = 0.6;
  }

  checkIfParticleNeedsToBeRestarted() {
    if (progress() == 1.0) { restart(); }}/// Get the animation progress
  /// 0 indicates the start and 1 indicates the end, which is the percentage of the animation progress
  double progress(a) {
    return ((DateTime.now().duration() - startTime) / duration)
        .clamp(0.0.1.0) .toDouble(); }}Copy the code

The effect is as follows 👇 :


Random particle size

This is as simple as adding random to the particle size in the restart method. To prevent the particle size from being zero, you can set a fixed value + random value

restart() { ... (abbreviated)// Particle size
    size = 0.2 + Random().nextDouble() * 0.4;
  }
Copy the code

The effect is as follows: particle size changes 👇 :


Random start/end point

In order to make the particle move not just straight, we need to randomize the x coordinates of the starting point and the x coordinates of the end point, and the y coordinates remain the same as the original 1.2->-0.2:

The restart function is still changed, and the corresponding code changes are as follows 👇 :

  restart() {

    // For the Y-axis: 0 for the top of the screen, 1 for the bottom of the screen, -0.2 for the 20% outside the top, and 1.2 for the bottom 20%
    // Start (x, y)
    final startPosition = Offset(-0.2 + 1.4 * Random().nextDouble(), 1.2);
    // End coordinates (X, y)
    final endPosition = Offset(-0.2 + 1.4 * Random().nextDouble(), -0.2); . (a)}Copy the code

The effect after transformation is as follows 👇 :


Increasing the particle

The next thing is relatively simple, first we change the original 1 particle to 50, the corresponding code is as follows:

. C-bound. Fill (child: partcleswidget)50))
Copy the code

It now looks like this 👇 :

The above effect looks a bit magical, because we have the same transition time for each particle. To make the particle distribution more uniform, we have added a random value to the transition time of the animation, as shown in 👇 :

  restart() {
    // Animation transition time
    duration = 3.seconds + Random().nextInt(30000).milliseconds; . (Other code omitted)}Copy the code

After the modification, the effect will be as follows after the app starts for 10 seconds + 👇 :

  • It’s got the inside flavour, but don’t worry, there’s still room for improvement

Start the optimization

For those of you who are careful, you may have noticed that I’ve made it bold10 seconds + after the app startsThis is because when the animation is first opened it looks like this 👇 :

Or if you cut the app to the background, wait about 30 seconds, and cut back, you’ll find that the animation has been reset again, like 👇 :

The reason for this is that we have all the particles start at the same time, so how can we make some of the particles’ animations “sneak away” when the app starts? This method returns a value between 0 and 1, which is the percentage of the progress of the current animation. This method then gets the Tween value based on the progress. In other words, we can change the starting position of the animation simply by changing the progress. For example, when a particle’s progress() returns 0.5, it must be in the middle of the y axis of the screen.

The parameters that affect progress() are startTime and duration. Obviously duration has already passed randomly in order to keep the particles out of alignment, so the next step is to change startTime.

The goal is for the particles to be evenly distributed when the application starts, which means the particles have to be “pre-played.” The initial return value of progress() must be >0. If the value of progress() is =0.5, then we can see a particle in the middle of the Y-axis when we open the app.

The next code change is to add a new function:

  /// "disrupt" the start time of the animation to "disrupt" the progress of the animation so that the particles are evenly distributed on the screen when the page is initialized
  void shuffle(a) {
    startTime -= (Random().nextDouble() * duration.inMilliseconds)
        .round()
        .milliseconds;
  }
Copy the code

The time to call is after the particle constructs and executes restart(), because our startTime initial value is set in restart() :

  ParticleModel() {
    restart();
    shuffle();
  }
Copy the code

After the revamp, we will find that the app starts with no problem 👇🏻 :

But don’t get too excited, let’s try the background switch, we will find that the problem is still 👇 :


Background switching optimization

The reason for this problem is that while the app is dormant in the background, real time is still flowing, but since the app is in the background, the interface is no longer rendered and the animation logic stops completely. It doesn’t matter if you don’t understand this. The obvious indication is that when your application enters the background, the frame bar of Flutter Performance will stop, but the memory map will still be moving. This is not a BUG, but the application animation will stop, as shown below 👇 :

Let’s review the progress() implementation again 👇 :

Obviously, when we wait 30 seconds to re-enter the application, the “pre-cast” time is long gone, because both startTime and duration are set at construction time, and duration is only 33 seconds at maximum. Eventually the progress of the particles () returns 1, for this reason, the background is cut back after all particles through checkIfParticleNeedsToBeRestarted trigger restart () () without triggering shuffle (), Finally, it comes back to the question of all particles having the same starting point.

To solve this problem is also very simple, as long as listen to the front and back after the switch again call restart, shuffle, the following directly put the code screenshot 👇 :

Try again to wait 30 seconds and then switch back to the application 👇 :

  • OK, no more questions

Finally as long as the gradient background is added to the complete, gradient background code is relatively simple, here will not say, want to see the final effect of the direct run the code link given at the beginning of the article.

That’s it 🎉