Drawing Every Line Pixel-perfectly with Custom Render Objects — Pure #FlutterClock face
Tags: Programming, Flutter, Design, Dart, Technology

Doing custom layouts, as I did for the clock challenge, takes a bit of extra effort because you need to handle layouting, painting, semantics (coming up next), and some other stuff for all your children yourself 😵. However, doing so allowed me to do something like what you can see below in a performant way. Each component only renders when it needs to (see repaint rainbow in the repainting section) and I can reuse every component anywhere I want to.

GIF capture of the spin-up animation with slow animations and paint baselines enabled

The components of the Canvas Clock

There are three main functions of the clock display: showing the time, the weather, and the temperature. Additionally, the location and date are indicated, however, not so prominently because they do not change a lot.

You can see the components that actually carry semantic information (unlike the background or the ball, which are pure aesthetics and joy 😸) in this screenshot of running my clock face through the SemanticsDebugger :

This is the information a visually impaired person might see when accessing the clock face using accessibility tools.

I was able to very easily add this information to my RenderObject s like so:

As I mentioned, some components do not carry any important information, so they can be excluded from the semantics tree:

I excluded (…) the process of obtaining the render objects. You can find the full method here.

As you can see, I am declaring which children of my multi-child render object are toBeVisited . These are determined by a hasSemanticsInformation flag I am setting in the parent data of the children.

Remember that I mentioned that the clock face is being drawn without any packages, assets, or whatsoever? Here is my pubspec.yaml file:

The Pubspec of my submission. The file can be found here.

DevTools’s representation of the widget tree

Oh, and about the “no prebuilt widgets” part — I actually mean it. Every render object of the clock face is manually inserted by me. As you can see on the left, every widget is custom made. Or do you see any familiar ones 😁?

This gives me complete control of the layout as discussed earlier. The ball dropping and movement of components in general is possible because of it.

Flutter’s pixel-perfect UI is fully realizable with this concept and we can also easily perform some optimizations. Everything I say also applies to widgets because all widgets are built this way. Doing it yourself simply gives you all the control 🌟

Painting and repainting (optimization)

GIF of the clock face running with the repaint rainbow enabled

As I mentioned above, the individual components only repaint (render) when they need to. You can see it for some components in the GIF: the border around the thermometer does not change color until either the theme is updated or the temperature changes (which are both animations). For other components, it is a bit harder to spot 🧐. For example, the rectangle around the slide (the bigger one at the top) only repaints when it needs to open up for the ball to pass through. You cannot see it for the weather icons because the rectangles overlap, but only the current weather condition updates (because it is the only one animating).

Now, let me tell you: this is not the case by default 😯 — by default, every child repaints when surrounding parts of the tree do. And this might surprise you because you think that Flutter is a performant framework — and it is 💪. The widgets you normally use place RepaintBoundary s themselves, i.e. the awesome people at Flutter that worked on them already did the optimization for you. A repaint boundary simply stops children from updating when other children update (see docs for more accuracy). Example: the rain icon does not need to update when only the sun icon is animating.

Repaint boundaries in render objects

When you write RenderObject s yourself, as I did for this challenge 😅, you will have to deal with repaint boundaries yourself. However, it sounds scarier than it is. It is as easy as writing the following code:

Part of the RenderWeatherIcon render object

You might be asking yourself how a render object actually repaints itself because we cannot simply call setState 🤔. The answer is a bit more complicated, but it boils down to marking different parts of the tree as dirty. You can also markNeedsSemanticsUpdate (which I needed to do for the semantics implementation) or markNeedsLayout . Mostly you will want to call markNeedsPaint and in the case of the weather icon animation, it is pretty straight forward:

Also part of RenderWeatherIcon (render object)

As you can see, we can just say: “Repaint when animating”, i.e. “ markNeedsPaint when the value of animation changes”. And this is also where the repaint boundary comes into play. When one weather icon marks that it needs to paint again, the other ones should not also repaint 🎉 This is awesome, right? Again, a bit more complicated in practice because there are e.g. also Layer s and thus some limitations, but this is the basic idea.

Drawing in render objects

When it is time to paint the render object, paint is called. In the case of our weather icon example from my clock face, there is quite a bit of stuff that happens in there. The canvas is rotated and translated and there are some save and restore operations because of this (read my explanation of what that is here). But most importantly, we draw our icon using drawCondition , which I implemented for every icon:

paintIcon is part of the RenderWeatherIcon.paint method and calls drawCondition.

The mighty Canvas

So now that I outlined how painting in render objects works 🥴, we can get to the fun part: the dart:ui Canvas . This is just a Dart wrapper for the SkCanvas from Skia, which is Flutter’s graphics engine. At the very beginning of this article, I had an illustration of the Skia debugger, showing draw operations of my clock face. Now, you know where it comes from:

An extremely simplified view of the path to Skia. There are other elements (😉), i.e. other trees, and components in Flutter that I will not be discussing. Enjoying my MS Paint skills 😂?

You already saw how the sun icon is drawn above. Now, I want to go into a little more canvas details and show you some more features I used 😋

An icon for the windy weather condition

It is probably clear how the horizontal lines in this icon for the windy weather condition are drawn: simply a lineTo(x, y) from another point. However, how do we get the curved lines at the ends 🤔?

Bézier curves are the answer. These are basically lines with a start point and an endpoint that have a number of n - 1 control points, which pull the line towards themselves. The result is a curve like the ones you can see in the image. The Skia canvas in Flutter supports Bézier curves with n = 2 and n = 3 , i.e. quadratic and cubic curves. The ones I used for the wind symbol would require n = 4 , i.e. three control points, but that is not supported — so I just combined a quadratic and a cubic curve:

Excerpt from _drawWind, which draws the lines for the wind

As you can see, I moveTo a point and then draw a straight lineTo some other point (the horizontal line). From there, I add the quadraticBezierTo an endpoint with a control point (first and second parameter). And at the end, I append a cubicTo my destination. The control coordinates come first as parameters in the x, y format. As this is a Path , I can trim it using PathMetric.extractPath :

Animation making use of an awesome trim path implementation by Luigi Rosso.

It just shows how versatile Flutter really is. Functionality like this is actually what enables packages like Rive to work as flawlessly as they do!

You might have noticed the shadows below some of the parts — even that is built into Flutter’s canvas!