Tutorial: Responsive GUI layouts using FlexBox and Grid

Build responsive GUI layouts that work across different screen sizes and orientations using the FlexBox and Grid classes. Learn how to quickly visualise Components using the Projucer Live Build editor.

Level: Intermediate

Platforms: Windows, macOS, Linux, iOS, Android

Classes: FlexBox, FlexItem, Grid, GridItem

Getting started

This tutorial assumes understanding of simple layout techniques using the Rectangle class as explained in Tutorial: Advanced GUI layout techniques. If you haven't done so already, you should read that tutorial first.

Download the demo project for this tutorial here: PIP | ZIP. Unzip the project and open the first header file in the Projucer.

If you need help with this step, see Tutorial: Projucer Part 1: Getting started with the Projucer.

The demo project

The demo project demonstrates different responsive layout techniques using FlexBox and Grid objects when dealing with variable screen sizes and resolutions. If we first run the project in its initial state, it should look something like this:

The demo project application window

Right now, the layout uses common non-responsive techniques to lay out the components on the screen and does not accomodate for orientation changes. We will make use of the FlexBox and Grid items to eradicate these problems.

The Projucer Live Build engine

Note
This section is optional for the purpose of this tutorial. Feel free to skip to the next section Using FlexItem and GridItem objects if you want to dive straight into responsive layouts.

Using the Projucer's Live Build engine, we can easily preview GUI components and iterate quickly over design decisions. Some of the useful features of this engine is the ability to simulate common phone and tablet screen sizes as well as flipping the device orientation all within the Projucer window. It removes the hassle of rebuilding projects and having to launch virtual mobile simulators every time we make a small change when working on mobile platforms.

In order to access the Live Build engine, click on the right tab with a toolkit icon located on the left side panel of the project window. Then click on Enable Now to start compiling the project within the Projucer.

Enabling the Live Build engine in the Projucer
Note
In order for the engine to preview the object of a class, it must inherit from the Component class.

The Projucer Live Build macro

The Live Build engine presents a constraint where components need to have a default constructor defined in their class. This may present inconveniences when developing outside of the Live Build engine. Fortunately, we are provided with a special JUCE_PROJUCER_LIVE_BUILD macro to determine our build environment and execute the default constructor only during a live build.

Let's start by modifying our constructor initialisation of our side panels in the MainContentComponent class to accomodate different build-time scenarios. At the moment, our RightSidePanel struct is implemented using a non-default constructor with a single argument to define the background color of the panel:

