Tutorial: Managing Android screen sizes

Table of Contents

Build your application for different screen sizes. There are many available screen sizes on Android, this tutorial examines some strategies to manage this.

Level: Intermediate

Platforms: Android, macOS, Windows

Classes: Desktop, AffineTransform, TabbedComponent

Getting started

This tutorial illustrates a handful of strategies for managing different screen sizes on the Android platform using JUCE. There are several demo projects to accompany this tutorial. Download links to these projects are provided in the relevant sections of the tutorial.

If you need help with this step in each of these sections, see Tutorial: Projucer Part 1: Getting started with the Projucer.

The demo projects

The demo projects provided with this tutorial illustrate several different methods for managing different screen sizes on the Android platform using JUCE. Broadly, these methods are:

Android screen sizes

With devices that expect fullscreen operation in particular (such as mobile devices) it is a challenge to design user interfaces that are effective on all screen sizes and for various device orientations. This is a particular challenge on the Android platform where there are many possible screen sizes and resolutions. There are three main issues here:

The relationship between the physical size and the resolution is important. It is especially important when considering high resolution screens where the physical pixels are smaller, and more densely packed, than standard resolution screens. The combination of a particular physical screen size and its resolution results in the screen's dots-per-inch (DPI). This is related to the screen pixel density. This is how many physical pixels take up the space of a "software" pixel, in each dimension, as a pixel on a standard density screen.

For some applications the physical size will be most important. This may be, for example, where an application uses complex interaction comprising delicate finger movements. In this case the screen size and the size of the typical user's hand is important. In other applications the DPI of the screen is more important. For example, text will remain readable using smaller font sizes at higher DPIs. But there is a limit to how readable text will be, when measured using its physical size on the screen. You will sometimes need to take both the physical size and the resolution (and therefore the DPI) into account when designing your application.

By default, JUCE scales its coordinate system based on the pixel density of the screen. This means that shapes and text drawn on a high density screen should appear roughly the same physical size as they do on standard density screens. In JUCE, you can access information about a particular display via the Desktop class. Here you can find the available displays and which one is marked as the "main display" (especially if there are multiple displays).

Unfortunately, the value that JUCE can access to obtain the display's DPI is only an approximation (since not all screen devices report this information properly). This means we can't measure the physical size of the user's screen accurately. But the information provided by the Desktop class should be good enough as a guide for scaling your user interface depending on the needs of your application.

In each of the examples that follow we use a child component, called ResizingComp, that is managed and resized by the parent component (MainContentComponent).

Note
If you test these projects on macOS or Windows you will be able to dynamically resize the width and height of the main window. While this works to some extent, it is not intended as a feature of the projects, except for testing purpose. The projects are designed to expect infrequent changes in size. For example, a one-time setting when the application launches, or when the user rotates the Android device.

Resizing child components (simple resize)

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

In this example we use a simple interface containing a collection of sliders and buttons. Each of these child components is given a proportion of the screen's height (minus a small border around the edge). A similar approach could be taken in the horizontal dimension. For simplicity, the sliders and buttons just take up the whole width of the screen (again, minus a small border). With a screen in portrait orientation and a size in the region of a few hundred pixels this will look similar to the following screenshot:

tutorial_android_screen_sizes_screenshot1.png
Simple resizing of child components: Portrait

In landscape orientation it will look something like the following screenshot:

tutorial_android_screen_sizes_screenshot2.png
Simple resizing of child components: Landscape

Arrays of components

To store the buttons and sliders in the ResizingComp class we use the OwnedArray template class (which means that these child components will be deleted automatically in the ResizingComp destructor). First, in the ResizingComp constructor, we build an array of Colour objects. These are used to set the colours of the buttons, the slider thumbs, and the slider tracks:

