Drawing Every Line Pixel-perfectly with Custom Render Objects — Pure #FlutterClock face

creativecreatorormaybenot
Flutter Community
Published in
12 min readJan 30, 2020

Everything is painted purely with Flutter’s Canvas: no assets, no packages, and no prebuilt widgets were used.

My entry to the Flutter Clock challenge running on Flutter web

I saw the Flutter Clock challenge coming up and was not very intrigued at first (we all know how awesome it was now 🤷). I felt like this was just going to be another compilation of external tools (like Rive, formerly Flare) with not a lot of tinkering being done in Flutter itself, but then it occurred to me:

Why not make use of my rendering layer knowledge and create these kinds of animations from scratch with complete layout and canvas control?

A 30-second preview of my Flutter Clock submission

I chose to approach the challenge in a playful manner. Perhaps the clock face might seem a little chaotic, but I will show you exactly what it is made up of:

Assembly of two frames of my clock face visualized (every single draw call)

The layout

Important to everything I will explain in the following is the way I laid out the parts of the clock face and how I painted them to the screen.

MultiChildRenderObjectWidget

This is what allows me to layout everything and it is really fancy 😏 First of all, I can size my children based on the given constraints, i.e. use relative/adaptive sizing:

My clock face on Flutter web adaptively resizing its parts

Technically, this can also be achieved by just using a LayoutBuilder, which provides you with the constraints available. More importantly, though, I can position components based on the size and position of other components 🚀

This is particularly useful with text because I cannot know what space my string is going to take up on the screen prior to drawing it. I have here a slightly modified version of my clock. See how the location always stays centered and the background and the weather dial react to it 😃?

Layout responding to the dimensions of dynamically entered text with paint baselines shown

The only changing part in this GIF is the location text. The background (dark brown curve) and the weather dial automatically adapt to the text’s changing size 😄. The code for this is placed in multiple render objects — so I summarized it for you:

Summary of how the TextPainter affects the rest of the layout

You might be able to tell that declaring a layout like this, as well as being able to access text layout information through a TextPainter, is quite powerful 👌.

If you want to see how I set up MultiChildRenderObjectWidgets for this project (one for the whole layout and one for the weather dial), you can take a look at my abstract composition definition here. Furthermore, I have some resources to get into these custom layouts at the end 👍

RenderBox

Somehow I need to actually draw my stuff to the screen 🤔 Canvas to the rescue!

A RenderBox is this neat little thing that gives me a canvas to draw whatever I want to the screen. Most of these boxes are inserted using LeafRenderObjectWidgets. See how I was able to draw the sun icon using some very simple operations:

Enlarged sun icon, so you can better see it 🙂
Part of drawCondition in RenderSunny responsible for drawing the sun icon

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 RenderObjects 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 RenderObjects 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!

Use of Canvas.drawShadow for the second hand

Given any Path, which most of my components use, you can easily add a shadow to it. You only have to give a color, elvation, and specify whether your object (shape described by the path) is opaque.

Below you can see the draw operations the canvas performs for two frames of the clock display again (different ones than at the beginning). Maybe you will recognize some parts now 🤯:

Skia debugger visualizing draw operations for two separate frames of my clock face — had to remove all shaders (gradients) because they are not supported.

Animations

A fundamental part of my clock design is animations — you will most likely have spotted them all over the place, e.g. above for the wind icon 💨. They control every single component in some way because everything at least has a color and all color palettes are animated in my submission. There are explicit and implicit animations. The color animations are implicit, but I want to start with:

Explicit animations

Every explicit animation begins with an AnimationController. You have probably heard of it already — it allows us to animate between some values (0 to 1 in my case) over a given duration with a curve if needed.

The second hand of the analog clock bouncing

You can see that the hand is not linearly moving from one second to the next. Instead, it has an elastic bounce to it. Flutter comes with the ElasticOutCurve, but I was not quite satisfied with it for the hand bounce. I watched this video of a real watch and with the help of a math expert, I was able to do some envelope transformations with the original curve 🎊. I arrived at the following equations:

HandBounceCurve is a subclass of Curve.

Supplying the values from my animation controller ranging from 0 to 1 to the transform method of this curve gives me the awesome bounce animation 🥂.

I simply start the animation controller once a second and get this amazing effect imitating a real clock ⏰.

Implicit animations

Implicit only means that I do not have to start the animation manually — the animation is started by the framework automatically whenever a given value changes. This works really nicely for the thermometer because I can animate to the new temperatures anytime without any extra work 🤑

Temperature (and themes) being changed.

You can see how the temperature animates to its new values on change. The color changes come from the automated customization flow I implemented.

The animation even works with temperature unit changes from Fahrenheit to Celsius and vice versa. It comes down to creating some Tweens and populating them with new end value when the temperature changes. Flutter provides the ImplicitlyAnimatedWidget, which then takes care of the animation for you 🥳.

Watch this episode of Flutter in Focus, an awesome video series by the official Flutter team, to learn everything you need to know about creating your own implicit animations.

View my implementation of the thermometer animation here.

In general, I applied this and all the other features I shared for more or less every component in the clock face. I find the color transitions to be one very nice and easy stunning animation. It is an implicit animation that I can place at the top of the tree to animate color changes for all children. Theme does this for you already out of the box 🎁, but I have a lot of different colors and created Palette to achieve it for my design. It contains every color of my clock face (e.g. a raindrop color that is used in multiple places). Then, I created four different palettes, two for light and two for dark theme. There is a vibrant and a subtle palette for both — looking back at the video(s), maybe you can spot it 🧐?

All of this might seem complicated but fear not because widgets in Flutter are only made to do that job for you (see Widget catalog). You can also look into CustomPaint, which allows you to draw custom shapes with a Canvas and even optimizes raster caching, which I did not do. If you want to create customized layouts that you cannot achieve with a Stack, you can go for a CustomMultiChildLayout. This is the less complex version of what I used and you can read my comprehensive explanation for how it works and how to use it here.

Short GIF version of my showcase video

Obviously, I did not show all of the code required to make the clock face work — there is a lot more to explore 🙂, e.g. the spin-up/flip-up animation and the ball dropping + slide and bounce movement. If you are interested in learning more about it, you can view the whole source code on GitHub and you can also see exactly my process of creating it. I have pushed every commit to the repository, which allows you to view my progression towards the end of my #FlutterClock journey.

If you want to know why I chose this difficult route, even though it would have been possible to achieve it more easily (e.g. using Rive or CustomPaint), the answer is the following: the #FlutterClock challenge gave me the ability to play around with this stuff (Flutter is fun ✨). This is not the way to go for regular Flutter projects, but I enjoyed doing so and it gave me the ability to do exactly what I wanted without any limitations. See the second part of this explanation for more elaboration on this.

Aaand, that is a wrap 🌯 for my first ever article 🙌

I had a lot of fun with it and hope you enjoyed the read, found something interesting and potentially new, and most importantly that you could learn something.

I always appreciate feedback of any kind! You can leave it as a response or contact me otherwise. Reach out to me on Twitter or via email (<my_github_username>@gmail.com) — I am friendly 🙃

I wish you a fun time developing and if there is one thing to take away from all of this, it is that Flutter is powerful 🚀.

--

--