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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 5 of this article. Click here to view the first page.

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.

Suddenly the circle is perceived as a sphere

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.

Proportional coordinate system with origin at 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.

A larger sphere is back

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. Offsets can be added to or subtracted from other Offsets 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.

Note: Matrix4 transforms are so called because they use a 4×4 number matrix to map one space onto another. There’s a great discussion of these techniques here: 2D computer graphics.

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.

x, y and z axes

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.

A gymnast rotating around the x-axis

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.

A gymnast simulating the author on the bars

Note: It’s important to understand the difference between rotating around and rotating in. Think of a gymnast on the parallel bars: They’ll rotate around the bars, so around the x-axis; but their feet will come toward you, in the z-axis.

A gymnast rotating around the x-axis

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.

A gymnast simulating the author on the bars

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.