ResizingComp()
{
Array<Colour> colours { Colour (0xffb3c3Da), Colour (0xff5973b8), Colour (0xffd65667), Colour (0xffd99154),
Colour (0xffe5ad6c), Colour (0xffecc664), Colour (0xffefe369), Colour (0xffdddB74) };
//...
Note
These happen to be the colours from the JUCE logo!

We then use a for() loop to allocate and configure the multiple buttons:

//...
for (auto i = 0; i < 6; ++i)
{
auto* button = buttons.add (new TextButton (String ("Button ") + String (i + 1)));
addAndMakeVisible (button);
colours.getUnchecked (i % colours.size()));
}
//...

And the sliders are set up similarly (although we mix up the colour selection using the array of colours to keep it interesting).

//...
for (auto i = 0; i < 6; ++i)
{
auto* slider = sliders.add (new Slider());
addAndMakeVisible (slider);
colours.getUnchecked ((buttons.size() + i) % colours.size()));
colours.getUnchecked ((buttons.size() + i + 2) % colours.size()).withAlpha (0.4f));
colours.getUnchecked ((buttons.size() + i + 2) % colours.size()));
}
//...

Using a custom slider thumb size

In order to be more usable with a touchscreen interface the slider thumb has been customised so that it is usually larger than the standard size. To do this we have added a subclass of the LookAndFeel_V4 and overridden the LookAndFeel::getSliderThumbRadius() function.

class CustomLookAndFeel : public LookAndFeel_V4
{
public:
int getSliderThumbRadius (Slider& slider) override
{
return jmin (slider.getWidth(), slider.getHeight()) / 2;
}
};

We add an instance of this class as a member of our ResizingComp class:

//...
CustomLookAndFeel lf;
};

And at the end of our ResizingComp constructor we set this as our look-and-feel for this component and all of its children.

//...
setLookAndFeel (&lf);
}

And in our ResizingComp destructor we set this to nullptr.

//...
setLookAndFeel (nullptr);
}

Resizing the buttons and sliders

In the ResizingComp::resized() function we iterate over the arrays of buttons and sliders and set their bounds:

void resized() override
{
auto space = 8;
auto widgetHeight = (getHeight() - space) / (buttons.size() + sliders.size()) - space;
for (auto* button : buttons)
button->setBounds (space, space + (widgetHeight + space) * buttons.indexOf (button),
getWidth() - space - space, widgetHeight);
for (auto* slider : sliders)
slider->setBounds (space, space + (widgetHeight + space) * (sliders.indexOf (slider) + buttons.size()),
getWidth() - space - space, widgetHeight);
}

Here we us a constant value (8) to separate the components. Then we calculate the "widget height" based on the available height and the number of "widgets" (buttons and sliders) that we have.

If the screen size is too small then the interface becomes unusable and unreadable as shown in the following screenshot:

tutorial_android_screen_sizes_screenshot3.png
Simple resizing when the screen is too small

Having said this, it should look reasonable on most Android devices.

Exercise
Vary the number of sliders and buttons in the interface.

Resizing the main component using a transform

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

This example uses an alternative to resizing the child components. Instead, the ResizingComp component is set to a nominal size (480 by 640 pixels) and then the MainContentComponent object applies an affine transform to scale this up or down to match the screen size. This is done while keeping the same aspect ratio (leaving whitespace to the side or above and below the sliders and buttons). The code for the ResizingComp class is the same as for the simple resize example. But in the MainContentComponent::resized() function we set the size of the ResizingComp component then calculate the required transform:

void resized() override
{
auto contentWidth = 480;
auto contentHeight = 640;
auto scaleX = getWidth() / static_cast<float> (contentWidth);
auto scaleY = getHeight() / static_cast<float> (contentHeight);
auto scale = jmin (scaleX, scaleY);
resizingComp->setTransform (AffineTransform::scale (scale, scale));
resizingComp->centreWithSize (contentWidth, contentHeight);
}

