In this post I will show you how to create a simple animated background from scratch using the Animated Backgrounds package available at https://pub.dartlang.org/packages/animated_background/, created by me.
By the end of this post we will have an app with a background looking like this:
Note that this background is interactive and you can pop the bubbles before they reach their maximum size. Also note that this post may be too detailed for new users. If you just want to check the code, go to my GitHub at https://github.com/AndreBaltazar8/animated_background_example.
1. Getting Started
If you are new to Flutter go to the Flutter website to download it. This should take you 5 to 10 minutes and you will be ready to develop applications in Flutter. I also suggest you to take some time to learn some basic concepts on their website.
Building an animated background with this package is simple, all you need to do is implement a few methods that update and render the objects the represent that animated components of your background. Follow the steps below to get started.
First of all, we need to have a project. If you are viewing this tutorial, you probably have a project you want to integrate this in, but I will create a new project to keep things simple.
To create a new project just run flutter create animated_background_example
on your terminal. This should give you a fresh Flutter project.
Open the project on your editor and open up the pubspec.yaml
file and add the package to your dependencies like this:
dependencies:
animated_background: ^0.0.4
(Note: the current version might be different, please check the package page and get the latest version.)
After that run the flutter packages get
command to get the package.
Your project is now ready to use animated backgrounds.
2. Creating an animated background with a default behaviour
Let's start by creating a background with a default behaviour. This is a behaviour that is already implemented in the package and available to everyone.
Open the lib/main.dart
file that contains the root of your application. In this file you will see the Home Page widget that creates home page for your application. We will be extending this file to add our animated background.
The package provides a widget appropriately named AnimatedBackground
that is used to add the background. This is the only widget necessary to render the animated background.
To be able to use the widget we need to import it. This can be done with the code below (put it on the top of the file).
import 'package:animated_background/animated_background.dart';
In the build
method of the _MyHomePageState
class, we have the creation of the main widgets of the app. This is here we will insert an AnimatedBackground
widget.
Wrap the Center
widget with an AnimatedBackground
widget and provide the behaviour
and vsync
parameters with the values RandomParticleBehaviour()
and this
, respectively.
Your build method should now look like this:
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: AnimatedBackground(
behaviour: RandomParticleBehaviour(),
vsync: this,
child: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'You have pushed the button this many times:',
),
new Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: new Icon(Icons.add),
),
);
}
This will have an error on the this
value provided to the vsync
parameter. Before we fix this, let me explain what this parameter does. In order to animate the background we need to receive a tick from the application allowing us to update the background. This is done via this parameter where we specify a TickerProvider
that is used to create a ticker which will update our background.
One simple way of getting a ticker provider is by making the state use the mixin TickerProviderStateMixin
. To do this edit the _MyHomePageState
declaration to use the mixin. It should look like the following:
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
// Class implementation
}
You can now run the application! It should look like this:
You can experiment with all particle options available through the options
parameter of the behaviour to get different effects and even use images as the shape particles. An example app with configurable particle behaviour can be found inside the package, or on GitHub at: https://github.com/AndreBaltazar8/flutter_animated_background/tree/master/example
3. Creating a new behaviour (our new background)
From this point forwards things get serious. It requires some understanding of rendering in Flutter, but I will try to make it as simple as I can.
As I showed in the start, we are trying to get a bubble like effect for our background. This is similar to the particles but we will be implementing it without using the particles behaviour as a base, because particles have movement, and we do not need that.
Lets start by creating a class to hold the information about each bubble. For each bubble we will need the following information: its position, its current and final sizes and finally the color. The class will look like the following:
class Bubble {
Offset position;
double radius;
double targetRadius;
Color color;
}
Now lets create the behaviour for these bubbles, lets call it BubbleBehaviour
and implement the Behaviour
class.
class BubbleBehaviour extends Behaviour {
@override
void init() {
// TODO: implement init
}
@override
void initFrom(Behaviour oldBehaviour) {
// TODO: implement initFrom
}
// TODO: implement isInitialized
@override
bool get isInitialized => null;
@override
void paint(PaintingContext context, Offset offset) {
// TODO: implement paint
}
@override
bool tick(double delta, Duration elapsed) {
// TODO: implement tick
}
}
Lets start by implementing the initialization of the bubbles. First we need a container to put the bubbles in. We will use a list for that. Inside the class declare a field of type List<Bubble>
that will hold our bubbles and name it _bubbles
, making it private and only editable by our class. One other thing that we need is how many bubbles will we display on screen. For this implementation we can use a constant number, so define with the number of bubbles you would like. Make sure this number is not too large, because it will cause performance problems if you create way too many bubbles.
We are now ready to create our bubbles. For this, we use the init
method, which is called when the behaviour should be initialized. In this method we can generate the bubbles and assign them to the list. We use the following piece of code to achieve that:
_bubbles = List<Bubble>.generate(numBubbles, (_) {
Bubble bubble = Bubble();
_initBubble(bubble);
return bubble;
});
We use the generator
constructor of a List
that allows us to generate a specified amount of values of a class. In this case we are generating numBubbles
amount of Bubble
instances, and calling _initBubble
with them as a parameter when they are created. This method will initialize each bubble according to our behaviour.
Now that we initialized our bubbles we can implement the isInitialized
getter, that is used to check if the behaviour is initialized or not. We just need to make it return true
if the behaviour is initialized. We can simply return _bubbles != null
as the value.
@override
bool get isInitialized => _bubbles != null;
Now we should also implement the initFrom
method which is used to initialize the behaviour from an old behaviour that was being used by the AnimatedBackground. This method will be called when the application updates state for example. In our case, we want to preserve the bubbles so we need to copy them from the old behaviour. To do this we can use this simple code:
@override
void initFrom(Behaviour oldBehaviour) {
if (oldBehaviour is BubbleBehaviour) {
_bubbles = oldBehaviour._bubbles;
}
}
For the final part of the initialization, we need to implement the _initBubble
method that we used before. This will take care of initializing the bubble.
Since we want to initialize the bubble in a random position on the screen, we need to have a way to generate these random numbers. For this we can use the Random
class provided in dart:math
.
Import it using import 'dart:math';
and create an instance of the Random
class. This instance can be a static instance on the BubbleBehaviour
class.
static Random random = Random();
Lets start implementing our _initBubble
method. We start by generating a random position for both coordinates of the bubble. To do this we generate a random number with random.nextDouble()
multiply it by the screen size on that axis and assign that to the coordinate. To get the screen size, we can use the size
getter that is available for all Behaviour
classes. The initialization of the position will look like the following:
void _initBubble(Bubble bubble) {
bubble.position = Offset(
random.nextDouble() * size.width,
random.nextDouble() * size.height,
);
// TODO: missing other initializations
}
Next we need to initialize our radius and target radius. For the radius
we can specify the value 0.0
, because we want to make the bubble grow in size to the target radius. In the case of the target radius, we need to get a random number for it. We again make use of the random.nextDouble
method and with two new constants maxTargetRadius
and minTargetRadius
we can initialize the target radius. Since the color is all we have left to initialize, we can also initialize it to some color, for example, Colors.red.shade300
from the material design color swatches. You can take care of randomizing a color after, to make it cooler, but for now we will use only one color.
The final code for _initBubble
will look like the following:
void _initBubble(Bubble bubble) {
bubble.position = Offset(
random.nextDouble() * size.width,
random.nextDouble() * size.height,
);
bubble.radius = 0.0;
bubble.targetRadius = random.nextDouble() * (maxTargetRadius - minTargetRadius) + minTargetRadius;
bubble.color = Colors.red.shade300;
}
By now you are probably tired and just want to see the bubbles on the screen of your device... but hang on, we are almost there. We just need to update the bubbles to make them grow and then paint them on the screen.
Since we are working on the logic side of things, lets make the bubbles grow. For this we need to implement the tick
method. This method is called every tick of the ticker provider passed to the background. Before we create a loop to iterate over the bubbles to update them, we need to check if the behaviour was initialized. The reason for this check is to make sure we do not iterate over the null
container, since the tick
is called even if the behaviour is not initialized yet. (This will probably change in the future, so do not rely on this to initialize or do anything when the behaviour is ticking but not initialized...)
Now we are ready to create the loop that iterates over our bubbles list. For each bubble we will need to increase its radius towards the target radius. This can be done using the elapsed
argument passed on the tick
method. This argument has the elapsed time (in seconds) since the last tick. This value is usually much less than 1 second. For example, if your app is running at normal speed (60 frames per second) it should be around 0.016667 seconds (1s / 60 frames = 0.016667 seconds per frame). We multiply this value, by a new constant that will be our growth rate, so call it growthRate
, and add the resulting value to the radius
.
After growing the radius, we check if it is greater or equals to the target radius and in the case this is true, we call _initBubble
with the bubble. Remember this function is the one we created to initialize the bubble. This will reinitialize the bubble in a new position with a radius of 0.0
and a new target radius, making it disappear. A custom effect for the bubble popping could be created but we will keep this simple. In the end of the function, return true
to specify that you want the background to repaint. The code for this function is like the following:
@override
bool tick(double delta, Duration elapsed) {
if (!isInitialized)
return false;
for (var bubble in _bubbles) {
bubble.radius += growthRate * delta;
if (bubble.radius >= bubble.targetRadius)
_initBubble(bubble);
}
return true;
}
Now comes the part you were waiting for, drawing of the bubble, which is done in the paint
method. To get started drawing our bubbles we need to have access to the canvas, this is done through the context
argument. Simply define a canvas
variable from the context.canvas
. After this, we need to create an instance of Paint
, which is the object that defines a style to use when drawing on a canvas. In this paint object we want to change to fields that will be similar in all bubbles we draw. The first one is strokeWidth
which defines how wide the edges are drawn for our bubbles, and the second field is style
which we set to PaintingStyle.stroke
which specifies that we only want to paint the edges.
We now have the canvas that we will draw the particle in and the paint that will be used to style our bubbles so its time to draw them. Create a loop to iterate the list of bubbles, and with the color
field of the bubble update the color
field of the paint instance. This will set the color for our next bubble that will be drawn. After that, use the drawCircle
method of the canvas to draw the bubble. Pass in the position as the first parameter, the radius as the second, and finally the paint object to be used to draw. And that's it! Finally we are done! If you run the application the bubbles should render on the screen. The code for this method should look as the following:
@override
void paint(PaintingContext context, Offset offset) {
var canvas = context.canvas;
Paint paint = Paint()
..strokeWidth = 3.0
..style = PaintingStyle.stroke;
for (var bubble in _bubbles) {
paint.color = bubble.color;
canvas.drawCircle(bubble.position, bubble.radius, paint);
}
}
The result is what I showed at the start, but without interaction:
This is cool, but we can take it a little further and make this interactive, where we can "pop" the bubbles before they reach the target radius. Follow me to the next section where I will show you how to do that.
4. Adding interactivity
As you are probably familiar, in order to make a Flutter app, we use widgets for almost everything. Gesture detection is no exception. To make our behaviour detect gestures we make use of the GestureDetector
widget. The onTapDown
callback contains the necessary details to know which bubbles we need to pop. Next we add the widget to the tree. This is done by overriding the builder
method of the behaviour, and specify the widget normally, except for the child
, which must be a call to the builder
of the super class. It is also important to define the behaviour of the gesture detector to either HitTestBehavior.translucent
or HitTestBehavior.opaque
to be able to get the gestures. The only difference between them is that one allows taps to go through the detector to elements behind and the other does not.
We still need to implement the logic for our tap down method. As I said previously, we receive the tap details in this callback, and we will use it to pop the bubbles. Set the callback of the tap as (details) => _onTapDown(context, details.globalPosition)
this will forward our call to other method with the necessary arguments. In the implementation of the method, we start by finding the render box in the context, using RenderBox renderBox = context.findRenderObject();
. This render box allows us to convert the global position of the gesture to a local position using renderBox.globalToLocal(globalPosition);
which then can be used to know which bubbles should be affected by our tap. That's what we implement next.
Create a loop for the bubbles, and subtract the position of the bubble from the local position of the tap that we just calculated. The resulting value should be of the type Offset
. With this you can get the square of the distance between the two positions with the distanceSquared
getter. Getting the square is faster than the actual distance between them because we don't need to perform a square root operation. You are probably thinking this is irrelevant but calculating the square root for many numbers, in our case, one for each bubble, can be quite expensive on the CPU.
Finally we can compare this distance with the radius of the bubble, but since the distance is squared we need to have bubble.radius * bubble.radius
and you can also multiply this value by a small constant (eg. 1.2
) to give the bubble a larger radius that we can press to pop it. If squared distance is smaller than the squared radius, we call _initBubble
to re-initialize the bubble. It should now be interactive. The full code for this section should be similar to:
@override
Widget builder(BuildContext context, BoxConstraints constraints, Widget child) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTapDown: (details) => _onTap(context, details.globalPosition),
child: super.builder(context, constraints, child),
);
}
void _onTap(BuildContext context, Offset globalPosition) {
RenderBox renderBox = context.findRenderObject();
var localPosition = renderBox.globalToLocal(globalPosition);
for (var bubble in _bubbles) {
if ((bubble.position - localPosition).distanceSquared < bubble.radius * bubble.radius * 1.2) {
_initBubble(bubble);
}
}
}
This concludes the creation of the interactivity and this example.
5. Conclusion
Finally you have a simple background working with interactivity. It can be easily extended to make a popping animation, implement the randomization of the color (which we setup the variable, but did not create code to change it) and some other tweaks. I leave it to you as an exercise.
I understand that this post is extensive, probably contains some errors and not everyone will read it until the end, but if you did, thank you! If you have any question regarding this or suggestions on how to improve it, please tweet me @AndreBaltazar.
The full code is available on my GitHub at:
https://github.com/AndreBaltazar8/animated_background_example
Hope you enjoyed! Have a nice day.