tl;dr
- Rotation of widgets can be applied using the Transform. The easiest usage is the named constructor
Transform.rotate()
- Rotating objects on canvas is not directly possible. Instead, we need to apply the rotation to the whole canvas and draw the object onto that
- The respective reusable function is described under coding
It’s a common task to rotate objects on the screen in a certain angle. While widgets can be wrapped with the Transform widget in order to perform the rotation, it’s not that easy for objects being drawn on Canvas.
Since we have no widget tree there and everything is a little bit more low-level, we need to perform the rotation ourselves.
How to rotate widgets
Let’s give the whole topic a shallow, pleasant start by having a look at how rotation works with widgets.
Like I said, the general way for transformations of widgets inside the widget tree is the Transform
widget.
It can be used like this:
1Transform(
2 angle: bacteria.rotation,
3 transform: Matrix4.rotationZ(
4 0.25
5 ),
6 child: Container(
7 width: 24,
8 height: 24,
9 color: Colors.black,
10 ),
11);
angle
is described as a double in radians where2 * π rad
equals to 360°.transform
is a 4d matrix where values are stored in column major order.
This sounds more complicated than it is. The only things you need to remember are the following:
- If you want to know the radian angle based on the angle in degree, just calculate
degree * π / 180
.
If you have the angle as number of full circles (e. g. 1 is 360°, 1/2 is 180°), you can just calculatefull_circle_amount * 2π
- Luckily, you can apply the same to the
Matrix4
and don’t have to create your matrix manually. The named constructorMatrix4.rotationZ()
expects the angle in radians as well
And there is even a named constructor of the Transform
widget that completely abstracts from matrices: Transform.rotate()
.
Use it like this:
1Transform.rotate(
2 angle: 0.25,
3 child: Container(
4 width: 24,
5 height: 24,
6 color: Colors.black,
7 ),
8);
Why use canvas
If there are widgets for the very task we are trying to accomplish, then why even bother with the low-level canvas, you might ask yourself.
Although Flutter has a lot of built-in performance optimizations, there are enormous differences between the widget tree and the canvas when it comes to speed.
This is mostly because widgets are on a higher abstraction level, carrying a lot more information than just how to be rendered. You can get an idea of the complexity by reading the article about BuildContext
.
How to rotate objects using CustomPaint / Canvas
When using the canvas, things are a little bit more complicated. That’s because the objects that can be drawn on the canvas like Rect
can not be rotated.
How do you rotate drawings if they do not provide an API for that?
An illustration
Let’s illustrate this with a real world example:
We assume we have a friend that is excellent at drawing. He has one problem, though: he can only draw his perfect objects upright.
Instead of making him draw the shape rotated, we just rotate the sheet of paper, let him draw and rotate it back.
The question is: how exactly do we rotate the paper? If we just rotate it using the upper left corner as the center of the rotation, the drawing may not even be on the sheet.
If we always use the center of sheet as the center of the rotation, the drawing might be displaced.
- Assuming the pen of the drawer is on the top left of the sheet (which it usually is), we need to move the sheet by the amount the object we want to have drawn on is displaced (relative to the top left of the sheet).
- Then we rotate the sheet around its center.
- We let the person draw its objects
- Afterwards we move the sheet back to where it was so that further drawings are not rotated anymore
Let’s illustrate how it looks if we want to draw the Flutter logo on the bottom right of the paper frame:
Coding
Okay, now we understood the reference of the real world. But how do we apply this to Dart code?
1canvas.save();
2canvas.translate(center.dx, center.dy);
3canvas.rotate(angle);
4canvas.translate(-center.dx, -center.dy);
5drawOurObject();
6canvas.restore();
Basically, we do something similar: before the rotation, we save the current canvas, because we want to restore it later.
After that, we do some translation so that the upcoming rotation actually the center of the object we want to draw.
Having rotated the canvas and translated back, we actually draw the object.
Finally, we need to restore the canvas, because we don’t want the transformation to be applied to all upcoming drawings.
Now we’d have a lot of code duplication if we copied and pasted these lines everywhere we want to rotate something, so let’s put it into a reusable code snippet:
1void drawRotated(
2 Canvas canvas,
3 Offset center,
4 double angle,
5 VoidCallback drawFunction,
6) {
7 canvas.save();
8 canvas.translate(center.dx, center.dy);
9 canvas.rotate(angle);
10 canvas.translate(-center.dx, -center.dy);
11 drawFunction();
12 canvas.restore();
13}
That’s it! Because we want every possible drawing to be rotated, we expect a function instead of an object. There is no unified interface for every drawable object on the canvas like a draw()
method.
Let’s see how the usage would look like:
1final Rect rect = Rect.fromLTWH(
2 object.x,
3 object.y,
4 object.width,
5 object.height,
6);
7
8drawRotated(
9 canvas,
10 Offset(
11 object.x + (object.width / 2),
12 object.y + (object.height / 2),
13 ),
14 math.pi / 2,
15 () => canvas.drawRect(roundedRectangle, paint),
16);
We’re assuming we have an object
with the properties x
, y
, width
and height
.
We use our newly created drawRotated()
function by providing the Canvas
object, the center of the object the rotation amount - in this case 90 °, a fourth of a circle as a circle is 2 * pi
.
Conclusion
Rotating object on Canvas
is not as intuitive as it is in the widget tree. We can not apply a rotation to an object we want to draw.
However, we can just rotate the whole canvas and apply the resulting Matrix
to it.
By having created the analogy of a rotated sheet of paper and a person that can draw only upright, we could improve our understanding.
Also, we have created a reusable function to make it work for everything drawn on Canvas
.
Comment this 🤌