Building macOS Inspired Doc In Flutter

Rutvik Tak
9 min readAug 30, 2022

A Flutter desktop experience

Article Hero

Originally published on Pieces blog — https://code.pieces.app/blog/build-a-macos-inspired-dock-with-flutter

A couple of months back, I started doing some UI challenges✨ in Flutter. The goal of these challenges was to be creative and explore building unique UI experiences in Flutter while sticking to what the Framework offers.

As a result, I did happen to create some 😁 really interesting animations, and designs in Flutter which showcase what you can build with it. In this article, I’ll walk you through one of my creation inspired by the macOS Dock experience.

Gif showing preview of the Doc bar

By the end of this article, along with how to build this Doc, you’ll have learned a little more about

  • Constraints in Flutter
  • Implicit Animations
  • Design breakdown

Getting Started :

Download the starter project from here.

Once you build it, you’ll see a black screen but we’ll soon add our beautiful Doc there.🧑🏽‍💻

Build & run :

Starter project initial screen

Constraints And Implicit Animations :

Before getting our hands dirty in the code, I would like to ask you if you’re familiar with Constraints and Implicit Animations in Flutter?🤔

If you’re, then you can skip the next two sections which talk about Constraints and Implicit Animations in short, and 🏎 race straight up to the Design Breakdown section.

But if you’re not familiar, then you should read through the following two sections to understand some basics of these two topics.

Understanding Constraints :

UI development in Flutter is quite different from the way it’s in Html and other frameworks.

We don’t deal with sizes directly but deal with constraints. The constraints are basically a set of four points, the min-max width, and the min-max height.

There’s this one rule of thumb that guides the UI development in Flutter.

constraints rule image from flutter.dev

While you’re building UI in Flutter, you will see this parent-child composition. When the parent widget wants to lay out on the screen it checks if it has any children. If there are, then it passes its constraints to its children and asks them how big/small they want to be. The child widget may repeat this process if it has children of its own.

Once the Parent gets the sizes for each of its child widget, it then checks those sizes against its own constraints, and based on this it calculates the final size for each of its child widget and lays them out.

As you can see, the one who’s in control of determining the size and position of a widget is the Parent, Not the Child widget.

That was constraints in short! This is a way bigger topic to continue in this article. If you want to read more about this in-depth then check out the official docs.

Now that you have a basic idea of constraints, let’s march forward!

Implicit Animations:

One of the reasons building beautiful animations in Flutter is easy is that it provides many ways to animate our widgets depending on our use case.

One of the ways is using Implicit Animation Widgets or also called as AnimatedFoo widgets where Foo is the property that you want animate.

Multiple animating Implicit Animation widgets

When working with AnimatedFoo widgets, you only have to worry about providing the property value. AnimatedFoo widgets handles the animation flow, or the transition from the old value to the new value whenever the property value changes.

There are different AnimatedFoo widgets for animating different properties on a widget like AnimatedScale, AnimatedOpacity, AnimatedSize, AnimatedContainer, etc.
We’ll be making use of some of these AnimatedFoo widgets. You can learn more about them through the official docs.

Design Breakdown:

When you’re trying to build animations or any kind of interaction then try always to break down the design in parts. And then understand each part and its animations and how it’s affected by other components in the design.

Below is a breakdown of our design

Breakdown of Doc bar into items and Doc

As you can see there are two main components here :

  1. The row of items
  2. The Dock

The item’s row is stacked on top of the Doc. Both of these components have different behaviour when you hover over any one of the items. Our items scale and translate along Y-axis while the Docs width increases as items are scaled.

In the next sections, we’ll explore the working of these animations in detail and see how we can implement them.

Adding The Items :

In the lib/macos_doc.dart, replace the //TODO: add items ui with the following code:

pieces link

Let’s go over the code step-by-step :

  1. Items Row: The Row holding the items has the mainAxisSize set to mainAxisSize.min. This tells the row to size itself to the combined width of the items within it, instead of expanding to capture all the available width along the main axis.

2. MouseRegion: Flutter provides a MouseRegion widget which we can use to get notified when the user hovers on our item. When the mouse is within the item hover region we set the hoveredIndex to that item’s index and reset it to null when it exits the item’s hover region.

3. AnimatedContainer: This will be the container that will hold our item. It’s an AnimatedContainer which will animate its scale and translate itself depending on the hoveredIndex.

4. FittedBox: If the child of this widget has a size larger than the constraints passed down to it, then this widget will scale and position the child to fit within the constraints based on the fit set.

5. The item: The item in our case is a text, so I’m using the AnimatedDefaultTextStyle for scaling it. Your item can be anything else like an icon, SVG, etc. Use the appropriate animation widgets to get the smooth scale animation for them.