This code calculates the ratio between our nominal size and the actual size of the screen in software pixels. It then chooses the smallest of these ratios in order to maintain the aspect ratio, while keeping all of the content onscreen. We then create an instance of the AffineTransform class using the AffineTransform::scale() function and centre the scale transform around the centre of the screen. The transform is applied to the component using the Component::setTransform() function. The result is quite different from the simple resize method.

tutorial_android_screen_sizes_screenshot4.png
Scaling the whole UI using a transform showing both portrait and landscape orientations
Note
Applying a transform to a component not only transforms the drawing of the user interface, it also transforms the position of touch (and mouse) activity.

Designing different layouts for different orientations

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

This example looks at one method of displaying a different layout depending on the orientation of the screen (or device). The Desktop::getCurrentOrientation() function provides means of accessing the device orientation. In fact, there are four possible orientations:

For simplicity, in this tutorial, we are going to treat portrait orientation as a screen that is taller than it is wide (and landscape orientation as a screen that is wider than it is tall).

This example uses the same technique for scaling the user interface using a transform that we saw earlier. The differences are that this ResizingComp class uses a different layout depending on the orientation, and the MainContentComponent class has two nominal sizes (one for landscape orientation and one for portrait orientation). The orientation is determined in the MainContentComponent::resized() function:

void resized() override
{
auto isLandscape = getWidth() > getHeight();
auto contentWidth = isLandscape ? 640 : 480;
auto contentHeight = isLandscape ? 480 : 640;
//...

Then, in the ResizingComp::resized() function we select from two resizing functions depending on the orientation:

void resized() override
{
if (getHeight() > getWidth())
resizedPortrait();
else
resizedLandscape();
}

The resizedPortrait() and resizedLandscape() functions then use different arithmetic to layout the buttons and sliders.

When the landscape orientation is used, the buttons and sliders are shown in two columns, rather than one column. This is shown in the following screenshot:

tutorial_android_screen_sizes_screenshot5.png
Using a different layout in landscape orientation
Exercise
Change the code to use the Desktop::getCurrentOrientation() function to determine the screen orientation, rather than comparing the width and height of the screen.

Designing different layouts for different screen sizes

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

This final example uses different layouts for different screen orientations and screen sizes. This kind of technique might be especially useful if you want to use a totally different layout for screen orientations or even make a universal application for Android phones and tablets. The method employed here is to use the TabbedComponent class to arrange pages of components if they won't fit onto a single page.

The responsibility of the ResizingComp class changes a little in this project compared to the three earlier projects. In particular, we don't add the buttons and sliders as direct child components. Notice the lack of calls to the Component::addAndMakeVisible() function in the following code for the constructor:

ResizingComp()
{
Array<Colour> colours { Colour (0xffb3c3Da), Colour (0xff5973b8), Colour (0xffd65667), Colour (0xffd99154),
Colour (0xffe5ad6c), Colour (0xffecc664), Colour (0xffefe369), Colour (0xffdddB74) };
for (auto i = 0; i < 6; ++i)
{
auto* button = buttons.add (new TextButton (String ("Button ") + String (i + 1)));
colours.getUnchecked (i % colours.size()));
}
for (auto i = 0; i < 6; ++i)
{
auto* slider = sliders.add (new Slider());
colours.getUnchecked ((buttons.size() + i) % colours.size()));
colours.getUnchecked ((buttons.size() + i + 2) % colours.size()).withAlpha (0.4f));
colours.getUnchecked ((buttons.size() + i + 2) % colours.size()));
}
setLookAndFeel (&lf);
}

The ResizingComp class manages the lifetime of the buttons and sliders but in terms of component hierarchy they are added to one or more instances of another component class called ComponentHolder.

Laying out the components