struct RightSidePanel : public Component
{
RightSidePanel (Colour c) : backgroundColour (c)
{
//...

The following code adds the JUCE_PROJUCER_LIVE_BUILD macro to remove the argument in live build mode:

struct RightSidePanel : public Component
{
#if JUCE_PROJUCER_LIVE_BUILD
RightSidePanel() : backgroundColour (Colours::lightblue)
#else
RightSidePanel (Colour c) : backgroundColour (c)
#endif
{
//...

As we can see, when testing inside the Projucer we have to explicitly set the background color, in this case in light blue inside the member initialisation list. The same applies to the LeftSidePanel with the same code adjustments.

We also have to modify our member initialisation list of our MainContentComponent() constructor in the MainContentComponent class as the color-specifying argument no longer applies:

MainContentComponent()
#if ! JUCE_PROJUCER_LIVE_BUILD
: rightPanel (Colours::lightgrey),
leftPanel (Colours::lightblue)
#endif
{
//...

Here, we have added the same macro to check whether we are building inside the Projucer. Now we should be able to use the Live Build engine to start visualising our GUI components. To launch the visualiser window, we can click on the corresponding launch button next to the class we want to examine as shown in the following image:

Launching the GUI visualiser

This popup window can be dynamically resized and presents us with useful features notably:

  • [1]: Zooming to help us scale the content to our screens when debugging.
  • [2]: Setting common device sizes from a dropdown list.
  • [3]: Changing the orientation from portrait to landscape and vice versa.
  • [4]: Reinstantiating Components after modifications.
  • [5]: Viewing the corresponding section of the code relating to the Component.
Note
The source code for this modified version of the code can be found in the FlexBoxGridTutorial_02.h file of the demo project.

The FlexBox and Grid layout systems

The FlexBox and Grid classes are highly inspired by the responsive layout practices used in CSS web development. If you have designed a responsive website before, you should be familiar with the layout systems described in this section.

When using FlexBox, we first need to define the direction of layout as horizontal or vertical and every subsequent computation will be executed on this basis. We call this direction the main axis and its perpendicular counterpart the cross axis. Based on this information the following properties will affect the layout as follows:

  • Justification affects the position of items along the main axis.
  • Alignment affects the position of items along the cross axis.
  • Wrapping is performed on overflow of items on the main axis by spilling on the cross axis.

The items inside the container are defined by the FlexItem class and have 3 flexible properties that affect its dynamic resizing:

  • Flex-grow defines the ability for an item to grow if necessary.
  • Flex-shrink defines the ability for an item to shrink if necessary.
  • Flex-basis defines the default size of an item before dynamic resizing.

As a two-dimensional layout system, Grid works on both the row axis and the column axis. Similarly to FlexBox, the following properties will affect the layout as follows:

  • Justification affects the position of items along the row axis.
  • Alignment affects the position of items along the column axis.
  • Wrapping can be performed on overflow of items on rows or columns.

GridItem objects are contained within the Grid and have useful properties that affect its size:

  • Margin can provide gaps around specific items.
  • Span can extend items to fill more than one grid cell.

Now that we know how specific properties can affect these layout systems we can start implementing those changes in our demo project.

Using FlexItem and GridItem objects

Let's start by replacing the button layout in the RightSidePanel::resized() method using FlexBox:

void resized() override
{
FlexBox fb; // [1]
for (auto* b : buttons) // [5]
fb.items.add (FlexItem (*b).withMinWidth (50.0f).withMinHeight (50.0f));
fb.performLayout (getLocalBounds().toFloat()); // [6]
}
  • [1]: We create a FlexBox object.
  • [2]: We can specify whether we want our objects to wrap around in case of overflow.
  • [3]: We justify the content to the center of the bounds.
  • [4]: We specify the alignment of the content to the center.
  • [5]: We iterate over the TextButton components and add them as FlexItem objects to the items array of the FlexBox object. The FlexItem can be constrained as in this case where we set the minimum width and height of the button. We can alternatively set its maximum width and height using the withMaxWidth() and withMaxHeight() methods respectively.
  • [6]: We perform the layout logic on the FlexItem objects by specifying the bounds to the performLayout() method.

As for the rotary slider layout on the left side panel, we adjust our LeftSidePanel::resized() method accordingly:

void resized() override
{
//==============================================================================
FlexBox knobBox;
for (auto* k : knobs) // [2]
knobBox.items.add (FlexItem (*k).withMinHeight (50.0f).withMinWidth (50.0f).withFlex (1));
//==============================================================================
FlexBox fb;
fb.items.add (FlexItem (knobBox).withFlex (2.5)); // [4]
fb.performLayout (getLocalBounds().toFloat());
}
  • [1]: This time we specify that we want to have the items spaced out by specifying the JustifyContent::spaceBetween property.
  • [2]: The knobs are added in the same way to the items array with an additional flex-grow value of 1. The flex-grow factor determines the amount of space inside the container that the item should take up.
  • [3]: Another FlexBox is created to act as a container for the previously created FlexItem objects and the main axis of the flex layout is set with the Direction::column property.
  • [4]: The previously defined FlexBox is added as a FlexItem to the container FlexBox with a flex-grow factor of 2.5.

Nesting FlexBox objects allows us to create intricate responsive layouts with ease by encapsulating smaller groups of Components together.

Lastly, we can deal with the main panel sliders by making them responsive to orientation changes in the MainPanel::resized() method:

void resized() override
{
auto isPortrait = getLocalBounds().getHeight() > getLocalBounds().getWidth(); // [1]
FlexBox fb;
for (auto* s : sliders)
{
s->setSliderStyle (isPortrait ? Slider::SliderStyle::LinearHorizontal
: Slider::SliderStyle::LinearVertical); // [3]
fb.items.add (FlexItem (*s).withFlex (0, 1, isPortrait ? getHeight() / 5.0f
: getWidth() / 5.0f)); // [4]
}
fb.performLayout (getLocalBounds().toFloat());
}
  • [1]: First, we determine whether our device is in portrait or landscape by checking the width and height.
  • [2]: Next, we can decide on the main axis direction accordingly and set the appropriate property.
  • [3]: Similarly, we set the appropriate slider style to match the device orientation.
  • [4]: When adding the sliders to the items array, we provide a flex-basis by determining the proportion of each slider in the direction of flow.

The sliders will now accomodate to the device orientation and adjust direction accordingly.

Finally, we can change the overall layout system of our panels to use flex as well:

void resized() override
{
FlexBox fb;
FlexItem left (getWidth() / 4.0f, getHeight(), leftPanel);
FlexItem right (getWidth() / 4.0f, getHeight(), rightPanel);
FlexItem main (getWidth() / 2.0f, getHeight(), mainPanel);
fb.items.addArray ( { left, main, right } );
fb.performLayout (getLocalBounds().toFloat());
}

If we run our newly-modified code to use flex, we should see something like this:

The demo project application window using flex
Note
The source code for this modified version of the code can be found in the FlexBoxGridTutorial_03.h file of the demo project.

Let's try to implement the last portion of code using the Grid class instead. Here we create a Grid object to perform our layout operations on, just like flex:

void resized() override
{
Grid grid;
using Track = Grid::TrackInfo;
grid.templateRows = { Track (1_fr) };
grid.templateColumns = { Track (1_fr), Track (2_fr), Track (1_fr) };
grid.items = { GridItem (leftPanel), GridItem (mainPanel), GridItem (rightPanel) };
grid.performLayout (getLocalBounds());
}

However instead of specifying the flex-grow, flex-shrink and flex-basis values on the individual FlexItem objects, in this case we set the number of rows and columns on the Grid object using TrackInfo objects. The constraints can be specified in fractions or pixels by using the _fr and _px suffixes respectively. In this example we define a grid with 1 row and 3 columns with the center column taking twice as much space as the others.

Warning
Pixels in JUCE are not equivalent to physical pixels. Internal calculations convert the pixel density depending on the screen DPI resolution.
Note
The source code for this modified version of the code can be found in the FlexBoxGridTutorial_04.h file of the demo project.

Pro and cons of FlexBox and Grid classes

There are many cases where either of these classes can be used to create responsive layouts. However there are certain scenarios where one is more suitable and sometimes even necessary to solve certain layout constraints.

Some of the advantages of the FlexBox class:

  • Suitable for laying out components where a main axis is desired.
  • Content wrapping, direction and alignment is easily specified.
  • Can accomodate unaligned content over the cross axis.

Some of the advantages of the Grid class:

  • Suitable for 2D grid-type layouts where rows and columns are aligned.
  • Ratios of components can be specified in fractions or pixels.
  • Can accomodate spanning content over multiple rows or columns.
Exercise
Implement the previous FlexBox layouts using the Grid class instead. Were there any inconvenient use cases where the FlexBox class was more suitable?

Summary

In this tutorial, we have learnt how to design responsive layouts using the FlexBox and Grid classes. In particular, we have:

  • Used the Projucer Live Build editor to check GUI changes efficiently.
  • Learnt the layout logic for FlexItem and GridItem objects.
  • Handled orientation changes and adapted our interface accordingly.
  • Discussed the pros and cons for each of these classes.

See also

FlexBox::items
Array< FlexItem > items
The set of items to lay-out.
Definition: juce_FlexBox.h:145
FlexBox::AlignContent::center
Lines of items are aligned towards the center of the cross axis.
FlexBox::JustifyContent::spaceBetween
Items are evenly spaced along the main axis with spaces between them.
FlexItem::withMinWidth
FlexItem withMinWidth(float newMinWidth) const noexcept
Returns a copy of this object with a new minimum width.
FlexBox::justifyContent
JustifyContent justifyContent
Defines how the container distributes space between and around items along the main-axis.
Definition: juce_FlexBox.h:142
FlexItem
Describes the properties of an item inside a FlexBox container.
Definition: juce_FlexItem.h:41
Colour
Represents a colour, also including a transparency value.
Definition: juce_Colour.h:42
FlexBox::performLayout
void performLayout(Rectangle< float > targetArea)
Lays-out the box's items within the given rectangle.
FlexBox
Represents a FlexBox container, which contains and manages the layout of a set of FlexItem objects.
Definition: juce_FlexBox.h:47
FlexItem::withMinHeight
FlexItem withMinHeight(float newMinHeight) const noexcept
Returns a copy of this object with a new minimum height.
Grid::performLayout
void performLayout(juce::Rectangle< int >)
Lays-out the grid's items within the given rectangle.
Array::add
void add(const ElementType &newElement)
Appends a new element at the end of the array.
Definition: juce_Array.h:422
Grid::items
juce::Array< GridItem > items
The set of items to lay-out.
Definition: juce_Grid.h:208
Colours::lightgrey
static const JUCE_API Colour lightgrey
Definition: juce_Colours.h:48
FlexItem::withFlex
FlexItem withFlex(float newFlexGrow) const noexcept
Returns a copy of this object with a new flex-grow value.
FlexBox::Wrap::wrap
Items are wrapped onto multiple lines from top to bottom.
FlexBox::Direction::row
Set the main axis direction from left to right.
GridItem
Defines an item in a Grid.
Definition: juce_GridItem.h:40
FlexBox::JustifyContent::center
Items are justified towards the center of the main axis.
Array::addArray
void addArray(const Type *elementsToAdd, int numElementsToAdd)
Adds elements from an array to the end of this array.
Definition: juce_Array.h:587
Grid::templateRows
juce::Array< TrackInfo > templateRows
The set of row tracks to lay out.
Definition: juce_Grid.h:187
Colours
Contains a set of predefined named colours (mostly standard HTML colours)
Definition: juce_Colours.h:42
Colours::lightblue
static const JUCE_API Colour lightblue
Definition: juce_Colours.h:48
Component
The base class for all JUCE user-interface objects.
Definition: juce_Component.h:40
Grid
Container that handles geometry for grid layouts (fixed columns and rows) using a set of declarative ...
Definition: juce_Grid.h:44
FlexBox::flexDirection
Direction flexDirection
Specifies how flex items are placed in the flex container, and defines the direction of the main axis...
Definition: juce_FlexBox.h:121
FlexBox::flexWrap
Wrap flexWrap
Specifies whether items are forced into a single line or can be wrapped onto multiple lines.
Definition: juce_FlexBox.h:126
FlexBox::alignContent
AlignContent alignContent
Specifies how a flex container's lines are placed within the flex container when there is extra space...
Definition: juce_FlexBox.h:132
Grid::templateColumns
juce::Array< TrackInfo > templateColumns
The set of column tracks to lay out.
Definition: juce_Grid.h:184
Grid::TrackInfo
Represents a track.
Definition: juce_Grid.h:70
FlexBox::Direction::column
Set the main axis direction from top to bottom.