Bouncing Ball in Flutter
WHAT WE’LL BUILD:
A Flutter app with a ball that bounces off the edges of the screen. You can tap the screen to make the ball change directions, or long press the screen to reset the ball.
WHAT YOU’LL LEARN:
Some basic Flutter (stateful widgets and setState, adding packages, Container, BoxDecoration, Stack, Positioned, Offset) and some math.
Let’s get started with the default counter app and reduce it to this:
Notice MyHomePage
is now DotScreen
. (You can change a name with Shift+F6 in Android Studio or F2 in VS Code.)
The Scaffold
widget has a Stack
in it since the dot will be stacked on top of the background widget that will detect where we tap.
In order to get the position of where we tap, we’ll import the positioned_tap_detector_2
package by adding the following to pubspec.yaml
andmain.dart
:
Inside the Stack
we need to add our dot on top of the background.
PositionedTapDetector2
is the background. The Container
is the dot, which we’ll create with the following:
We’ll create a variable _circleSize
outside the build method. By using a variable, if we ever want the dot to be a different size, we can change the value of this variable instead of changing all the values manually.
We can make the variable final
since we won’t be changing it, but you would remove the final
if you wanted to add a way to change the dot size. The underscore means that the variable is private to this DotScreen
, and it will be helpful to distinguish it from variables we’ll pass inside of methods that we’ll create later, which won’t have an underscore.
Where is the dot on the screen?
We’ll need to wrap the Container
in a Positioned
widget to specify where it is on the screen. Since the dot will be moving, we’ll need to use a variable. The Offset
widget is used for specifying positions on the screen. The top left corner of the screen is Offset(0,0)
, and the x and y coordinates increase as you go right and down, respectively. You can get individual x- and y-coordinates with .dx
and .dy
that we’ll use in our Positioned
widget.
The ?
is used when defining _circlePosition
because we’re not giving it an initial value. This is because we want to start the dot in the middle of the screen, and we’ll need the BuildContext
that comes with the build
method to do so.
The bang !
operator is used because we didn’t give an initial value to _circlePosition
. Without it, the compiler complains that _circlePosition
might be null and thus it won’t know where to draw the dot. The !
implies that _circlePosition
will definitely not be null since we will give it an initial value:
The ??=
means this operation will only happen if _circlePosition
is null, which should only be when we first launch the app. MediaQuery.of(context).size.width / 2
is half your screen’s width. We subtract _circleSize
because technically, the position of the dot is the top left corner of an imaginary square around the dot. We want the middle of the dot to be in the middle of the screen instead of the top left corner of that imaginary square to be in the middle of the screen.
Quick math review
To make the ball move to where we tap on the screen, we first need to find the slope between where the dot currently is on the screen and where we tapped.
Given two points with coordinates x1, y1
and x2, y2
, the slope between them is given by the formula (y2-y1)/(x2-x1)
. HOWEVER, since the y-coordinate increases as you move down the screen (which is the opposite of a normal graph in math class), we’ll have to change the signs of the y-coordinates.
To find the slope, notice that the onTap
parameter in the PositionedTapDetector2
widget has a position
. We can use position.global
to get the point on the screen where we tapped. If the current _circlePosition
is x1, y1
and position.global
is x2, y2
, then the slope formula, with the signs changed for the y-coordinates, is
which we’ll save to a _slope
variable
Now we can create two methods, moveRight
and moveLeft
, which will be called whenever we tap to the right or left of the ball; the slope being positive or negative will determine whether the ball moves up or down.
Before we see how moveLeft
and moveRight
are defined, we need some more math. To keep the dot from changing speeds, we’ll make it move 1 pixel at a time in any direction we tap. In general, moving x units to the right along a line means you move slope * x units up. If we want the dot to travel 1 pixel in any direction, we can use our friend the Pythagorean theorem (a² + b² = c²) to solve for x.
We’ll need the Dart math package: import 'dart:math';
. We’ll define a variable for x in our class and use the above formula inside the moveRight
and moveLeft
methods.
For moveRIght
, the x-coordinate of _circlePosition
increases by _xDistance
. When moving right, a positive slope means the dot should move up. Since moving up the screen makes the y-coordinate of the Offset widget DECREASE, then we subtract slope * _xDistance
instead of adding it. We’ll do the opposite for the x- and y-coordinates inmoveLeft
:
At this point, the dot moves only once when we tap the screen.
How do we make the dot keep moving?
import 'dart:async';
We’ll add a timer that repeats the method many times a second. Many high-end phones can run at 120 frames per second, which is about 8 milliseconds per frame.
We also need the timer to stop when we tap the screen so that it’ll stop repeating in that direction and start in a new direction. To do this, we’ll define a _tapCount
variable that increases every time we tap the screen.
Both methods now take an integer parameter to stop the current timer and start a new one when the screen is tapped.
At this point, the dot moves around but doesn’t bounce.
How do we make the dot bounce off the edges?
If the ball is moving right, it can bounce off of the top, bottom, or right side. If it bounces off the top or bottom, it will keep moving right but with the slope changing signs. If it bounces off the right, it’ll move left with the slope changing signs.
We can add the following to moveRight
:
Since the official Offset of the ball is the top left corner of an imaginary square around the ball, notice we subtract _circleSize
when checking if the ball has reached the bottom or the right edge of the screen. Otherwise, the ball won’t bounce until the top left the ball has reached the bottom or the right edge of the screen.
We do something similar for moveLeft:
Now we just need to make the ball reset when we long press the screen.
That’s it!
Here is the full main.dart
file: