Building Complex UI in Flutter: Magic 8-Ball
Learn how to build complex user interfaces in Flutter by creating a nearly 3D Magic 8-Ball using standard Flutter components. By Nic Ford.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Building Complex UI in Flutter: Magic 8-Ball
35 mins
- Getting Started
- Creating Novel User Experiences: Beyond Material and Cupertino
- Understanding 2.5D
- Understanding Neumorphic Design
- Building the Magic 8-Ball in Flutter
- Understanding the Difference That Lighting Can Make
- Understanding Coordinate Systems
- Using a Command-and-Control Widget
- Understanding the Glorious Offset Class
- Understanding Matrix4 Transformations and the Three Axes
- Adding a Shadow of Doubt
- Using the Transform Widget
- Adding the Prediction
- Getting Things Moving
- Setting the Rest Position
- Responding to Gestures
- Warping the Window
- Adding Animations
- Moving the Window
- Having Fun with Curves
- Fading the Prediction
- Where to Go From Here?
Understanding the Difference That Lighting Can Make
The human eye is easily fooled, nearly as easily as the human who owns it. It’s a survival technique rather than a failing, enabling reaction to danger before the brain has even noticed. It’s a trait the 2.5D app developer can exploit.
To fool the eye into believing a flat circle is a sphere… add mood lighting. Specifically, a color gradient in the right position suggests not merely a shaded circle, but a sphere on which a light is shining.
However, if it is a sphere, it’s one lit by a flashlight from directly in front. That rarely happens in reality, so it doesn’t look right. You need to move the light source up, by moving the radial gradient’s center. Add a parameter to your RadialGradient
:
center: Alignment(0, -0.75),
This places the center of the gradient — the point from which colors radiate — proportionally 75% up from the center to the top of Container
.
Save your code and perform a hot reload.
Your eye perceives the light source to have moved above the object and what was a black circle now looks very much like a sphere.
Understanding Coordinate Systems
How does this Alignment
work? What do its arguments mean?
You’re probably familiar with the school-taught coordinate system: a horizontal x-axis coupled with a vertical y-axis, meeting at the bottom-left corner — the origin.
However, that’s not the only coordinate system available. Flutter, for example, often places the origin in the top-left corner: As y increases, the position descends. That’s annoying if you’ve planned using the school system — but only a bit, because a simple function can translate these coordinates into what’s needed: (x, y) => (x, height − y)
.
Flutter’s Alignment
widget uses a different, proportional system. The origin is right in the center, and x and y both lie in the range -1 (far left/top) to +1 (far right/bottom), with values between that are proportionally distanced from the center.
Time to start pulling it all together.
Using a Command-and-Control Widget
As you add features, things get more complex — so you’ll need a widget to manage interactions and subsequent UI changes: the “moving parts” of the app. It won’t have any distinct UI itself, instead calculating parameters for — and transforming the shape of — its children, including SphereOfDestiny
. You’ll build that now.
Create a file, components/magic_8_ball.dart. Add a StatefulWidget
called — you’ve guessed it — Magic8Ball
.
class Magic8Ball extends StatefulWidget {
const Magic8Ball({Key? key}) : super(key: key);
@override
_Magic8BallState createState() => _Magic8BallState();
}
class _Magic8BallState extends State<Magic8Ball> {
@override
Widget build(BuildContext context) {
return Container();
}
}
Next, in main.dart, replace SphereOfDestiny(diameter: 200),
with Magic8Ball(),
. Save your code and perform another hot reload.
The sphere’s diameter should fit the screen, rather than using the arbitrary value of 200. Inside the build
method of magic_8_ball.dart, add:
final size = Size.square(MediaQuery.of(context).size.shortestSide);
Because the sphere is fundamentally a circle, you need the largest square that can contain it. The media query returns the maximum space available — a rectangle — so Size.square
using its shortest side gives you what you need.
Now, replace Container()
with:
SphereOfDestiny(
diameter: size.shortestSide
);
Do another hot reload and the sphere is back, bigger and better than ever.
Lastly, the light source that makes the circle a sphere will also light up other features, so move it out from SphereOfDestiny
into Magic8Ball
, to make it ready to use. Add the following line to the top of _Magic8BallState
:
static const lightSource = Offset(0, -0.75);
And pass it in as a parameter to the widget:
SphereOfDestiny(
lightSource: lightSource,
diameter: size.shortestSide
);
Now set up SphereOfDestiny
to use it. Fix the red lines by adding lightSource
as a required final parameter and change Alignment
thus:
center: Alignment(lightSource.dx, lightSource.dy),
But why use an Offset
when SphereOfDestiny
requires Alignment
?
Understanding the Glorious Offset Class
Offset
is a simple service class that gives you a whole lot for free. Essentially, it represents an offset from a point — x and y coordinates, just like Alignment
— but adds loads more too. In addition to coordinates, dx
and dy
, Offset
also calculates the distance
to the point and the angular direction
in radians. Later on, when you apply a bit of trigonometry — don’t run away! — these will be invaluable.
Even better, those nice people at Flutter have written Offset
to behave like a number. Offset
s can be added to or subtracted from other Offset
s or resized by multiplying by a scale factor. Really useful stuff, as you’ll see.
Understanding Matrix4 Transformations and the Three Axes
Since there’s a lightSource
, the sphere should throw a shadow. Introducing the Matrix4 transformation.
Matrix4s transform the shape of a widget once it has been rendered by scaling it, translating its position or rotating the angle at which it’s displayed. Rotation goes a bit further than you might expect: You can rotate in three dimensions.
The familiar x- and y-axes describe a 2D plane, left/right and up/down; but you can also use a third axis, z, which introduces in-out to the equation: 3D.
In fact, the z-axis is probably the one you’re most familiar with. Imagine a book lying on a table. Now turn it 45°. Since x and y describe the 2D table surface, you’ve just rotated it around the only axis left, z.
If it were most people, their arms would also be desperately flailing sideways for balance. That’d be in the y-axis, around… well, everything.
If it were most people, their arms would also be desperately flailing sideways for balance. That’d be in the y-axis, around… well, everything.
You can use Matrix4 to introduce a 2.5D concept of depth by rotating around the x- or y-axes — which is the same as rotating in the z-axis. For example, when you add a shadow.