The ComponentHolder class holds an array of Component pointers and lays the components out in one or two columns depending on the orientation. (The technique for achieving this layout was covered earlier when looking at different screen orientations.) There is a single function—(ComponentHolder::addComp()—for adding a component that adds it to the internal array and calls Component::addAndMakeVisible():

void addComp (Component* comp)
{
comps.add (comp);
addAndMakeVisible (comp);
}

The layout functions—ResizingComp::resizedPortrait() and ResizingComp::resizedLandscape()—should look familiar. Although these need to be slightly different since we no longer have separate arrays of sliders and buttons:

void resizedPortrait()
{
auto space = 8;
auto widgetHeight = (getHeight() - space) / comps.size() - space;
for (auto* comp : comps)
comp->setBounds (space, space + (widgetHeight + space) * comps.indexOf (comp),
getWidth() - space - space, widgetHeight);
}
void resizedLandscape()
{
auto space = 8;
auto halfComps = comps.size() / 2;
auto widgetHeight = (getHeight() - space) / halfComps - space;
for (auto* comp : comps)
{
auto index = comps.indexOf (comp);
if (index < halfComps)
{
comp->setBounds (space, space + (widgetHeight + space) * index,
getWidth() / 2 - space - space, widgetHeight);
}
else
{
comp->setBounds (getWidth() / 2 + space, space + (widgetHeight + space) * (index - halfComps),
getWidth() / 2 - space - space, widgetHeight);
}
}
}

Choosing single or multiple pages

If the screen size is large, then only one of these ComponentHolder components is created and all of the buttons and sliders are added to it. If the screen size is small then the ResizingComp class uses a TabbedComponent object and adds two instances of the ComponentHolder class to form the tabs of the TabbedComponent. This decision to use a single page or multiple pages is managed by the ResizingComp class in the ResizingComp::resized() function.

void resized() override
{
if (holder.get() != nullptr)
{
removeChildComponent (holder.get());
holder.reset();
}
auto minimumDimension = jmin (getWidth(), getHeight());
if (minimumDimension >= 480)
layoutSinglePage();
else
layoutTabs();
}

Here you can see that we're saying a "large" screen is one that has one of its dimensions greater than or equal to 480 software pixels. Of course, you can choose a different value for your applications. The ResizingComp::layoutSinglePage() function is straightforward:

void layoutSinglePage()
{
holder.reset (new ComponentHolder());
for (auto* button : buttons)
dynamic_cast<ComponentHolder*> (holder.get())->addComp (button);
for (auto* slider : sliders)
dynamic_cast<ComponentHolder*> (holder.get())->addComp (slider);
addAndMakeVisible (holder.get());
holder->setBounds (getLocalBounds());
}

Here you can see that we add all of the buttons and sliders to the ComponentHolder instance and add it as a child component of our ResizingComp object. The ResizingComp::layoutTabs() function is only a little more involved:

void layoutTabs()
{
auto orientation = getWidth() < getHeight() ? TabbedButtonBar::TabsAtBottom
holder.reset (new TabbedComponent (orientation)); // [1]
addAndMakeVisible (holder.get()); // [2]
auto* buttonTab = new ComponentHolder(); // [3]
auto* sliderTab = new ComponentHolder();
dynamic_cast<TabbedComponent*> (holder.get())->addTab ("Buttons", Colours::white, buttonTab, true); // [4]
dynamic_cast<TabbedComponent*> (holder.get())->addTab ("Sliders", Colours::white, sliderTab, true);
for (auto* button : buttons) // [5]
buttonTab->addComp (button);
for (auto* slider : sliders) // [6]
sliderTab->addComp (slider);
holder->setBounds (getLocalBounds()); // [7]
}

When the size of the screen is determined as "small" then the TabbedComponent is used as shown in the following screenshot.

tutorial_android_screen_sizes_screenshot6.png
The tabbed interface
Exercise
Increase the number of sliders and buttons in the interface and then devise a means of distributing these controls over more than two tabs if necessary.

Summary

In this tutorial we have examined various issues to do with Android device screen sizes and orientations. In particular we have:

See also