Build & run :

Animating The Items :

Let’s dive a little deeper into our items animation.

Separate items scale and translation animations demo side-by-side

The item has two animation properties, the first one is the change in its size and the second is its translation along Y-axis. The item in focus has the max scale and the max translation along Y-axis. The other items are then scaled and translated accordingly.

Now both of these properties change for items depending on their position from the hovered item. The trick to this animation is to calculate these two properties based on the items index and the hoveredIndex.

Let’s see how we can calculate those values!

Replace the //TODO: add getPropertyValue method with the following code :

pieces link

First of all, if you’re scared a bit😅 coze of all the math this method is screaming📢 at you, don’t worry! It’s actually really simple😄 and I’ll try my best to explain what’s going on here.

The getPropertyValue method is a general property value calculator for our scale and translation properties. It has four parameters.

  1. index: index of the item for whom the value needs to be calculated.
  2. baseValue: Value when none of the items are in focus.
  3. maxValue: Value for the hovered item.
  4. nonHoveredMaxValue: Maximum value for items around the hovered item.

Going over the method step-by-step :

  1. You check if hoveredIndex is null, if it’s then return baseValue.
  2. Here you calculate the difference between the index of the hovered item and the current. Note that we take the positive of the difference. So, if we try to plot the difference for our items by assuming the hoveredIndex is 2 then for each index we get the following differences as shown by the graph.
Graph of difference vs index

[0] -> 2, [1] ->1, [2] ->0, [3]->1,[4]->2

Note how the difference is distributed equally on left and right of the hoveredIndex.

4. For the hovered item, the difference would be 0 as hoveredIndex and index would be the same. And so we return the maxValue for it.

5. If the difference is less than or equal to the itemsAffected , then we do two things, first calculate a ratio, which is a value between 0.0–1.0 by subtracting the difference from itemsAffected and then dividing it by itemsAffected.

Graph of ratio vs index

The propertyValue is then calculated by doing a lerpDouble over the baseValue and nonHoveredMaxValue, the ratio decides the lerpDouble percentage.

To understand what lerpDouble is actually doing, take a look at the following explainer,

Visual explanation of lerpDouble method

a represents baseValue and b represents nonHoveredMaxValue .

For a ratio of 0.0, the lerp percentage would be 0%, so the propertyValue will be baseValue, for a ratio of 1.0 the lerp percentage would be 100%, i.e the nonHoveredMaxValue.

For any other value of the ratio, the propertyValue will lie within baseValue and nonHoveredMaxValue. Thus, items that are closer to the hoveredIndex will have slightly greater value than those that are far.

6. If any of the conditions don’t match, we simply return the baseValue.

Now, to finally calculate our scale value and translation values for items, replace the //TODO: add scale and translation calculator methods with the following code:

pieces link

In your items UI code, replace the baseItemHeight with the getScaledSize(index) and baseTranslationY with the getTranslationY(index) .

Test your animations by hovering over the items!🙌

Build & run :

Complete animation preview of items

Adding Doc:

The most interesting part of this design is how we size the Doc.If you look at the animation carefully, then you’ll realize that the width of the Doc is always equal to the width of the item’s row and its height is equal to the baseHeight of the item’s row when none of the items are in focus. As the item’s scale, so does the width of the doc.

But, when the items are animating we don’t know what the width of the item’s row will be at any point in the interaction. So, how can we go around this?

We’ll use our knowledge of constraints to solve this. We need to position our Doc in the Stack such that the constraints passed down to it will force it to have the maximum width of the Stack but limit the height to what we want.

Let’s see how we can do this!

Replace the //TODO: add Doc with the following code:

pieces link

Welcome, Positioned widget! This widget is used to position widgets within Stack. Here we tell the Positioned widget that the child should be positioned to 0 from the left and right edges of the Stack and will have a height equal to baseItemHeight. These constraints are then passed down to the DecoratedBox.

One thing to understand about DecoratedBox is that it will size itself to its child’s size when the child is present and the constraints passed to it are not forced constraints.

Here, as the child is not present, the DecoratedBox should have zero size but our Positioned widget forces it to scale within the constraints we defined as the DecoratedBox itself doesn’t provide any information about its size and positioning.

Hot reload the app one final time to see our complete interaction in action!💯

Build & run :

Final animation of items and Doc bar together

You did it!🥳 You now have a really cool Doc for your Flutter apps!💙

Src Code : MacOS Inspired Doc In Flutter

Next:

🙏 Thanx for spending your precious time reading the article. Hope you had a blast🙌 reading and learned some cool things!🙇‍♂️
If you want to check out my other creations then head over to this repo .

Have an AMAZING day and keep Fluttering!💙

--

--