Spinning the wheel in Flutter
In this article, I will show how to use animations in Flutter by building a very simple yet powerful widget that can be used to create really fun interactions with the user.
Spin the wheel!
A few years ago I had to implement a spinning wheel for a web application and, well… I had a hard time with it, so I thought that I would try to do the same in Flutter. A couple of days later I was ready to publish it as a library https://pub.dartlang.org/packages/flutter_spinning_wheel. You can see below an example of what you can do with it.
Men at work
The requirements that I set myself for this development were the following ones:
- I could use any png image as a wheel instead of dynamically creating one with a canvas. That way I could easily change the look of it, but I will still need to know what option is being selected as the wheel spins.
- The wheel will be interactive; that is, the user can at least start the wheel by taping or dragging.
- The speed at which the wheel spins will be determined by how fast the user was dragging when the finger stopped contacting the screen.
- Once spinning, the wheel will decelerate at a constant rate.
- There will be at least a callback function to inform the user when the wheel stops about the selected (winner) option.
It seems like a lot of work, but it takes less than 200 lines of code to implement it, which I think it’s very impressive.
We can separate the work here in two different areas:
- Animation. We want to take an image and rotate it during a certain amount of time.
- Interaction. Once we know how to animate the wheel we will see how to trigger the animation; that is, using gestures.
In the final implementation there’s a bit more stuff going on with configuration parameters and fine-tuning, so feel free to check it out in the Github repo. Here though, I’ll try to keep things simple and go through each step in order.
The first thing we need to do is animate the wheel. Let’s look at the complete code and then break it down.
First thing, drawing the wheel, so we need at least the image and the dimensions, and as we want this to be configurable we will have them passed in through the constructor.
What is an animation, really?
An animation is basically a change of state over time for a particular element (a
Widget in this case), so we obviously will need a
StatefulWidget to keep a state, but we also need something that triggers a change of that state, and in Flutter that's a
Ticker will fire many times per second (up to 120 if supported) allowing us to update our state and thus rebuild our widget. By "extending" the mixin
SingleTickerProviderStateMixin we are basically using our own class as a
TickerProvider. We will then update our state with
AnimationController will configure our widget as a
TickerProvider with the
vsync parameter and also the duration of the animation, which in this case I set to 5 seconds, but later it will depend on the speed of the spin.
As I said before, an animation is basically a change in the state. In our case, we can make the wheel spin if we just change the rotation angle, so that’s the value we will need to modify to create our animation.
Animation lets us do that by using the
animate method from
Tween, which will interpolate a value on each tick from 0.0 to 2*pi (a complete spin goes from 0 to 2pi radians). I'm also using a linear curve animation because I want the wheel to spin at the same speed during the whole animation so the interpolation will be linear.
So what I’m getting with this configuration is a linear interpolation from 0.0 to 2*pi during 5 seconds. That is, the wheel will do exactly one full spin that will take 5 seconds to complete.
Rebuild on tick
We could use
setState() to update our state and rebuild, but things are much easier thanks to a special widget by Flutter called
We can use
AnimateBuilder the same way we do with
FutureBuilder but, in this case, the rebuild will be triggered by our
TickerProvider which is served through an
Animation. Then the
builder() function should return the widget we want to display. Obviously, we want this widget to change with the interpolated value from the
Tween, so we can use another handy widget from Flutter,
Transform.rotate(), that will render a widget (an
Image in our case) with a specific rotation angle.
3, 2, 1… start!
Our animation is ready, but nothing happens. Fear not, we still need to start it so, for now, we can use a temporary button to interact with it.
Every time we press the button we will start the animation or stop it depending on the current status.
It’s all gestures
Time to add real user interaction using gestures. For that, I’m going to simplify a little bit our scenario and assume that once the wheel is spinning the user won’t be allowed to stop it. So these are the use cases I want to cover:
- when the user taps down and drags the pointer (finger) the wheel will spin with it.
- as soon as the user stops dragging, the animation will start with a certain speed and direction (clockwise or anti-clockwise).
So drag me, baby
We did something very similar to this in a previous article with our circular-slider and this is much simpler because we don’t have to deal with canvas but we still need to do some operations to transform our drag coordinates to radians to calculate the rotation. I created a class SpinVelocity that abstract a few utility methods, so feel free to look for the implementation in the Github repo as here I will just use them and assume it’s working as I expect.
_updateLocalPosition() will transform a global (in the screen) position for the pointer to a local one (in the wheel), while
_moveWheel() will calculate the new angle by transforming the previous offset into a radians value between 0 and 2*pi and rebuilding the widget.
Note that there is no animation here, this is a regular rebuild from a
StatefulWidget. Now we just need to temporarily break our animation code and build the image with a new rotation angle.
Time to integrate the animation into our mix. We want to start the animation when the user drag movement ends and the pointer stops contacting the screen. Before jumping into the code there are a few things we need to consider:
- The velocity for the wheel is not constant and the spin will probably exceed one full rotation, so our Tween can no longer go from 0.0 to 2*pi radians; instead, if I know the initial circular speed of the wheel and I establish a fixed deceleration I can calculate how long the spin is going to last. I will use now an interpolation from 0.0 to 1.0, being 1.0 the total duration of the animation.
- Now the animation will probably not start from a rotation angle of value 0 as the user might have been dragging the pointer and rotating the wheel so we will need to consider an initial rotation angle when calculating the final position of the wheel.
- I created another utility class
NonUniformCircularMotionthat will take care of calculating speeds and decelerations, again feel free to check the source code but I will assume everything works as I expect.
First, let’s add a new listener to the wheel for
onPanEnd that will execute the following method
Here I calculate the velocity for the wheel based on the
pixelsPerSecond value that Flutter graciously provides for the end of the drag movement. This velocity will be negative for anti-clockwise rotation, and then I can also calculate the total duration for the whole spin, update my
AnimationController with it and start the animation calling the
Once I trigger the start of the animation,
AnimatedBuilder will be executed.
As you can see I squeezed another function in there that will be updating the values that the
builder() function depends on. Let's see it:
On the first condition in
updateAnimationValues() I update the current distance (again in radians). Calculating the current time first using the
_animation.value interpolation value and using an utility function from
NonUniformCircularMotion then to calculate the total distance covered during the animation, positive for clockwise and negative otherwise.
Finally, if the spin is over (the animation is finished) I want to set the distance back to 0 and calculate the initial spin angle for the next time the animation is triggered, but to simplify things I use a modulo function to limit the values from 0.0 to 2*pi again.
All we need is love
Everybody deserves some love, even our wheel which, at the moment is a bit useless. Let’s add a couple of new features that will make a difference.
We have a winner
How do we know who won? Well, let’s add a simple feature so that the user can also provide a secondary image that will be rendered on top and will remain static during the animation.
We just need the image and the dimensions, and we will make sure it remains fixed and centered in the wheel by using a
Stack widget and some minor calculations.
But really, who is the winner
We got a visual indicator now for the winner, but we want to make sure we can react to it in our code, so let’s just add some extra parameters to deal with it.
First, we need to know the divisions in our wheel, and since we can use any png to do that, we can not infer that value, it needs to be provided. Of course, all divisions have to be equal in angle.
Again we will use a method
anglePerDivision() from our
NonUniformCircularMotion class to calculate the angle for each division inside the
initState() function, as this value will remain constant through the animation.
Now we just need to calculate the
currentDivider every time the wheel status changes.
modulo representing the current rotation angle for the wheel and knowing the angle per division we have all we need.
Now, our last step will be informing the outside world about it, so we can use a callback from our constructor and execute it right after
That’s all folks
Some recap about what we did:
- Display an image with specific dimensions and make it interactive. This is easily done with
GestureDetectorand a standard
StatefulWidget, plus a
GestureDetectorto detect the end of the drag movement and calculate the velocity and duration of the animation.
- Animate the wheel from an initial position all the way to the end using
AnimationBuilderto rebuild the widget on every tick of the animation.
- Connect it to the world with a simple callback function.
That’s not 200 lines code!
You’re right, it’s even less. Here you have the complete code.
You can check the final version in https://github.com/davidanaya/flutter-spinning-wheel where I added some extra configuration options. Thanks for reading!
Originally published at https://www.davidanaya.io on April 22, 2019.